From c412e4de5f36641a54c53200a3c20c90e06a9133 Mon Sep 17 00:00:00 2001
From: butubb <1422726308@qq.com>
Date: Tue, 7 Apr 2026 14:11:45 +0800
Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 3 +
.trae/rules/gz.md | 464 +++++
Dockerfile | 43 +
README.md | 106 +-
api/__init__.py | 0
api/callback.py | 182 ++
api/external.py | 176 ++
api/messages.py | 148 ++
api/nodes.py | 201 ++
api/plugins.py | 412 +++++
api/webhook.py | 137 ++
config.json | 15 +
config.py | 213 +++
database_service.py | 283 +++
doc/wxauto_api/api_documentation.md | 2253 +++++++++++++++++++++++
doc/wxauto_api/api_usage.md | 317 ++++
doc/wxauto_api/wxauto_docs_organized.md | 1909 +++++++++++++++++++
doc/wxauto_center/API.md | 344 ++++
doc/wxauto_center/ARCHITECTURE.md | 235 +++
doc/wxauto_center/CONFIG.md | 618 +++++++
doc/wxauto_center/DATABASE.md | 282 +++
doc/wxauto_center/DEVELOPMENT.md | 770 ++++++++
doc/wxauto_center/FLOWS.md | 662 +++++++
doc/wxauto_center/MODULES.md | 615 +++++++
doc/wxauto_center/PLUGIN.md | 615 +++++++
doc/wxauto_center/README.md | 286 +++
doc/wxauto_center/TROUBLESHOOTING.md | 619 +++++++
doc/wxautorelogin/API.md | 271 +++
doc/wxautorelogin/CLIENT.md | 375 ++++
doc/wxautorelogin/README.md | 400 ++++
docker-compose.yml | 70 +
main.py | 208 +++
models.py | 136 ++
plugin_config.json | 48 +
plugins/__init__.py | 0
plugins/advanced_sender_plugin.py | 839 +++++++++
plugins/base.py | 280 +++
plugins/builtin.py | 195 ++
plugins/custom/README.md | 125 ++
plugins/custom/ces.py | 15 +
plugins/custom/sample_plugin.py | 24 +
plugins/http_scheduled_sender.py | 251 +++
plugins/news_plugin.py | 181 ++
plugins/plugin_config_schema.py | 363 ++++
plugins/plugin_loader.py | 305 +++
requirements.txt | 15 +
run.py | 54 +
scripts/database/clean_logs.sh | 44 +
scripts/database/query_logs.sh | 35 +
scripts/database/query_nodes.sh | 29 +
scripts/database/query_webhooks.sh | 29 +
scripts/database/table_stats.sh | 54 +
scripts/start_amd64.sh | 93 +
scripts/start_arm64.sh | 93 +
services/__init__.py | 11 +
services/log_service.py | 145 ++
services/monitor_service.py | 179 ++
services/node_manager.py | 225 +++
services/plugin_notification_service.py | 148 ++
services/queue_service.py | 119 ++
services/scheduler_service.py | 66 +
static/css/style.css | 685 +++++++
static/index.html | 56 +
static/js/api.js | 155 ++
static/js/app.js | 759 ++++++++
static/js/pages/templates.js | 190 ++
utils/__init__.py | 17 +
utils/common.py | 50 +
utils/helpers.py | 77 +
69 files changed, 19321 insertions(+), 1 deletion(-)
create mode 100644 .trae/rules/gz.md
create mode 100644 Dockerfile
create mode 100644 api/__init__.py
create mode 100644 api/callback.py
create mode 100644 api/external.py
create mode 100644 api/messages.py
create mode 100644 api/nodes.py
create mode 100644 api/plugins.py
create mode 100644 api/webhook.py
create mode 100644 config.json
create mode 100644 config.py
create mode 100644 database_service.py
create mode 100644 doc/wxauto_api/api_documentation.md
create mode 100644 doc/wxauto_api/api_usage.md
create mode 100644 doc/wxauto_api/wxauto_docs_organized.md
create mode 100644 doc/wxauto_center/API.md
create mode 100644 doc/wxauto_center/ARCHITECTURE.md
create mode 100644 doc/wxauto_center/CONFIG.md
create mode 100644 doc/wxauto_center/DATABASE.md
create mode 100644 doc/wxauto_center/DEVELOPMENT.md
create mode 100644 doc/wxauto_center/FLOWS.md
create mode 100644 doc/wxauto_center/MODULES.md
create mode 100644 doc/wxauto_center/PLUGIN.md
create mode 100644 doc/wxauto_center/README.md
create mode 100644 doc/wxauto_center/TROUBLESHOOTING.md
create mode 100644 doc/wxautorelogin/API.md
create mode 100644 doc/wxautorelogin/CLIENT.md
create mode 100644 doc/wxautorelogin/README.md
create mode 100644 docker-compose.yml
create mode 100644 main.py
create mode 100644 models.py
create mode 100644 plugin_config.json
create mode 100644 plugins/__init__.py
create mode 100644 plugins/advanced_sender_plugin.py
create mode 100644 plugins/base.py
create mode 100644 plugins/builtin.py
create mode 100644 plugins/custom/README.md
create mode 100644 plugins/custom/ces.py
create mode 100644 plugins/custom/sample_plugin.py
create mode 100644 plugins/http_scheduled_sender.py
create mode 100644 plugins/news_plugin.py
create mode 100644 plugins/plugin_config_schema.py
create mode 100644 plugins/plugin_loader.py
create mode 100644 requirements.txt
create mode 100644 run.py
create mode 100755 scripts/database/clean_logs.sh
create mode 100755 scripts/database/query_logs.sh
create mode 100755 scripts/database/query_nodes.sh
create mode 100755 scripts/database/query_webhooks.sh
create mode 100755 scripts/database/table_stats.sh
create mode 100644 scripts/start_amd64.sh
create mode 100644 scripts/start_arm64.sh
create mode 100644 services/__init__.py
create mode 100644 services/log_service.py
create mode 100644 services/monitor_service.py
create mode 100644 services/node_manager.py
create mode 100644 services/plugin_notification_service.py
create mode 100644 services/queue_service.py
create mode 100644 services/scheduler_service.py
create mode 100644 static/css/style.css
create mode 100644 static/index.html
create mode 100644 static/js/api.js
create mode 100644 static/js/app.js
create mode 100644 static/js/pages/templates.js
create mode 100644 utils/__init__.py
create mode 100644 utils/common.py
create mode 100644 utils/helpers.py
diff --git a/.gitignore b/.gitignore
index 36b13f1..21603e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.pypirc
+# Docker data
+docker_data/
+
diff --git a/.trae/rules/gz.md b/.trae/rules/gz.md
new file mode 100644
index 0000000..5606912
--- /dev/null
+++ b/.trae/rules/gz.md
@@ -0,0 +1,464 @@
+# WXAuto Center AI 开发流程规则
+
+## 概述
+
+本项目是 WXAuto Center 中控系统,采用 FastAPI + PostgreSQL + Redis 架构。本规则用于规范 AI Agent 开发流程,确保代码质量和项目一致性。
+
+---
+
+## 项目结构
+
+```
+wxauto_api/
+├── api/ # API 路由层
+│ ├── __init__.py
+│ ├── nodes.py # 节点管理
+│ ├── messages.py # 消息发送
+│ ├── external.py # 外部接口
+│ ├── plugins.py # 插件管理
+│ └── callback.py # 回调接口
+├── services/ # 业务逻辑层
+│ ├── node_manager.py
+│ ├── monitor_service.py
+│ └── log_service.py
+├── models.py # 数据模型
+├── config.py # 配置管理
+├── main.py # FastAPI 主入口
+├── data/ # 数据目录
+│ ├── db/ # PostgreSQL 数据
+│ ├── redis/ # Redis 数据
+│ └── logs/ # 应用日志
+├── doc/ # 项目文档
+├── scripts/ # 部署脚本
+├── static/ # 前端资源
+└── plugins/ # 插件系统
+```
+
+---
+
+## 开发流程
+
+### 1. 需求分析 (Requirement Analysis)
+
+**原则**:在开始任何代码修改前,必须充分理解需求。
+
+#### 执行步骤
+
+1. **澄清需求**
+ - 明确功能目标和边界
+ - 确认输入输出格式
+ - 识别依赖关系和影响范围
+
+2. **影响评估**
+ - 确定修改涉及的文件
+ - 评估对现有功能的影响
+ - 检查是否有breaking change
+
+3. **任务分解**
+ - 使用 TodoWrite 工具创建任务清单
+ - 优先级排序
+ - 预估任务间的依赖
+
+#### 检查清单
+
+- [ ] 需求已明确,无歧义
+- [ ] 影响范围已评估
+- [ ] 任务已分解并创建 Todo
+- [ ] 依赖项已识别
+
+---
+
+### 2. 设计 (Design)
+
+**原则**:设计先行,避免返工。
+
+#### 执行步骤
+
+1. **接口设计**
+ - RESTful API 设计规范
+ - 请求/响应格式定义
+ - 错误码设计
+
+2. **数据模型设计**
+ - 数据库表结构(如需)
+ - 配置项设计
+ - 数据流设计
+
+3. **模块设计**
+ - 公共函数抽取
+ - 模块边界定义
+ - 依赖关系梳理
+
+#### 检查清单
+
+- [ ] API 接口设计已确定
+- [ ] 数据模型已设计(如需)
+- [ ] 模块划分已明确
+- [ ] 向后兼容性已考虑
+
+---
+
+### 3. 实现 (Implementation)
+
+**原则**:遵循代码规范,保持一致性。
+
+#### 代码规范
+
+1. **文件头部注释**
+```python
+# ///
+# filename.py
+# 描述:模块功能描述
+# 作者:AI Generated
+# 创建日期:YYYY-MM-DD
+# ///
+```
+
+2. **函数注释**
+```python
+async def function_name(param1: str, param2: int) -> Dict[str, Any]:
+ """
+ 函数功能说明
+
+ Args:
+ param1: 参数1说明
+ param2: 参数2说明
+
+ Returns:
+ 返回值说明
+
+ Raises:
+ HTTPException: 异常说明
+ """
+```
+
+3. **命名规范**
+ - 类名:PascalCase (如 `NodeManager`)
+ - 函数名:snake_case (如 `send_message`)
+ - 常量:UPPER_SNAKE_CASE (如 `MAX_RETRIES`)
+ - API 字段:snake_case (如 `node_id`, `wechat_status`)
+
+4. **路径规范**
+ - 项目根目录:`/Users/skn/Desktop/wxauto_api`
+ - 使用绝对路径
+ - 数据目录:`data/db`, `data/redis`, `data/logs`
+
+#### 执行步骤
+
+1. **创建 Todo 跟踪任务**
+```python
+TodoWrite(todos=[
+ {"id": "1", "content": "任务1", "status": "in_progress"},
+ {"id": "2", "content": "任务2", "status": "pending"},
+])
+```
+
+2. **按优先级实现**
+ - 先核心功能,后辅助功能
+ - 先底层模块,后上层调用
+ - 先接口定义,后具体实现
+
+3. **同步更新相关文件**
+ - API 路由 ↔ 服务层
+ - 前端 ↔ 后端接口
+ - 配置 ↔ 代码
+
+#### 检查清单
+
+- [ ] 代码符合注释规范
+- [ ] 函数有文档字符串
+- [ ] 命名符合规范
+- [ ] 错误处理完善
+- [ ] 无硬编码配置
+
+---
+
+### 4. 代码审核 (Code Review)
+
+**原则**:主动发现并修复问题。
+
+#### 自检清单
+
+1. **逻辑检查**
+ - [ ] 所有分支都有处理
+ - [ ] 异常情况已处理
+ - [ ] 边界条件已考虑
+
+2. **安全检查**
+ - [ ] 无硬编码密钥
+ - [ ] 输入已验证
+ - [ ] SQL 注入防护
+ - [ ] 权限检查完整
+
+3. **性能检查**
+ - [ ] 无阻塞操作在 async 中
+ - [ ] 数据库连接正确关闭
+ - [ ] 无内存泄漏
+
+4. **一致性检查**
+ - [ ] `node_id` vs `node_name` 使用正确
+ - [ ] 配置命名一致
+ - [ ] API 响应格式一致
+
+#### 执行步骤
+
+1. **代码自查**
+ - 逐文件检查
+ - 逐函数验证
+
+2. **路径验证**
+ - Grep 搜索关键路径引用
+ - 确认无残留 `wxauto_center` 等旧路径
+
+3. **导入验证**
+```bash
+python -c "from module import something"
+```
+
+#### 检查清单
+
+- [ ] 代码通过自检清单
+- [ ] 无路径错误
+- [ ] 导入测试通过
+- [ ] 配置一致性检查通过
+
+---
+
+### 5. 测试 (Testing)
+
+**原则**:测试驱动开发,验证所有功能。
+
+#### 测试类型
+
+1. **单元测试**
+ - 工具函数测试
+ - 数据模型测试
+ - 业务逻辑测试
+
+2. **集成测试**
+ - API 接口测试
+ - 数据库操作测试
+ - Redis 操作测试
+
+3. **手动测试**
+ - 前端功能测试
+ - 端到端流程测试
+
+#### 执行步骤
+
+1. **启动服务**
+```bash
+# 本地开发
+python main.py
+
+# Docker 环境
+docker compose up -d
+```
+
+2. **API 测试**
+```bash
+# 获取节点列表
+curl -X GET "http://localhost:8080/api/v1/nodes/" \
+ -H "X-API-Key: your-external-api-key"
+
+# 发送消息
+curl -X POST "http://localhost:8080/api/v1/external/send" \
+ -H "X-ExternalKey: your-external-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"node_id": "wx1", "who": "文件传输助手", "msg": "test"}'
+```
+
+3. **验证清单**
+ - [ ] API 请求成功
+ - [ ] 响应格式正确
+ - [ ] 错误处理正确
+ - [ ] 前端显示正确
+
+#### 检查清单
+
+- [ ] 核心 API 测试通过
+- [ ] 错误场景测试通过
+- [ ] 前端功能正常
+- [ ] 日志输出正常
+
+---
+
+### 6. 文档更新 (Documentation)
+
+**原则**:文档与代码同步更新。
+
+#### 文档类型
+
+1. **API 文档** (`doc/API.md`)
+ - 接口说明
+ - 请求/响应示例
+ - 错误码说明
+
+2. **架构文档** (`doc/ARCHITECTURE.md`)
+ - 系统架构图
+ - 模块说明
+ - 流程图
+
+3. **部署文档** (`scripts/README.md`)
+ - Docker 部署说明
+ - 环境配置
+ - 故障排查
+
+#### 执行步骤
+
+1. **识别需更新的文档**
+ - 新增功能 → API 文档
+ - 架构变更 → 架构文档
+ - 部署变更 → 部署文档
+
+2. **更新文档内容**
+ - 接口签名变更
+ - 响应格式变更
+ - 配置项变更
+
+3. **添加变更记录**
+ - 版本号
+ - 变更日期
+ - 变更说明
+
+#### 文档格式规范
+
+**接口文档示例**
+```markdown
+### POST /api/v1/resource
+
+发送消息
+
+**请求参数**
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| node_id | string | 是 | 节点ID |
+| who | string | 是 | 接收人 |
+| msg | string | 是 | 消息内容 |
+
+**响应示例**
+```json
+{
+ "success": true,
+ "data": {...}
+}
+```
+
+**错误码**
+| 错误码 | 说明 |
+|--------|------|
+| 404 | 节点不存在 |
+| 503 | 节点不可用 |
+```
+
+#### 检查清单
+
+- [ ] API 文档已更新
+- [ ] 响应示例完整
+- [ ] 错误码已说明
+- [ ] 变更已记录
+
+---
+
+### 7. 脚本与部署 (Scripts & Deployment)
+
+**原则**:部署脚本自动化,环境一致。
+
+#### 脚本类型
+
+1. **启动脚本** (`scripts/start_*.sh`)
+ - 环境检查
+ - 服务启动/停止
+ - 状态查看
+
+2. **Docker 配置** (`Dockerfile`, `docker-compose.yml`)
+ - 多架构支持 (arm64/amd64)
+ - 数据卷挂载
+ - 环境变量
+
+#### 执行步骤
+
+1. **更新启动脚本**
+ - 路径正确性
+ - 架构兼容性
+
+2. **更新 Docker 配置**
+ - 镜像版本
+ - 端口映射
+ - 数据目录
+
+3. **验证部署**
+```bash
+# 本地构建测试
+docker build -t wxauto-center:test .
+
+# 启动测试
+docker compose up -d
+
+# 验证服务
+curl http://localhost:8080/api/v1/nodes/
+```
+
+#### 检查清单
+
+- [ ] 启动脚本路径正确
+- [ ] Docker 配置正确
+- [ ] 数据目录已创建
+- [ ] 端口无冲突
+- [ ] 多架构支持(如需)
+
+---
+
+## 流程总结
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ AI 开发流程 │
+├─────────────────────────────────────────────────────────────┤
+│ 1. 需求分析 → 2. 设计 → 3. 实现 → 4. 审核 → 5. 测试 │
+│ ↓ │
+│ 6. 文档更新 → 7. 部署脚本 │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 关键规则
+
+1. **Todo 跟踪**:每个任务使用 TodoWrite 跟踪
+2. **先设计后实现**:避免返工
+3. **边做边测**:不要最后才测试
+4. **文档同步**:代码变更即文档变更
+5. **路径规范**:使用绝对路径,项目根目录为 `/Users/skn/Desktop/wxauto_api`
+
+### 常用命令
+
+```bash
+# 安装依赖
+pip install -r requirements.txt
+
+# 启动服务
+python main.py
+
+# Docker 启动
+docker compose up -d
+
+# API 测试
+curl http://localhost:8080/api/v1/nodes/
+```
+
+---
+
+## 附录
+
+### 配置管理
+- 开发环境:`config.json`
+- 环境变量:`.env`
+- Docker 环境:通过 `docker-compose.yml` 配置
+
+### 数据库
+- PostgreSQL:端口 5432
+- Redis:端口 6379
+- 数据目录:`data/db`, `data/redis`
+
+### 日志
+- 应用日志:`data/logs/app.log`
+- Docker 日志:`docker compose logs -f`
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e88b82d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,43 @@
+# ///
+# Dockerfile
+# 描述:WXAuto Center Docker 镜像构建文件
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+FROM python:3.11-slim
+
+WORKDIR /app
+
+RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
+ apt-get update && apt-get install -y --no-install-recommends \
+ curl \
+ tzdata \
+ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+ && echo "Asia/Shanghai" > /etc/timezone \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt .
+
+RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
+
+COPY main.py .
+COPY config.py .
+COPY config.json .
+COPY models.py .
+COPY database_service.py .
+COPY run.py .
+COPY api ./api
+COPY services ./services
+COPY utils ./utils
+COPY plugins ./plugins
+COPY static ./static
+
+RUN mkdir -p /app/logs /app/plugins/custom
+
+ENV PYTHONUNBUFFERED=1
+ENV TZ=Asia/Shanghai
+
+EXPOSE 8080
+
+CMD ["python", "main.py"]
diff --git a/README.md b/README.md
index c8a20e8..a8b64cc 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,106 @@
-# wxauto_api
+# ///
+# README.md
+# 描述:WXAuto Center 项目说明
+# 作者:User
+# 创建日期:2026-04-06
+# 更新日期:2026-04-07
+# ///
+# WXAuto Center
+
+微信多节点中控系统 - 模块化、可扩展的微信管理平台。
+
+## 功能特性
+
+- **多节点管理** - 同时管理多个微信节点
+- **消息发送** - 支持单发、群发、批量发送
+- **插件系统** - 支持自定义插件扩展、表单化配置、通知功能
+- **定时任务** - 支持 Cron 表达式的定时任务
+- **Webhook 通知** - 支持 Bark、钉钉、企业微信等
+- **外部接口** - 提供 RESTful API 供外部调用
+- **监控告警** - 节点健康监控和告警
+
+## 快速开始
+
+### 1. 启动服务
+
+```bash
+docker compose up -d
+```
+
+### 2. 访问后台
+
+打开浏览器访问 http://localhost:8080
+
+### 3. 默认配置
+
+- API Key: `your-external-api-key`
+
+## 目录结构
+
+```
+wxauto_api/
+├── api/ # API 路由
+├── services/ # 业务服务(含插件通知服务)
+├── plugins/ # 插件系统
+│ ├── base.py # 插件基类
+│ ├── plugin_config_schema.py # 表单化配置系统
+│ └── advanced_sender_plugin.py # 高级多节点推送插件
+├── static/ # 前端静态文件
+└── doc/ # 开发文档
+```
+
+## 文档
+
+- [API 文档](doc/wxauto_center/API.md)
+- [插件开发指南](doc/wxauto_center/PLUGIN.md)
+- [架构文档](doc/wxauto_center/ARCHITECTURE.md)
+- [数据库文档](doc/wxauto_center/DATABASE.md)
+
+## API 示例
+
+### 发送消息
+
+```bash
+curl -X POST "http://localhost:8080/api/v1/messages/send" \
+ -H "X-API-Key: your-external-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"node_id": "wx1", "who": "文件传输助手", "msg": "Hello"}'
+```
+
+### 插件管理
+
+```bash
+# 获取所有插件
+curl "http://localhost:8080/api/v1/plugins/" \
+ -H "X-API-Key: your-external-api-key"
+
+# 获取插件表单Schema
+curl "http://localhost:8080/api/v1/plugins/advanced_sender/schema" \
+ -H "X-API-Key: your-external-api-key"
+
+# 获取插件日志
+curl "http://localhost:8080/api/v1/plugins/advanced_sender/logs?lines=50" \
+ -H "X-API-Key: your-external-api-key"
+```
+
+### 配置告警通知
+
+```bash
+curl -X POST "http://localhost:8080/api/v1/webhook/" \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "插件通知",
+ "url": "https://api.day.app/your-bark-key",
+ "format": "bark",
+ "event_types": ["plugin_notification"]
+ }'
+```
+
+## 技术栈
+
+- **后端**: Python 3.11 + FastAPI
+- **数据库**: PostgreSQL
+- **缓存**: Redis
+- **容器**: Docker
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/callback.py b/api/callback.py
new file mode 100644
index 0000000..7b0f7ae
--- /dev/null
+++ b/api/callback.py
@@ -0,0 +1,182 @@
+# ///
+# callback.py
+# 描述:回调接口,用于重置和检测节点微信状态
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from fastapi import APIRouter, Depends, HTTPException
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+
+from services.node_manager import node_manager
+from services.log_service import log_service
+from database_service import get_db_service
+from utils import require_api_key
+
+
+router = APIRouter(prefix="/callback", tags=["回调接口"])
+
+
+class NodeResetRequest(BaseModel):
+ node_id: str
+
+
+class NodeCallbackRequest(BaseModel):
+ node_id: str
+
+
+async def sync_node_from_db(node_id: str) -> bool:
+ db_service = get_db_service()
+ if not db_service:
+ return False
+ node = db_service.get_node(node_id)
+ if not node:
+ return False
+ existing = node_manager.nodes.get(node_id)
+ if existing:
+ existing.api_url = node.api_url
+ existing.api_key = node.api_key
+ else:
+ await node_manager.add_node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ return True
+
+
+@router.post("/node/reset", summary="重置节点微信状态")
+async def reset_node_wechat_status(request: NodeResetRequest, _=Depends(require_api_key)) -> Dict[str, Any]:
+ """
+ 重置节点的微信状态为online
+ 通过发送消息到文件传输助手来检测微信是否正常
+ 如果发送成功,则重置状态为online
+ """
+ node_id = request.node_id
+ await sync_node_from_db(node_id)
+ node = await node_manager.get_node(node_id)
+ if not node:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+
+ await node_manager.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+
+ result = await node_manager.call_node_api(
+ node_id,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": "文件传输助手", "message": "WXAuto Center 状态检测消息"}
+ )
+
+ if result.get("success"):
+ node.wechat_status = "online"
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status="online")
+ log_service.success(f"Node {node_id} WeChat status reset to online", "Callback")
+ return {
+ "success": True,
+ "message": "WeChat status reset",
+ "data": {
+ "node_id": node_id,
+ "wechat_status": "online",
+ "check_result": "success"
+ }
+ }
+ else:
+ node.wechat_status = "offline"
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status="offline")
+ error_msg = result.get("error", "Unknown error")
+ log_service.error(f"Node {node_id} WeChat status check failed: {error_msg}", "Callback")
+ return {
+ "success": False,
+ "message": "WeChat status check failed",
+ "data": {
+ "node_id": node_id,
+ "wechat_status": "offline",
+ "check_result": "failed",
+ "error": error_msg
+ }
+ }
+
+
+@router.post("/node/check", summary="检测节点微信状态")
+async def check_node_wechat_status(request: NodeCallbackRequest, _=Depends(require_api_key)) -> Dict[str, Any]:
+ """
+ 检测节点的微信状态
+ 通过发送消息到文件传输助手来检测微信是否正常
+ 返回检测结果
+ """
+ node_id = request.node_id
+ await sync_node_from_db(node_id)
+ node = await node_manager.get_node(node_id)
+ if not node:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+
+ await node_manager.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+
+ result = await node_manager.call_node_api(
+ node_id,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": "文件传输助手", "message": "WXAuto Center 状态检测消息"}
+ )
+
+ if result.get("success"):
+ node.wechat_status = "online"
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status="online")
+ return {
+ "success": True,
+ "message": "WeChat is normal",
+ "data": {
+ "node_id": node_id,
+ "wechat_status": "online",
+ "api_status": node.status
+ }
+ }
+ else:
+ node.wechat_status = "offline"
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status="offline")
+ return {
+ "success": False,
+ "message": "WeChat is abnormal",
+ "data": {
+ "node_id": node_id,
+ "wechat_status": "offline",
+ "api_status": node.status,
+ "error": result.get("error", "Unknown error")
+ }
+ }
+
+
+@router.get("/node/{node_id}/status", summary="获取节点回调状态")
+async def get_node_callback_status(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ """
+ 获取节点的当前状态,用于回调确认
+ """
+ node = await node_manager.get_node(node_id)
+ if not node:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+
+ return {
+ "success": True,
+ "data": {
+ "node_id": node.node_id,
+ "name": node.name,
+ "api_url": node.api_url,
+ "api_status": node.status,
+ "wechat_status": node.wechat_status,
+ "is_healthy": node.is_healthy,
+ "enabled": node.enabled,
+ "group": node.group
+ }
+ }
diff --git a/api/external.py b/api/external.py
new file mode 100644
index 0000000..9623fca
--- /dev/null
+++ b/api/external.py
@@ -0,0 +1,176 @@
+# ///
+# external.py
+# 描述:外部扩展接口,供其他程序调用中控系统
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from fastapi import APIRouter, HTTPException, Depends
+from typing import List, Optional, Dict, Any
+from pydantic import BaseModel
+
+from services.node_manager import node_manager
+from plugins.base import PluginRegistry
+from utils import require_external_key
+
+
+router = APIRouter(prefix="/external", tags=["外部扩展接口"])
+
+
+class ExternalMessageRequest(BaseModel):
+ node_id: str
+ who: str
+ msg: str
+ msg_type: str = "text"
+
+
+class DataTriggerRequest(BaseModel):
+ node_id: str
+ source: str
+ event: str
+ data: Dict[str, Any]
+ auto_send: bool = True
+ target: Optional[str] = None
+
+
+class PluginExecuteRequest(BaseModel):
+ plugin_name: str
+ action: str
+ params: Dict[str, Any] = {}
+
+
+async def sync_node_from_db(node_id: str) -> bool:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if not db_service:
+ return False
+ node = db_service.get_node(node_id)
+ if not node:
+ return False
+ existing = node_manager.nodes.get(node_id)
+ if existing:
+ existing.api_url = node.api_url
+ existing.api_key = node.api_key
+ else:
+ await node_manager.add_node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ return True
+
+
+@router.post("/send", summary="外部接口-发送消息")
+async def external_send_message(request: ExternalMessageRequest,
+ _=Depends(require_external_key)) -> Dict[str, Any]:
+ await sync_node_from_db(request.node_id)
+ result = await node_manager.send_message(
+ node_id=request.node_id,
+ who=request.who,
+ msg=request.msg,
+ msg_type=request.msg_type
+ )
+ return result
+
+
+@router.post("/trigger", summary="外部接口-触发数据处理")
+async def external_trigger(request: DataTriggerRequest,
+ _=Depends(require_external_key)) -> Dict[str, Any]:
+ plugin = PluginRegistry.get(request.source)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{request.source}' not found")
+
+ if not plugin.enabled:
+ raise HTTPException(status_code=400, detail=f"Plugin '{request.source}' is disabled")
+
+ execute_result = plugin.execute({
+ "event": request.event,
+ "data": request.data
+ })
+
+ if request.auto_send and request.target:
+ await sync_node_from_db(request.node_id)
+ msg = request.data.get("message", str(request.data))
+ send_result = await node_manager.send_message(
+ node_id=request.node_id,
+ who=request.target,
+ msg=msg
+ )
+ execute_result["send_result"] = send_result
+
+ return {
+ "success": True,
+ "plugin_result": execute_result,
+ "data_triggered": True
+ }
+
+
+@router.post("/plugin/execute", summary="外部接口-执行插件")
+async def execute_plugin(request: PluginExecuteRequest,
+ _=Depends(require_external_key)) -> Dict[str, Any]:
+ plugin = PluginRegistry.get(request.plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{request.plugin_name}' not found")
+
+ if not plugin.enabled:
+ raise HTTPException(status_code=400, detail=f"Plugin '{request.plugin_name}' is disabled")
+
+ result = plugin.execute(request.params)
+ return {"success": True, "result": result}
+
+
+@router.get("/plugins", summary="外部接口-获取可用插件列表")
+async def get_plugins(_=Depends(require_external_key)) -> Dict[str, Any]:
+ plugins = PluginRegistry.get_all()
+ enabled_plugins = [p for p in plugins if p.enabled]
+ return {
+ "success": True,
+ "plugins": [p.get_info() for p in enabled_plugins],
+ "count": len(enabled_plugins)
+ }
+
+
+@router.get("/nodes", summary="外部接口-获取可用节点")
+async def get_available_nodes(_=Depends(require_external_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ nodes = db_service.get_enabled_nodes()
+ return {"success": True, "nodes": [n.to_dict() for n in nodes], "count": len(nodes)}
+ nodes = await node_manager.get_enabled_nodes()
+ return {"success": True, "nodes": [n.to_dict() for n in nodes], "count": len(nodes)}
+
+
+@router.post("/ai/process", summary="外部接口-AI处理消息")
+async def ai_process_message(
+ input_text: str,
+ context: Optional[Dict[str, Any]] = None,
+ model: Optional[str] = None,
+ _=Depends(require_external_key)
+) -> Dict[str, Any]:
+ ai_plugin = PluginRegistry.get("simple_ai_agent")
+ if not ai_plugin or not ai_plugin.enabled:
+ raise HTTPException(status_code=400, detail="AI agent not available")
+
+ if model:
+ ai_plugin.model = model
+
+ result = ai_plugin.process(input_text, context or {})
+ return {"success": True, "result": result}
+
+
+@router.post("/webhook/{event}", summary="外部接口-Webhook接收")
+async def receive_webhook(event: str,
+ payload: Dict[str, Any],
+ _=Depends(require_external_key)) -> Dict[str, Any]:
+ webhook_plugin = PluginRegistry.get("webhook_trigger")
+ if webhook_plugin and webhook_plugin.enabled:
+ result = webhook_plugin.execute({
+ "event": event,
+ "data": payload
+ })
+ return {"success": True, "result": result}
+ return {"success": True, "message": "Webhook received", "event": event}
diff --git a/api/messages.py b/api/messages.py
new file mode 100644
index 0000000..a586567
--- /dev/null
+++ b/api/messages.py
@@ -0,0 +1,148 @@
+# ///
+# messages.py
+# 描述:消息发送API路由
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from fastapi import APIRouter, HTTPException, Depends
+from typing import List, Dict, Any
+from pydantic import BaseModel
+
+from services.node_manager import node_manager
+from utils import require_api_key, check_node_health
+
+
+router = APIRouter(prefix="/messages", tags=["消息管理"])
+
+
+class SendMessageRequest(BaseModel):
+ node_id: str
+ who: str
+ msg: str
+ msg_type: str = "text"
+
+
+class SendGroupMessageRequest(BaseModel):
+ node_id: str
+ group_name: str
+ msg: str
+ msg_type: str = "text"
+
+
+class BatchMessageRequest(BaseModel):
+ messages: List[Dict[str, Any]]
+
+
+async def sync_node_from_db(node_id: str) -> bool:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if not db_service:
+ return False
+ node = db_service.get_node(node_id)
+ if not node:
+ return False
+ existing = node_manager.nodes.get(node_id)
+ if existing:
+ existing.api_url = node.api_url
+ existing.api_key = node.api_key
+ else:
+ await node_manager.add_node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ return True
+
+
+@router.post("/send", summary="发送消息")
+async def send_message(request: SendMessageRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ await sync_node_from_db(request.node_id)
+ await check_node_health(request.node_id)
+
+ result = await node_manager.send_message(
+ node_id=request.node_id,
+ who=request.who,
+ msg=request.msg,
+ msg_type=request.msg_type
+ )
+ return result
+
+
+@router.post("/send_group", summary="发送群消息")
+async def send_group_message(request: SendGroupMessageRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ await sync_node_from_db(request.node_id)
+ await check_node_health(request.node_id)
+
+ result = await node_manager.send_message_to_group(
+ node_id=request.node_id,
+ group_name=request.group_name,
+ msg=request.msg,
+ msg_type=request.msg_type
+ )
+ return result
+
+
+@router.post("/batch", summary="批量发送消息")
+async def batch_send(request: BatchMessageRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ results = []
+ for msg_item in request.messages:
+ node_id = msg_item.get("node_id")
+ who = msg_item.get("who")
+ msg = msg_item.get("msg")
+ msg_type = msg_item.get("msg_type", "text")
+
+ if not all([node_id, who, msg]):
+ results.append({
+ "success": False,
+ "error": "Missing required fields",
+ "item": msg_item
+ })
+ continue
+
+ is_healthy, reason = await node_manager.is_node_healthy(node_id)
+ if not is_healthy:
+ results.append({
+ "success": False,
+ "error": f"Node not healthy: {reason}",
+ "node_id": node_id,
+ "who": who
+ })
+ continue
+
+ result = await node_manager.send_message(node_id, who, msg, msg_type)
+ results.append({"node_id": node_id, "who": who, "result": result})
+
+ return {"success": True, "results": results, "total": len(results)}
+
+
+@router.post("/template/{node_id}/{who}", summary="发送模板消息")
+async def send_template_message(node_id: str, who: str,
+ template: str,
+ params: Dict[str, str],
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ await check_node_health(node_id)
+
+ try:
+ msg = template.format(**params)
+ except KeyError as e:
+ raise HTTPException(status_code=400, detail=f"Missing template parameter: {e}")
+
+ result = await node_manager.send_message(node_id, who, msg)
+ return result
+
+
+@router.get("/history/{node_id}/{chat_name}", summary="获取消息历史")
+async def get_message_history(node_id: str, chat_name: str,
+ limit: int = 50,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ await check_node_health(node_id)
+
+ result = await node_manager.get_messages(node_id, chat_name, limit)
+ return result
diff --git a/api/nodes.py b/api/nodes.py
new file mode 100644
index 0000000..cc6052c
--- /dev/null
+++ b/api/nodes.py
@@ -0,0 +1,201 @@
+# ///
+# nodes.py
+# 描述:节点管理API路由
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from fastapi import APIRouter, HTTPException, Depends
+from typing import List, Optional, Dict, Any
+from pydantic import BaseModel
+
+from services.node_manager import node_manager
+from config import save_config_to_file, get_settings, NodeConfig
+from utils import require_api_key
+
+
+router = APIRouter(prefix="/nodes", tags=["节点管理"])
+
+
+class NodeAddRequest(BaseModel):
+ node_id: str
+ name: str
+ api_url: str
+ api_key: str
+ description: str = ""
+ group: str = "default"
+
+
+class NodeUpdateRequest(BaseModel):
+ name: Optional[str] = None
+ api_url: Optional[str] = None
+ api_key: Optional[str] = None
+ description: Optional[str] = None
+ group: Optional[str] = None
+ enabled: Optional[bool] = None
+
+
+def save_nodes_config():
+ nodes_data = node_manager.to_config()
+ get_settings().nodes = [NodeConfig(**n) for n in nodes_data]
+ save_config_to_file()
+
+
+@router.get("/", summary="获取所有节点")
+async def get_nodes(group: Optional[str] = None, _=Depends(require_api_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ nodes = db_service.get_all_nodes()
+ result_nodes = [n.to_dict() for n in nodes]
+ return {"success": True, "data": result_nodes, "count": len(result_nodes)}
+
+ if group:
+ nodes = await node_manager.get_nodes_by_group(group)
+ else:
+ nodes = await node_manager.get_all_nodes()
+
+ result_nodes = []
+ for node in nodes:
+ node_dict = node.to_dict()
+ result_nodes.append(node_dict)
+
+ return {
+ "success": True,
+ "data": result_nodes,
+ "count": len(result_nodes)
+ }
+
+
+@router.get("/{node_id}", summary="获取指定节点信息")
+async def get_node(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ node = db_service.get_node(node_id)
+ if node:
+ return {"success": True, "data": node.to_dict()}
+ node = await node_manager.get_node(node_id)
+ if not node:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+ return {"success": True, "data": node.to_dict()}
+
+
+@router.post("/", summary="添加节点")
+async def add_node(request: NodeAddRequest, _=Depends(require_api_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ db_service.add_node(
+ node_id=request.node_id,
+ name=request.name,
+ api_url=request.api_url,
+ api_key=request.api_key,
+ description=request.description or "",
+ group=request.group or "default"
+ )
+
+ success = await node_manager.add_node(
+ node_id=request.node_id,
+ name=request.name,
+ api_url=request.api_url,
+ api_key=request.api_key,
+ description=request.description,
+ group=request.group
+ )
+ if not success:
+ raise HTTPException(status_code=400, detail=f"Node '{request.node_id}' already exists")
+ save_nodes_config()
+ return {"success": True, "message": f"Node '{request.node_id}' added successfully"}
+
+
+@router.put("/{node_id}", summary="更新节点信息")
+async def update_node(node_id: str, request: NodeUpdateRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ update_data = request.model_dump(exclude_unset=True)
+
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, **update_data)
+
+ success = await node_manager.update_node(node_id, **update_data)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+ save_nodes_config()
+ return {"success": True, "message": f"Node '{node_id}' updated successfully"}
+
+
+@router.delete("/{node_id}", summary="删除节点")
+async def delete_node(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ db_service.delete_node(node_id)
+
+ success = await node_manager.remove_node(node_id)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+ save_nodes_config()
+ return {"success": True, "message": f"Node '{node_id}' deleted successfully"}
+
+
+@router.get("/{node_id}/status", summary="检查节点状态")
+async def check_node_status(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ api_status = await node_manager.check_node_status(node_id)
+ is_healthy, health_msg = await node_manager.is_node_healthy(node_id)
+
+ return {
+ "success": True,
+ "data": {
+ "node_id": node_id,
+ "api_status": api_status,
+ "is_healthy": is_healthy,
+ "health_message": health_msg
+ }
+ }
+
+
+@router.post("/{node_id}/enable", summary="启用节点")
+async def enable_node(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, enabled=True)
+ success = await node_manager.update_node(node_id, enabled=True)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+ save_nodes_config()
+ return {"success": True, "message": f"Node '{node_id}' enabled"}
+
+
+@router.post("/{node_id}/disable", summary="禁用节点")
+async def disable_node(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, enabled=False)
+ success = await node_manager.update_node(node_id, enabled=False)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
+ save_nodes_config()
+ return {"success": True, "message": f"Node '{node_id}' disabled"}
+
+
+@router.get("/{node_id}/friends", summary="获取节点好友列表")
+async def get_friends(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ result = await node_manager.get_friends_list(node_id)
+ return result
+
+
+@router.get("/{node_id}/groups", summary="获取节点群列表")
+async def get_groups(node_id: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ result = await node_manager.get_groups_list(node_id)
+ return result
+
+
+@router.get("/{node_id}/messages/{chat_name}", summary="获取聊天消息")
+async def get_messages(node_id: str, chat_name: str, limit: int = 20,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ result = await node_manager.get_messages(node_id, chat_name, limit)
+ return result
diff --git a/api/plugins.py b/api/plugins.py
new file mode 100644
index 0000000..2206a72
--- /dev/null
+++ b/api/plugins.py
@@ -0,0 +1,412 @@
+# ///
+# plugins.py
+# 描述:插件管理API路由
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from fastapi import APIRouter, HTTPException, Depends
+from typing import List, Optional, Dict, Any
+from pydantic import BaseModel
+
+from plugins.base import PluginRegistry, PluginType
+from utils import require_api_key
+from database_service import get_db_service
+
+
+router = APIRouter(prefix="/plugins", tags=["插件管理"])
+
+
+class PluginConfigRequest(BaseModel):
+ name: str
+ config: Dict[str, Any]
+
+
+def _sync_plugin_from_db(plugin_name: str):
+ db_service = get_db_service()
+ if not db_service:
+ return
+ plugin_config = db_service.get_plugin_config(plugin_name)
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ return
+ if plugin_config:
+ import json
+ plugin.initialize(json.loads(plugin_config.config_json) if plugin_config.config_json else {})
+ if plugin_config.enabled:
+ plugin.enable()
+ else:
+ plugin.disable()
+
+
+@router.get("/", summary="获取所有插件")
+async def get_plugins(_=Depends(require_api_key)) -> Dict[str, Any]:
+ import json
+ plugins = PluginRegistry.get_all()
+ result = []
+ for p in plugins:
+ info = p.get_info()
+ db_service = get_db_service()
+ if db_service:
+ pc = db_service.get_plugin_config(p.plugin_name)
+ if pc:
+ info["config"] = json.loads(pc.config_json) if pc.config_json else {}
+ info["enabled"] = pc.enabled
+ result.append(info)
+ return {
+ "success": True,
+ "plugins": result,
+ "count": len(result)
+ }
+
+
+@router.get("/hotloaded", summary="获取热加载插件列表")
+async def get_hotloaded_plugins(_=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ from plugins.plugin_loader import hot_plugin_loader
+ plugins = hot_plugin_loader.get_loaded_plugins()
+ return {
+ "success": True,
+ "plugins": plugins,
+ "count": len(plugins),
+ "plugin_dir": hot_plugin_loader.plugin_dir
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.post("/hotreload", summary="手动触发插件热重载")
+async def trigger_hot_reload(_=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ from plugins.plugin_loader import hot_plugin_loader
+ hot_plugin_loader.scan_and_load_plugins()
+ plugins = hot_plugin_loader.get_loaded_plugins()
+ return {
+ "success": True,
+ "message": "Hot reload triggered",
+ "plugins": plugins
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.get("/type/{plugin_type}", summary="按类型获取插件")
+async def get_plugins_by_type(plugin_type: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ ptype = PluginType(plugin_type)
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid plugin type: {plugin_type}")
+
+ plugins = PluginRegistry.get_by_type(ptype)
+ return {
+ "success": True,
+ "plugins": [p.get_info() for p in plugins],
+ "count": len(plugins)
+ }
+
+
+@router.get("/custom/list", summary="列出自定义插件文件")
+async def list_custom_plugins(_=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ import os
+ custom_dir = "/app/plugins/custom"
+ if not os.path.exists(custom_dir):
+ return {"success": True, "plugins": [], "count": 0}
+
+ files = []
+ for f in os.listdir(custom_dir):
+ if f.endswith('.py') and not f.startswith('_'):
+ filepath = os.path.join(custom_dir, f)
+ stat = os.stat(filepath)
+ files.append({
+ "name": f,
+ "size": stat.st_size,
+ "modified": stat.st_mtime,
+ "path": filepath
+ })
+
+ return {
+ "success": True,
+ "plugins": files,
+ "count": len(files),
+ "directory": custom_dir
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.post("/custom/create", summary="创建自定义插件文件")
+async def create_custom_plugin(name: str, code: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ import os
+ custom_dir = "/app/plugins/custom"
+ os.makedirs(custom_dir, exist_ok=True)
+
+ safe_name = name.replace("..", "").replace("/", "").replace("\\", "")
+ if not safe_name.endswith(".py"):
+ safe_name += ".py"
+
+ filepath = os.path.join(custom_dir, safe_name)
+ with open(filepath, 'w') as f:
+ f.write(code)
+
+ from plugins.plugin_loader import hot_plugin_loader
+ hot_plugin_loader.scan_and_load_plugins()
+
+ return {
+ "success": True,
+ "message": f"Plugin {safe_name} created",
+ "path": filepath
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.delete("/custom/{plugin_name}", summary="删除自定义插件")
+async def delete_custom_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ import os
+ custom_dir = "/app/plugins/custom"
+
+ safe_name = plugin_name.replace("..", "").replace("/", "").replace("\\", "")
+ if not safe_name.endswith(".py"):
+ safe_name += ".py"
+
+ filepath = os.path.join(custom_dir, safe_name)
+ if os.path.exists(filepath):
+ os.remove(filepath)
+
+ from plugins.plugin_loader import hot_plugin_loader
+ hot_plugin_loader.scan_and_load_plugins()
+
+ return {"success": True, "message": f"Plugin {safe_name} deleted"}
+ else:
+ return {"success": False, "error": "Plugin file not found"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.get("/custom/{plugin_name}/download", summary="下载自定义插件源码")
+async def download_custom_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ import os
+ custom_dir = "/app/plugins/custom"
+
+ safe_name = plugin_name.replace("..", "").replace("/", "").replace("\\", "")
+ if not safe_name.endswith(".py"):
+ safe_name += ".py"
+
+ filepath = os.path.join(custom_dir, safe_name)
+ if os.path.exists(filepath):
+ with open(filepath, 'r') as f:
+ code = f.read()
+ return {
+ "success": True,
+ "name": safe_name,
+ "code": code
+ }
+ else:
+ return {"success": False, "error": "Plugin file not found"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.get("/{plugin_name}", summary="获取插件详情")
+async def get_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ import json
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+ info = plugin.get_info()
+ db_service = get_db_service()
+ if db_service:
+ pc = db_service.get_plugin_config(plugin_name)
+ if pc:
+ info["config"] = json.loads(pc.config_json) if pc.config_json else {}
+ info["enabled"] = pc.enabled
+ return {"success": True, "plugin": info}
+
+
+@router.post("/{plugin_name}/enable", summary="启用插件")
+async def enable_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+ PluginRegistry.enable(plugin_name)
+ db_service = get_db_service()
+ if db_service:
+ pc = db_service.get_plugin_config(plugin_name)
+ if pc:
+ db_service.update_plugin_enabled(plugin_name, True)
+ else:
+ db_service.save_plugin_config(plugin_name, plugin.config or {}, True)
+ return {"success": True, "message": f"Plugin '{plugin_name}' enabled"}
+
+
+@router.post("/{plugin_name}/disable", summary="禁用插件")
+async def disable_plugin(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+ PluginRegistry.disable(plugin_name)
+ db_service = get_db_service()
+ if db_service:
+ pc = db_service.get_plugin_config(plugin_name)
+ if pc:
+ db_service.update_plugin_enabled(plugin_name, False)
+ else:
+ db_service.save_plugin_config(plugin_name, plugin.config or {}, False)
+ return {"success": True, "message": f"Plugin '{plugin_name}' disabled"}
+
+
+@router.put("/{plugin_name}/config", summary="更新插件配置")
+async def update_plugin_config(plugin_name: str,
+ request: PluginConfigRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+
+ success = plugin.initialize(request.config)
+ if not success:
+ raise HTTPException(status_code=400, detail=f"Failed to initialize plugin '{plugin_name}'")
+
+ db_service = get_db_service()
+ if db_service:
+ pc = db_service.get_plugin_config(plugin_name)
+ enabled = pc.enabled if pc else False
+ db_service.save_plugin_config(plugin_name, request.config, enabled)
+
+ return {"success": True, "message": f"Plugin '{plugin_name}' config updated"}
+
+
+@router.post("/{plugin_name}/execute", summary="执行插件")
+async def execute_plugin(plugin_name: str,
+ request: Optional[Dict[str, Any]] = None,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+
+ if not plugin.enabled:
+ return {"success": False, "error": f"Plugin '{plugin_name}' is disabled"}
+
+ params = request or {}
+ result = plugin.execute(params)
+ return {"success": True, "result": result}
+
+
+@router.get("/{plugin_name}/logs", summary="获取插件日志")
+async def get_plugin_logs(
+ plugin_name: str,
+ lines: int = 100,
+ level: Optional[str] = None,
+ _=Depends(require_api_key)
+) -> Dict[str, Any]:
+ try:
+ import logging
+ import os
+
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+
+ log_dir = "/app/data/logs"
+ if not os.path.exists(log_dir):
+ os.makedirs(log_dir, exist_ok=True)
+
+ log_file = os.path.join(log_dir, f"{plugin_name}.log")
+ app_log = os.path.join(log_dir, "app.log")
+
+ all_logs = []
+
+ if os.path.exists(log_file):
+ with open(log_file, 'r', encoding='utf-8') as f:
+ all_logs.extend(f.readlines())
+
+ if os.path.exists(app_log):
+ with open(app_log, 'r', encoding='utf-8') as f:
+ for line in f:
+ if plugin_name in line or f"[{plugin_name}]" in line:
+ all_logs.append(line)
+
+ filtered_logs = []
+ for line in all_logs:
+ if level:
+ if level.upper() in line.upper():
+ filtered_logs.append(line.strip())
+ else:
+ filtered_logs.append(line.strip())
+
+ filtered_logs = filtered_logs[-lines:] if len(filtered_logs) > lines else filtered_logs
+
+ return {
+ "success": True,
+ "plugin_name": plugin_name,
+ "log_file": log_file,
+ "count": len(filtered_logs),
+ "logs": filtered_logs
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.get("/{plugin_name}/schema", summary="获取插件配置表单Schema")
+async def get_plugin_schema(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+
+ schema = None
+ if hasattr(plugin, 'get_config_schema'):
+ schema = plugin.get_config_schema()
+ elif hasattr(plugin, 'config_schema'):
+ schema = plugin.config_schema
+
+ if schema:
+ if callable(schema):
+ schema = schema()
+ if hasattr(schema, 'to_form_schema'):
+ return {
+ "success": True,
+ "schema": schema.to_form_schema(),
+ "has_form": True
+ }
+ return {
+ "success": True,
+ "schema": schema,
+ "has_form": True
+ }
+
+ return {
+ "success": True,
+ "schema": None,
+ "has_form": False,
+ "message": "此插件暂无表单配置"
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+@router.post("/{plugin_name}/logs/clear", summary="清空插件日志")
+async def clear_plugin_logs(plugin_name: str, _=Depends(require_api_key)) -> Dict[str, Any]:
+ try:
+ import os
+
+ plugin = PluginRegistry.get(plugin_name)
+ if not plugin:
+ raise HTTPException(status_code=404, detail=f"Plugin '{plugin_name}' not found")
+
+ log_file = f"/app/data/logs/{plugin_name}.log"
+
+ if os.path.exists(log_file):
+ with open(log_file, 'w') as f:
+ f.write("")
+
+ return {
+ "success": True,
+ "message": f"Plugin '{plugin_name}' logs cleared"
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
diff --git a/api/webhook.py b/api/webhook.py
new file mode 100644
index 0000000..bde4bdc
--- /dev/null
+++ b/api/webhook.py
@@ -0,0 +1,137 @@
+# ///
+# webhook.py
+# 描述:Webhook管理API路由
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# ///
+
+from fastapi import APIRouter, HTTPException, Depends
+from typing import List, Dict, Any
+from pydantic import BaseModel
+
+from database_service import get_db_service
+from utils import require_api_key
+
+
+router = APIRouter(prefix="/webhook", tags=["Webhook管理"])
+
+
+class WebhookAddRequest(BaseModel):
+ name: str
+ url: str
+ format: str = "bark"
+ event_types: List[str] = []
+
+
+class WebhookUpdateRequest(BaseModel):
+ name: str = None
+ url: str = None
+ format: str = None
+ enabled: bool = None
+ event_types: List[str] = None
+
+
+@router.get("/", summary="获取所有Webhook")
+async def get_webhooks(_=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ webhooks = db_service.get_all_webhooks()
+ return {
+ "success": True,
+ "data": [w.to_dict() for w in webhooks],
+ "count": len(webhooks)
+ }
+
+
+@router.get("/{webhook_id}", summary="获取指定Webhook")
+async def get_webhook(webhook_id: int, _=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ webhook = db_service.get_webhook(webhook_id)
+ if not webhook:
+ raise HTTPException(status_code=404, detail=f"Webhook {webhook_id} not found")
+
+ return {"success": True, "data": webhook.to_dict()}
+
+
+@router.post("/", summary="添加Webhook")
+async def add_webhook(request: WebhookAddRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ success = db_service.add_webhook(
+ name=request.name,
+ url=request.url,
+ format=request.format,
+ event_types=request.event_types
+ )
+
+ if not success:
+ raise HTTPException(status_code=400, detail="Failed to add webhook")
+
+ return {"success": True, "message": "Webhook added successfully"}
+
+
+@router.put("/{webhook_id}", summary="更新Webhook")
+async def update_webhook(webhook_id: int,
+ request: WebhookUpdateRequest,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ update_data = request.model_dump(exclude_unset=True)
+ success = db_service.update_webhook(webhook_id, **update_data)
+
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Webhook {webhook_id} not found")
+
+ return {"success": True, "message": "Webhook updated successfully"}
+
+
+@router.delete("/{webhook_id}", summary="删除Webhook")
+async def delete_webhook(webhook_id: int,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ success = db_service.delete_webhook(webhook_id)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Webhook {webhook_id} not found")
+
+ return {"success": True, "message": "Webhook deleted successfully"}
+
+
+@router.post("/{webhook_id}/enable", summary="启用Webhook")
+async def enable_webhook(webhook_id: int,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ success = db_service.update_webhook(webhook_id, enabled=True)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Webhook {webhook_id} not found")
+
+ return {"success": True, "message": "Webhook enabled"}
+
+
+@router.post("/{webhook_id}/disable", summary="禁用Webhook")
+async def disable_webhook(webhook_id: int,
+ _=Depends(require_api_key)) -> Dict[str, Any]:
+ db_service = get_db_service()
+ if not db_service:
+ raise HTTPException(status_code=500, detail="Database not available")
+
+ success = db_service.update_webhook(webhook_id, enabled=False)
+ if not success:
+ raise HTTPException(status_code=404, detail=f"Webhook {webhook_id} not found")
+
+ return {"success": True, "message": "Webhook disabled"}
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..f481a81
--- /dev/null
+++ b/config.json
@@ -0,0 +1,15 @@
+{
+ "app_name": "WXAuto Center",
+ "host": "0.0.0.0",
+ "port": 8080,
+ "debug": true,
+ "secret_key": "wxauto-center-secret-key-change-in-production",
+ "workers": 1,
+ "cors_origins": ["*"],
+ "api_prefix": "/api/v1",
+ "external_api_key": "your-external-api-key",
+ "monitor": {
+ "enabled": true,
+ "check_interval": 30
+ }
+}
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..c937c9f
--- /dev/null
+++ b/config.py
@@ -0,0 +1,213 @@
+# ///
+# config.py
+# 描述:系统配置管理,支持多环境配置
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import os
+from typing import Dict, List, Optional
+from pydantic import BaseModel
+from pydantic_settings import BaseSettings
+
+
+class NodeConfig(BaseModel):
+ node_id: str
+ name: str
+ api_url: str
+ api_key: str
+ enabled: bool = True
+ description: str = ""
+ group: str = "default"
+
+
+class PluginConfig(BaseModel):
+ name: str
+ enabled: bool = True
+ config: Dict = {}
+
+
+class WebhookConfig(BaseModel):
+ urls: List[str] = []
+ enabled: bool = False
+ timeout: int = 10
+ retry_times: int = 3
+ format: str = "wechat"
+ event_types: List[str] = ["api_error", "wechat_offline", "wechat_online", "node_offline", "node_online"]
+
+
+class MonitorConfig(BaseModel):
+ enabled: bool = True
+ check_interval: int = 60
+
+
+class RedisConfig(BaseModel):
+ url: str = "redis://localhost:6379/0"
+ db: int = 0
+ password: Optional[str] = None
+
+
+class DatabaseConfig(BaseModel):
+ url: str = "postgresql://wxauto:wxauto@localhost:5432/wxauto"
+ pool_size: int = 10
+ max_overflow: int = 20
+
+ class Config:
+ populate_by_name = True
+
+
+class Settings(BaseSettings):
+ app_name: str = "WXAuto Center"
+ host: str = "0.0.0.0"
+ port: int = 8080
+ debug: bool = False
+ secret_key: str = "wxauto-center-secret-key-change-in-production"
+
+ nodes: List[NodeConfig] = []
+ plugins: List[PluginConfig] = []
+ webhook: WebhookConfig = WebhookConfig()
+ monitor: MonitorConfig = MonitorConfig()
+ redis: RedisConfig = RedisConfig()
+ database: DatabaseConfig = DatabaseConfig()
+
+ cors_origins: List[str] = ["*"]
+ api_prefix: str = "/api/v1"
+ external_api_key: str = "your-external-api-key"
+ workers: int = 1
+
+ class Config:
+ env_file = ".env"
+ env_file_encoding = "utf-8"
+ populate_by_name = True
+ extra = "allow"
+ env_nested_delimiter = "_"
+
+_settings: Optional[Settings] = None
+_config_file: str = "config.json"
+
+
+def get_settings() -> Settings:
+ global _settings
+ if _settings is None:
+ _settings = Settings()
+ _settings.database.url = os.environ.get("DATABASE_URL", _settings.database.url)
+ _settings.redis.url = os.environ.get("REDIS_URL", _settings.redis.url)
+ return _settings
+
+
+def update_settings(**kwargs):
+ global _settings
+ if _settings is None:
+ _settings = Settings()
+ for key, value in kwargs.items():
+ if hasattr(_settings, key):
+ setattr(_settings, key, value)
+
+
+def load_config_from_file(config_file: str = "config.json") -> bool:
+ global _settings, _config_file
+ _config_file = config_file
+
+ if not os.path.exists(config_file):
+ return False
+
+ try:
+ import json
+ with open(config_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ settings = get_settings()
+ settings.app_name = data.get("app_name", settings.app_name)
+ settings.host = data.get("host", settings.host)
+ settings.port = data.get("port", settings.port)
+ settings.debug = data.get("debug", settings.debug)
+ settings.secret_key = data.get("secret_key", settings.secret_key)
+ settings.cors_origins = data.get("cors_origins", settings.cors_origins)
+ settings.api_prefix = data.get("api_prefix", settings.api_prefix)
+ settings.external_api_key = data.get("external_api_key", settings.external_api_key)
+
+ plugins_data = data.get("plugins", [])
+ settings.plugins = [PluginConfig(**p) for p in plugins_data]
+
+ monitor_data = data.get("monitor", {})
+ settings.monitor = MonitorConfig(**monitor_data) if monitor_data else MonitorConfig()
+
+ settings.workers = data.get("workers", 1)
+
+ settings.database.url = os.environ.get("DATABASE_URL", settings.database.url)
+ settings.redis.url = os.environ.get("REDIS_URL", settings.redis.url)
+
+ return True
+ except Exception:
+ return False
+
+
+def save_config_to_file(config_file: str = None):
+ global _config_file
+ if config_file:
+ _config_file = config_file
+
+ import json
+ settings = get_settings()
+ config_data = {
+ "app_name": settings.app_name,
+ "host": settings.host,
+ "port": settings.port,
+ "debug": settings.debug,
+ "secret_key": settings.secret_key,
+ "plugins": [p.model_dump() for p in settings.plugins],
+ "monitor": settings.monitor.model_dump(),
+ "workers": settings.workers,
+ "cors_origins": settings.cors_origins,
+ "api_prefix": settings.api_prefix,
+ "external_api_key": settings.external_api_key,
+ }
+ with open(_config_file, "w", encoding="utf-8") as f:
+ json.dump(config_data, f, indent=2, ensure_ascii=False)
+
+
+class ConfigManager:
+ def __init__(self, config_file: str = "config.json"):
+ self.config_file = config_file
+
+ def get_node(self, node_id: str) -> Optional[NodeConfig]:
+ settings = get_settings()
+ for node in settings.nodes:
+ if node.node_id == node_id:
+ return node
+ return None
+
+ def get_all_nodes(self) -> List[NodeConfig]:
+ return get_settings().nodes
+
+ def get_enabled_nodes(self) -> List[NodeConfig]:
+ return [n for n in get_settings().nodes if n.enabled]
+
+ def add_node(self, node: NodeConfig) -> bool:
+ existing = self.get_node(node.node_id)
+ if existing:
+ return False
+ get_settings().nodes.append(node)
+ return True
+
+ def update_node(self, node_id: str, **kwargs) -> bool:
+ for i, node in enumerate(get_settings().nodes):
+ if node.node_id == node_id:
+ for key, value in kwargs.items():
+ if hasattr(node, key):
+ setattr(node, key, value)
+ return True
+ return False
+
+ def remove_node(self, node_id: str) -> bool:
+ for i, node in enumerate(get_settings().nodes):
+ if node.node_id == node_id:
+ get_settings().nodes.pop(i)
+ return True
+ return False
+
+ def save_config(self):
+ save_config_to_file(self.config_file)
+
+ def load_config(self) -> bool:
+ return load_config_from_file(self.config_file)
diff --git a/database_service.py b/database_service.py
new file mode 100644
index 0000000..0d01277
--- /dev/null
+++ b/database_service.py
@@ -0,0 +1,283 @@
+# ///
+# database_service.py
+# 描述:数据库服务层
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from typing import List, Optional, Dict, Any
+from datetime import datetime
+
+from models import DatabaseManager, Node, LogEntry, WebhookUrl, PluginConfig
+
+
+class DatabaseService:
+ def __init__(self, db_manager: DatabaseManager):
+ self.db = db_manager
+
+ # ==================== Node Operations ====================
+
+ def get_all_nodes(self) -> List[Node]:
+ session = self.db.get_session()
+ try:
+ return session.query(Node).all()
+ finally:
+ session.close()
+
+ def get_node(self, node_id: str) -> Optional[Node]:
+ session = self.db.get_session()
+ try:
+ return session.query(Node).filter(Node.node_id == node_id).first()
+ finally:
+ session.close()
+
+ def get_nodes_by_group(self, group: str) -> List[Node]:
+ session = self.db.get_session()
+ try:
+ return session.query(Node).filter(Node.group == group).all()
+ finally:
+ session.close()
+
+ def get_enabled_nodes(self) -> List[Node]:
+ session = self.db.get_session()
+ try:
+ return session.query(Node).filter(Node.enabled == True).all()
+ finally:
+ session.close()
+
+ def add_node(self, node_id: str, name: str, api_url: str, api_key: str,
+ description: str = "", group: str = "default") -> bool:
+ session = self.db.get_session()
+ try:
+ existing = session.query(Node).filter(Node.node_id == node_id).first()
+ if existing:
+ return False
+ node = Node(
+ node_id=node_id,
+ name=name,
+ api_url=api_url,
+ api_key=api_key,
+ description=description,
+ group=group
+ )
+ session.add(node)
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ def update_node(self, node_id: str, **kwargs) -> bool:
+ session = self.db.get_session()
+ try:
+ node = session.query(Node).filter(Node.node_id == node_id).first()
+ if not node:
+ return False
+ for key, value in kwargs.items():
+ if hasattr(node, key):
+ setattr(node, key, value)
+ node.updated_at = datetime.now()
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ def delete_node(self, node_id: str) -> bool:
+ session = self.db.get_session()
+ try:
+ node = session.query(Node).filter(Node.node_id == node_id).first()
+ if not node:
+ return False
+ session.delete(node)
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ # ==================== Webhook Operations ====================
+
+ def get_all_webhooks(self) -> List[WebhookUrl]:
+ session = self.db.get_session()
+ try:
+ return session.query(WebhookUrl).all()
+ finally:
+ session.close()
+
+ def get_webhook(self, webhook_id: int) -> Optional[WebhookUrl]:
+ session = self.db.get_session()
+ try:
+ return session.query(WebhookUrl).filter(WebhookUrl.id == webhook_id).first()
+ finally:
+ session.close()
+
+ def get_enabled_webhooks(self) -> List[WebhookUrl]:
+ session = self.db.get_session()
+ try:
+ return session.query(WebhookUrl).filter(WebhookUrl.enabled == True).all()
+ finally:
+ session.close()
+
+ def add_webhook(self, name: str, url: str, format: str = "bark",
+ event_types: List[str] = None) -> bool:
+ session = self.db.get_session()
+ try:
+ webhook = WebhookUrl(
+ name=name,
+ url=url,
+ format=format,
+ event_types=",".join(event_types) if event_types else ""
+ )
+ session.add(webhook)
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ def update_webhook(self, webhook_id: int, **kwargs) -> bool:
+ session = self.db.get_session()
+ try:
+ webhook = session.query(WebhookUrl).filter(WebhookUrl.id == webhook_id).first()
+ if not webhook:
+ return False
+ for key, value in kwargs.items():
+ if key == "event_types" and isinstance(value, list):
+ value = ",".join(value)
+ if hasattr(webhook, key):
+ setattr(webhook, key, value)
+ webhook.updated_at = datetime.now()
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ def delete_webhook(self, webhook_id: int) -> bool:
+ session = self.db.get_session()
+ try:
+ webhook = session.query(WebhookUrl).filter(WebhookUrl.id == webhook_id).first()
+ if not webhook:
+ return False
+ session.delete(webhook)
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ # ==================== Plugin Config Operations ====================
+
+ def get_plugin_config(self, plugin_name: str) -> Optional[PluginConfig]:
+ session = self.db.get_session()
+ try:
+ return session.query(PluginConfig).filter(PluginConfig.plugin_name == plugin_name).first()
+ finally:
+ session.close()
+
+ def get_all_plugin_configs(self) -> List[PluginConfig]:
+ session = self.db.get_session()
+ try:
+ return session.query(PluginConfig).all()
+ finally:
+ session.close()
+
+ def save_plugin_config(self, plugin_name: str, config: Dict[str, Any], enabled: bool = False) -> bool:
+ session = self.db.get_session()
+ try:
+ import json
+ existing = session.query(PluginConfig).filter(PluginConfig.plugin_name == plugin_name).first()
+ if existing:
+ existing.config_json = json.dumps(config)
+ existing.enabled = enabled
+ existing.updated_at = datetime.now()
+ else:
+ plugin_config = PluginConfig(
+ plugin_name=plugin_name,
+ config_json=json.dumps(config),
+ enabled=enabled
+ )
+ session.add(plugin_config)
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ def update_plugin_enabled(self, plugin_name: str, enabled: bool) -> bool:
+ session = self.db.get_session()
+ try:
+ plugin_config = session.query(PluginConfig).filter(PluginConfig.plugin_name == plugin_name).first()
+ if plugin_config:
+ plugin_config.enabled = enabled
+ plugin_config.updated_at = datetime.now()
+ session.commit()
+ return True
+ return False
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ # ==================== Log Operations ====================
+
+ def add_log(self, level: str, source: str, message: str) -> bool:
+ session = self.db.get_session()
+ try:
+ log = LogEntry(
+ level=level,
+ source=source,
+ message=message
+ )
+ session.add(log)
+ session.commit()
+ return True
+ except Exception:
+ session.rollback()
+ return False
+ finally:
+ session.close()
+
+ def get_recent_logs(self, count: int = 50) -> List[LogEntry]:
+ session = self.db.get_session()
+ try:
+ return session.query(LogEntry).order_by(
+ LogEntry.timestamp.desc()
+ ).limit(count).all()
+ finally:
+ session.close()
+
+
+_db_service: Optional[DatabaseService] = None
+_db_manager: Optional[DatabaseManager] = None
+
+
+def init_database(database_url: str):
+ global _db_service, _db_manager
+ _db_manager = DatabaseManager(database_url)
+ _db_manager.create_tables()
+ _db_service = DatabaseService(_db_manager)
+
+
+def get_db_service() -> DatabaseService:
+ return _db_service
+
+
+def get_db_manager() -> DatabaseManager:
+ return _db_manager
diff --git a/doc/wxauto_api/api_documentation.md b/doc/wxauto_api/api_documentation.md
new file mode 100644
index 0000000..dc11003
--- /dev/null
+++ b/doc/wxauto_api/api_documentation.md
@@ -0,0 +1,2253 @@
+# WxAuto HTTP API 接口文档
+
+## API 概述
+
+本文档详细说明了WxAuto HTTP API的使用方法、请求格式和响应格式。所有接口都需要通过API密钥认证。
+
+## 认证方式
+
+所有API请求都需要在HTTP Header中包含API密钥:
+
+```http
+X-API-Key: your_api_key_here
+```
+
+## 通用响应格式
+
+```json
+{
+ "code": 0, // 状态码:0成功,非0失败
+ "message": "", // 响应消息
+ "data": {} // 响应数据
+}
+```
+
+## 错误码说明
+
+- 0: 成功
+- 1001: 认证失败
+- 1002: 参数错误
+- 2001: 微信未初始化
+- 2002: 微信已掉线
+- 3001: 发送消息失败
+- 3002: 获取消息失败
+- 3003: 文件下载失败
+- 4001: 群操作失败
+- 5001: 好友操作失败
+
+## API 功能分类
+
+### 基础功能
+- 认证相关 (`/api/auth/`)
+- 微信基础功能 (`/api/wechat/`)
+- 系统功能 (`/api/system/`, `/api/health`)
+
+### 消息功能
+- 消息发送接收 (`/api/message/`)
+- 消息监听 (`/api/message/listen/`)
+- 消息操作 (`/api/message/`)
+- 聊天窗口操作 (`/api/chat-window/`)
+
+### 新增功能 (v2.0)
+- Chat类操作 (`/api/chat/`)
+- 群组管理 (`/api/group/`)
+- 好友管理 (`/api/friend/`)
+- WeChat类扩展 (`/api/wechat/`)
+- 朋友圈功能 (`/api/moments/`)
+- 辅助类功能 (`/api/auxiliary/`)
+
+### Plus版本功能
+标记为 "(Plus版)" 的功能需要wxautox库支持
+
+## API 端点详细说明
+
+### 1. 认证相关
+
+#### 验证API密钥
+```http
+POST /api/auth/verify
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/auth/verify \
+ -H "X-API-Key: test-key-2"
+```
+
+请求头:
+```http
+X-API-Key: your_api_key_here
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "验证成功",
+ "data": {
+ "valid": true
+ }
+}
+```
+
+### 2. 微信基础功能
+
+#### 初始化微信实例
+```http
+POST /api/wechat/initialize
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/wechat/initialize \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "初始化成功",
+ "data": {
+ "status": "connected"
+ }
+}
+```
+
+#### 获取微信状态
+```http
+GET /api/wechat/status
+```
+
+CURL 示例:
+```bash
+curl -X GET http://10.255.0.90:5000/api/wechat/status \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "status": "online",
+ "last_active": "2025-04-23 10:30:00"
+ }
+}
+```
+
+### 3. 消息相关接口
+
+#### 发送普通文本消息
+```http
+POST /api/message/send
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/send \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "receiver": "文件传输助手",
+ "message": "这是一条测试消息",
+ "at_list": ["张三", "李四"],
+ "clear": true
+ }'
+```
+
+请求体:
+```json
+{
+ "receiver": "文件传输助手",
+ "message": "这是一条测试消息",
+ "at_list": ["张三", "李四"], // 可选
+ "clear": true // 可选,是否清除输入框
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "message_id": "xxxxx"
+ }
+}
+```
+
+#### 发送打字机模式消息
+```http
+POST /api/message/send-typing
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/send-typing \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "receiver": "文件传输助手",
+ "message": "这是打字机模式消息\n这是第二行",
+ "at_list": ["张三", "李四"],
+ "clear": true
+ }'
+```
+
+请求体:
+```json
+{
+ "receiver": "文件传输助手",
+ "message": "这是打字机模式消息\n这是第二行",
+ "at_list": ["张三", "李四"], // 可选
+ "clear": true // 可选
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "message_id": "xxxxx"
+ }
+}
+```
+
+#### 发送文件消息
+```http
+POST /api/message/send-file
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/send-file \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "receiver": "文件传输助手",
+ "file_paths": [
+ "D:/test/test1.txt",
+ "D:/test/test2.txt"
+ ]
+ }'
+```
+
+请求体:
+```json
+{
+ "receiver": "文件传输助手",
+ "file_paths": [
+ "D:/test/test1.txt",
+ "D:/test/test2.txt"
+ ]
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "success_count": 2,
+ "failed_files": []
+ }
+}
+```
+
+#### 下载文件
+```http
+POST /api/file/download
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/file/download \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "file_path": "C:\\Code\\wxauto-ui\\wxauto文件\\部门信息表(1).xlsx"
+ }'
+```
+
+请求体:
+```json
+{
+ "file_path": "C:\\Code\\wxauto-ui\\wxauto文件\\部门信息表(1).xlsx"
+}
+```
+
+响应说明:
+- 成功时返回文件内容,Content-Type 为 application/octet-stream
+- 失败时返回错误信息,格式如下:
+
+```json
+{
+ "code": 3003,
+ "message": "文件下载失败",
+ "data": {
+ "error": "文件不存在或无法访问"
+ }
+}
+```
+
+错误码:
+- 3003: 文件下载失败,可能的原因:
+ - 文件不存在
+ - 文件路径无效
+ - 文件访问权限不足
+ - 其他文件系统错误
+
+注意事项:
+1. 文件路径必须使用双反斜杠(\\)作为分隔符
+2. 确保文件路径有访问权限
+3. 文件大小限制为100MB
+
+#### 获取聊天记录
+```http
+POST /api/message/get-history
+```
+
+请求体:
+```json
+{
+ "chat_name": "文件传输助手",
+ "save_pic": false, // 可选,是否保存图片
+ "save_video": false, // 可选,是否保存视频
+ "save_file": false, // 可选,是否保存文件
+ "save_voice": false // 可选,是否保存语音
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "messages": [
+ {
+ "type": "text",
+ "sender": "张三",
+ "content": "测试消息",
+ "time": "2025-04-23 10:30:00"
+ }
+ ]
+ }
+}
+```
+
+#### 获取主窗口未读消息
+```http
+GET /api/message/get-next-new?savepic=false&savevideo=false&savefile=false&savevoice=false&parseurl=false
+```
+
+CURL 示例:
+```bash
+curl -X GET "http://10.255.0.90:5000/api/message/get-next-new?savepic=false&savevideo=false&savefile=false&savevoice=false&parseurl=false" \
+ -H "X-API-Key: test-key-2"
+```
+
+查询参数:
+- savepic: bool,是否保存图片(可选,默认false)
+- savevideo: bool,是否保存视频(可选,默认false)
+- savefile: bool,是否保存文件(可选,默认false)
+- savevoice: bool,是否保存语音(可选,默认false)
+- parseurl: bool,是否解析链接(可选,默认false)
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "messages": {
+ "张三": [
+ {
+ "type": "text",
+ "content": "你好",
+ "sender": "张三",
+ "id": "123456",
+ "mtype": 1,
+ "sender_remark": "老张"
+ }
+ ]
+ }
+ }
+}
+```
+
+### 4. 消息监听相关接口
+
+#### 添加监听对象
+```http
+POST /api/message/listen/add
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/listen/add \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "who": "测试群",
+ "savepic": false,
+ "savevideo": false,
+ "savefile": false,
+ "savevoice": false,
+ "parseurl": false,
+ "exact": false
+ }'
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "savepic": false, // 可选,是否保存图片
+ "savevideo": false, // 可选,是否保存视频
+ "savefile": false, // 可选,是否保存文件
+ "savevoice": false, // 可选,是否保存语音
+ "parseurl": false, // 可选,是否解析URL
+ "exact": false // 可选,是否精确匹配名称
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "添加监听成功",
+ "data": {
+ "who": "测试群"
+ }
+}
+```
+
+#### 添加当前聊天窗口到监听列表
+```http
+POST /api/message/listen/add-current
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/listen/add-current \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "savepic": false,
+ "savevideo": false,
+ "savefile": false,
+ "savevoice": false,
+ "parseurl": false
+ }'
+```
+
+请求体:
+```json
+{
+ "savepic": false, // 可选,是否保存图片
+ "savevideo": false, // 可选,是否保存视频
+ "savefile": false, // 可选,是否保存文件
+ "savevoice": false, // 可选,是否保存语音
+ "parseurl": false // 可选,是否解析URL
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "添加监听成功",
+ "data": {
+ "who": "测试群",
+ "options": {
+ "savepic": false,
+ "savevideo": false,
+ "savefile": false,
+ "savevoice": false,
+ "parseurl": false
+ }
+ }
+}
+```
+
+错误码:
+- 2001: 微信未初始化
+- 3001: 添加监听失败,可能的原因:
+ - 未找到当前聊天窗口
+ - 当前窗口不是聊天窗口(例如:主窗口)
+ - 其他添加监听失败的情况
+
+注意事项:
+1. 使用此API前,请确保已打开要监听的聊天窗口
+2. 如果当前窗口是微信主窗口而不是聊天窗口,API将返回错误
+3. 所有选项参数都是可选的,默认值为false
+
+#### 获取监听消息
+```http
+GET /api/message/listen/get?who=测试群
+```
+
+CURL 示例:
+```bash
+curl -X GET "http://10.255.0.90:5000/api/message/listen/get?who=测试群" \
+ -H "X-API-Key: test-key-2"
+```
+
+查询参数:
+- who: string,要获取消息的对象(可选,不传则获取所有监听对象的消息)
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "messages": {
+ "测试群": [
+ {
+ "type": "text",
+ "content": "新消息",
+ "sender": "张三",
+ "id": "123456",
+ "mtype": 1,
+ "sender_remark": "老张"
+ }
+ ]
+ }
+ }
+}
+```
+
+#### 移除监听对象
+```http
+POST /api/message/listen/remove
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/listen/remove \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "who": "测试群"
+ }'
+```
+
+请求体:
+```json
+{
+ "who": "测试群"
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "移除监听成功",
+ "data": {
+ "who": "测试群"
+ }
+}
+```
+
+### 5. 聊天窗口操作接口
+
+注意:以下接口需要先通过 `/api/message/listen/add` 将目标添加到监听列表。
+
+#### 发送普通消息
+```http
+POST /api/chat-window/message/send
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/chat-window/message/send \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "who": "测试群",
+ "message": "测试消息",
+ "at_list": ["张三", "李四"],
+ "clear": true
+ }'
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message": "测试消息",
+ "at_list": ["张三", "李四"], // 可选
+ "clear": true // 可选,是否清除输入框
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "message_id": "success"
+ }
+}
+```
+
+#### 发送打字机模式消息
+```http
+POST /api/chat-window/message/send-typing
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/chat-window/message/send-typing \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "who": "测试群",
+ "message": "测试消息",
+ "at_list": ["张三", "李四"],
+ "clear": true
+ }'
+```
+
+请求体和响应格式同上。
+
+#### 发送文件
+```http
+POST /api/chat-window/message/send-file
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/chat-window/message/send-file \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "who": "测试群",
+ "file_paths": [
+ "D:/test/test1.txt",
+ "D:/test/test2.txt"
+ ]
+ }'
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "file_paths": [
+ "D:/test/test1.txt",
+ "D:/test/test2.txt"
+ ]
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "success_count": 2,
+ "failed_files": []
+ }
+}
+```
+
+#### @所有人
+```http
+POST /api/chat-window/message/at-all
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/chat-window/message/at-all \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "who": "测试群",
+ "message": "请大家注意"
+ }'
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message": "请大家注意" // 可选
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "message_id": "success"
+ }
+}
+```
+
+#### 获取聊天窗口信息
+```http
+GET /api/chat-window/info?who=测试群
+```
+
+CURL 示例:
+```bash
+curl -X GET "http://10.255.0.90:5000/api/chat-window/info?who=测试群" \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "member_count": 100,
+ "name": "测试群",
+ "members": ["张三", "李四"]
+ }
+}
+```
+
+#### 移除监听对象 (兼容接口)
+```http
+POST /api/message/listen/remove
+```
+
+
+
+
+**请求体:**
+```json
+{
+ "nickname": "测试群"
+}
+```
+
+**响应示例:**
+```json
+{
+ "code": 0,
+ "message": "移除监听成功",
+ "data": {
+ "nickname": "测试群",
+ "library": "wxauto"
+ }
+}
+```
+
+
+
+
+**API测试工具**
+
+
+
+
+
CURL预览:
+
curl -X POST http://localhost:5000/api/message/listen/remove \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "nickname": "测试群"
+ }'
+
+
+
+
响应结果:
+
点击"移除监听对象"按钮查看响应
+
+
+
+
+
+### 6. 群组相关接口
+
+#### 获取群列表
+```http
+GET /api/group/list
+```
+
+CURL 示例:
+```bash
+curl -X GET http://10.255.0.90:5000/api/group/list \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "groups": [
+ {
+ "name": "测试群",
+ "member_count": 100
+ }
+ ]
+ }
+}
+```
+
+#### 群管理操作
+```http
+POST /api/group/manage
+```
+
+CURL 示例:
+```bash
+# 重命名群
+curl -X POST http://10.255.0.90:5000/api/group/manage \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "group_name": "测试群",
+ "action": "rename",
+ "params": {
+ "new_name": "新群名"
+ }
+ }'
+
+# 退出群
+curl -X POST http://10.255.0.90:5000/api/group/manage \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "group_name": "测试群",
+ "action": "quit"
+ }'
+```
+
+请求体:
+```json
+{
+ "group_name": "测试群",
+ "action": "rename", // rename/set_announcement/quit
+ "params": {
+ "new_name": "新群名" // 根据action不同,参数不同
+ }
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "操作成功",
+ "data": {
+ "success": true
+ }
+}
+```
+
+### 7. 好友相关接口
+
+#### 获取好友列表
+```http
+GET /api/contact/list
+```
+
+CURL 示例:
+```bash
+curl -X GET http://10.255.0.90:5000/api/contact/list \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "friends": [
+ {
+ "nickname": "张三",
+ "remark": "张总",
+ "tags": ["同事"]
+ }
+ ]
+ }
+}
+```
+
+#### 添加好友
+```http
+POST /api/contact/add
+```
+
+CURL 示例:
+```bash
+curl -X POST http://10.255.0.90:5000/api/contact/add \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "keywords": "wxid_xxxxx",
+ "message": "你好,我是...",
+ "remark": "备注名",
+ "tags": ["同事", "朋友"]
+ }'
+```
+
+请求体:
+```json
+{
+ "keywords": "wxid_xxxxx", // 微信号/手机号/QQ号
+ "message": "你好,我是...",
+ "remark": "备注名", // 可选
+ "tags": ["同事", "朋友"] // 可选
+}
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送请求成功",
+ "data": {
+ "status": 1, // 0:失败 1:成功 2:已是好友 3:被拉黑 4:未找到账号
+ "message": "发送请求成功"
+ }
+}
+```
+
+### 8. 健康检查接口
+
+获取服务和微信连接状态。
+
+```http
+GET /api/health
+```
+
+CURL 示例:
+```bash
+curl -X GET http://10.255.0.90:5000/api/health \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "服务正常",
+ "data": {
+ "status": "ok",
+ "wechat_status": "connected",
+ "uptime": 3600
+ }
+}
+```
+
+### 9. 系统监控接口
+
+获取当前系统的CPU和内存使用情况。
+
+```http
+GET /api/system/resources
+```
+
+CURL 示例:
+```bash
+curl -X GET http://10.255.0.90:5000/api/system/resources \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "cpu": {
+ "usage_percent": 45.2,
+ "core_count": 8
+ },
+ "memory": {
+ "total": 16384, // 单位:MB
+ "used": 8192, // 单位:MB
+ "free": 8192, // 单位:MB
+ "usage_percent": 50.0
+ }
+ }
+}
+```
+
+响应说明:
+- cpu.usage_percent: CPU总体使用率(百分比)
+- cpu.core_count: CPU核心数
+- memory.total: 总内存大小(MB)
+- memory.used: 已使用内存(MB)
+- memory.free: 空闲内存(MB)
+- memory.usage_percent: 内存使用率(百分比)
+
+注意事项:
+1. 此接口返回的是系统级别的资源使用情况
+2. CPU使用率为所有核心的平均值
+3. 内存数据包含系统缓存
+
+## 注意事项
+
+1. 所有接口调用都需要先调用初始化接口
+2. 注意处理接口调用频率,避免触发微信限制
+3. 文件相关操作需要确保文件路径正确且有访问权限
+4. 建议在调用接口前先检查微信在线状态
+
+## 使用注意事项
+
+1. 所有接口调用前请确保:
+ - 已通过API密钥验证
+ - 已调用初始化接口
+ - 微信处于登录状态
+
+2. 使用监听相关功能时:
+ - 添加监听前请确认对象存在
+ - 及时处理监听消息避免内存占用过大
+ - 不再需要时记得移除监听
+
+3. 发送消息时:
+ - 注意消息频率,避免触发微信限制
+ - 文件发送前确认路径正确且有访问权限
+ - 使用打字机模式时预留合适的打字间隔
+
+4. 异常处理:
+ - 妥善处理各类错误码
+ - 实现合适的重试机制
+ - 保持日志记录便于问题排查
+
+## 开发建议
+
+1. 使用合适的HTTP客户端库
+2. 实现请求重试和超时处理
+3. 做好异常捕获和日志记录
+4. 合理设置并发和队列处理
+5. 定期检查微信状态保持连接
+
+## 示例代码
+
+### Python示例
+
+```python
+import requests
+
+class WxAutoAPI:
+ def __init__(self, base_url, api_key):
+ self.base_url = base_url
+ self.headers = {
+ 'X-API-Key': api_key,
+ 'Content-Type': 'application/json'
+ }
+
+ def initialize(self):
+ response = requests.post(
+ f"{self.base_url}/api/wechat/initialize",
+ headers=self.headers
+ )
+ return response.json()
+
+ def send_message(self, receiver, message, at_list=None):
+ data = {
+ 'receiver': receiver,
+ 'message': message,
+ 'at_list': at_list or []
+ }
+ response = requests.post(
+ f"{self.base_url}/api/message/send",
+ headers=self.headers,
+ json=data
+ )
+ return response.json()
+
+ def add_listen(self, who, **kwargs):
+ data = {'who': who, **kwargs}
+ response = requests.post(
+ f"{self.base_url}/api/message/listen/add",
+ headers=self.headers,
+ json=data
+ )
+ return response.json()
+
+ def get_messages(self, who=None):
+ params = {'who': who} if who else {}
+ response = requests.get(
+ f"{self.base_url}/api/message/listen/get",
+ headers=self.headers,
+ params=params
+ )
+ return response.json()
+
+# 使用示例
+api = WxAutoAPI('http://10.255.0.90:5000', 'your-api-key')
+
+# 初始化
+api.initialize()
+
+# 发送消息
+api.send_message('文件传输助手', '测试消息')
+
+# 添加监听
+api.add_listen('测试群', savepic=True)
+
+# 获取消息
+messages = api.get_messages('测试群')
+print(messages)
+```
+
+### JavaScript示例
+
+```javascript
+class WxAutoAPI {
+ constructor(baseUrl, apiKey) {
+ this.baseUrl = baseUrl;
+ this.headers = {
+ 'X-API-Key': apiKey,
+ 'Content-Type': 'application/json'
+ };
+ }
+
+ async initialize() {
+ const response = await fetch(`${this.baseUrl}/api/wechat/initialize`, {
+ method: 'POST',
+ headers: this.headers
+ });
+ return response.json();
+ }
+
+ async sendMessage(receiver, message, atList = []) {
+ const response = await fetch(`${this.baseUrl}/api/message/send`, {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify({
+ receiver,
+ message,
+ at_list: atList
+ })
+ });
+ return response.json();
+ }
+
+ async addListen(who, options = {}) {
+ const response = await fetch(`${this.baseUrl}/api/message/listen/add`, {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify({
+ who,
+ ...options
+ })
+ });
+ return response.json();
+ }
+
+ async getMessages(who = null) {
+ const url = new URL(`${this.baseUrl}/api/message/listen/get`);
+ if (who) url.searchParams.set('who', who);
+
+ const response = await fetch(url, {
+ headers: this.headers
+ });
+ return response.json();
+ }
+}
+
+// 使用示例
+const api = new WxAutoAPI('http://10.255.0.90:5000', 'your-api-key');
+
+async function demo() {
+ // 初始化
+ await api.initialize();
+
+ // 发送消息
+ await api.sendMessage('文件传输助手', '测试消息');
+
+ // 添加监听
+ await api.addListen('测试群', { savepic: true });
+
+ // 获取消息
+ const messages = await api.getMessages('测试群');
+ console.log(messages);
+}
+
+demo().catch(console.error);
+```
+
+---
+
+## 新增API功能 (v2.0)
+
+### Chat类操作 (`/api/chat/`)
+
+#### 显示聊天窗口
+```http
+POST /api/chat/show
+```
+
+请求体:
+```json
+{
+ "who": "测试群"
+}
+```
+
+#### 加载更多聊天记录
+```http
+POST /api/chat/load-more-messages
+```
+
+请求体:
+```json
+{
+ "who": "测试群"
+}
+```
+
+#### 获取所有消息
+```http
+GET /api/chat/get-all-messages?who=测试群
+```
+
+#### 关闭聊天窗口
+```http
+POST /api/chat/close
+```
+
+请求体:
+```json
+{
+ "who": "测试群"
+}
+```
+
+#### 发送自定义表情 (Plus版)
+```http
+POST /api/chat/send-emotion
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "emotion_index": 1
+}
+```
+
+#### 合并转发消息 (Plus版)
+```http
+POST /api/chat/merge-forward
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_ids": ["msg1", "msg2"],
+ "to_friends": ["好友1", "好友2"]
+}
+```
+
+#### 获取对话框 (Plus版)
+```http
+GET /api/chat/get-dialog?who=测试群
+```
+
+#### 获取置顶消息 (Plus版)
+```http
+GET /api/chat/get-top-message?who=测试群
+```
+
+#### 添加监听对象
+```http
+POST /api/chat/listen/add
+```
+
+
+
+
+**请求体:**
+```json
+{
+ "nickname": "测试群"
+}
+```
+
+**响应示例:**
+```json
+{
+ "code": 0,
+ "message": "添加监听成功",
+ "data": {
+ "nickname": "测试群",
+ "library": "wxauto"
+ }
+}
+```
+
+
+
+
+**API测试工具**
+
+
+
+
+
CURL预览:
+
curl -X POST http://localhost:5000/api/chat/listen/add \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "nickname": "测试群"
+ }'
+
+
+
+
响应结果:
+
点击"添加监听对象"按钮查看响应
+
+
+
+
+
+#### 获取监听消息
+```http
+GET /api/chat/listen/get
+```
+
+
+
+
+**响应示例:**
+```json
+{
+ "code": 0,
+ "message": "获取消息成功",
+ "data": {
+ "messages": {
+ "测试群": [
+ {
+ "type": "text",
+ "content": "测试消息",
+ "sender": "发送者",
+ "id": "msg123",
+ "time": "2025-06-27 23:17"
+ }
+ ]
+ }
+ }
+}
+```
+
+
+
+
+**API测试工具**
+
+
+
+
+
CURL预览:
+
curl -X GET http://localhost:5000/api/chat/listen/get \
+ -H "X-API-Key: test-key-2"
+
+
+
+
响应结果:
+
点击"获取监听消息"按钮查看响应
+
+
+
+
+
+#### 移除监听对象
+```http
+POST /api/chat/listen/remove
+```
+
+
+
+
+**请求体:**
+```json
+{
+ "nickname": "测试群"
+}
+```
+
+**响应示例:**
+```json
+{
+ "code": 0,
+ "message": "移除监听成功",
+ "data": {
+ "nickname": "测试群",
+ "library": "wxauto"
+ }
+}
+```
+
+
+
+
+**API测试工具**
+
+
+
+
+
CURL预览:
+
curl -X POST http://localhost:5000/api/chat/listen/remove \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "nickname": "测试群"
+ }'
+
+
+
+
响应结果:
+
点击"移除监听对象"按钮查看响应
+
+
+
+
+
+### 群组管理 (`/api/group/`)
+
+#### 添加群成员 (Plus版)
+```http
+POST /api/group/add-members
+```
+
+请求体:
+```json
+{
+ "group": "测试群",
+ "members": ["好友1", "好友2"],
+ "reason": "邀请理由"
+}
+```
+
+#### 获取群成员列表 (Plus版)
+```http
+GET /api/group/get-members?who=测试群
+```
+
+或者使用POST方法:
+```http
+POST /api/group/get-members
+```
+
+请求体:
+```json
+{
+ "group_name": "测试群"
+}
+```
+
+#### 移除群成员 (Plus版)
+```http
+POST /api/group/remove-members
+```
+
+请求体:
+```json
+{
+ "group": "测试群",
+ "members": ["成员1", "成员2"]
+}
+```
+
+#### 管理群聊 (Plus版)
+```http
+POST /api/group/manage
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "name": "新群名",
+ "remark": "群备注",
+ "myname": "我的群昵称",
+ "notice": "群公告",
+ "quit": false
+}
+```
+
+#### 获取最近群聊列表 (Plus版)
+```http
+GET /api/group/get-recent-groups
+```
+
+别名路由(兼容性):
+```http
+GET /api/group/get-recent?limit=10
+```
+
+**参数说明**:
+- 此接口无需参数,获取所有最近聊天中的群聊
+
+**响应示例**:
+```json
+{
+ "code": 0,
+ "message": "获取最近群聊成功",
+ "data": {
+ "groups": [
+ ["群聊名称1", "未读消息数"],
+ ["群聊名称2", "未读消息数"]
+ ]
+ }
+}
+```
+
+#### 获取通讯录群聊列表 (Plus版)
+```http
+GET /api/group/get-contact-groups?speed=1&interval=0.1
+```
+
+**参数说明**:
+- `speed` (可选): 滚动速度,默认为1
+- `interval` (可选): 滚动时间间隔,默认为0.1秒
+
+**响应示例**:
+```json
+{
+ "code": 0,
+ "message": "获取通讯录群聊成功",
+ "data": {
+ "groups": ["群聊1", "群聊2", "群聊3"]
+ }
+}
+```
+
+**注意**:此接口获取的是通讯录中的群聊,不是最近聊天中的群聊。如果通讯录中没有群聊,将返回空列表。要获取最近聊天中的群聊,请使用 `get-recent-groups` 接口。
+
+### 好友管理 (`/api/friend/`)
+
+#### 获取好友详情 (Plus版)
+```http
+GET /api/friend/get-details?n=10&tag=标签&timeout=60000
+```
+
+或者使用POST方法:
+```http
+POST /api/friend/get-details
+```
+
+请求体:
+```json
+{
+ "n": 10,
+ "tag": "标签",
+ "timeout": 60000
+}
+```
+
+#### 获取新好友申请 (Plus版)
+```http
+GET /api/friend/get-new-friends?acceptable=true
+```
+
+别名路由(兼容性):
+```http
+GET /api/friend/get-new-requests?acceptable=true
+```
+
+#### 添加新好友 (Plus版)
+```http
+POST /api/friend/add-new-friend
+```
+
+请求体:
+```json
+{
+ "keywords": "搜索关键词",
+ "addmsg": "添加好友消息",
+ "remark": "好友备注",
+ "tags": ["标签1", "标签2"],
+ "permission": "朋友圈",
+ "timeout": 5
+}
+```
+
+#### 管理好友 (Plus版)
+```http
+POST /api/friend/manage
+```
+
+请求体:
+```json
+{
+ "who": "好友名",
+ "remark": "新备注",
+ "tags": ["新标签"]
+}
+```
+
+#### 从群聊添加好友 (Plus版)
+```http
+POST /api/friend/add-from-group
+```
+
+请求体:
+```json
+{
+ "who": "群名",
+ "index": 1,
+ "addmsg": "添加好友消息",
+ "remark": "好友备注",
+ "tags": ["标签"],
+ "permission": "仅聊天",
+ "exact": false
+}
+```
+
+### WeChat类扩展 (`/api/wechat/`)
+
+#### 获取会话列表
+```http
+GET /api/wechat/get-session
+```
+
+#### 发送链接卡片 (Plus版)
+```http
+POST /api/wechat/send-url-card
+```
+
+请求体:
+```json
+{
+ "url": "https://example.com",
+ "friends": ["好友1", "好友2"],
+ "timeout": 10
+}
+```
+
+#### 打开聊天窗口
+```http
+POST /api/wechat/chat-with
+```
+
+请求体:
+```json
+{
+ "who": "好友名",
+ "exact": false
+}
+```
+
+#### 获取子窗口
+```http
+GET /api/wechat/get-sub-window?nickname=好友名
+```
+
+#### 获取所有子窗口
+```http
+GET /api/wechat/get-all-sub-windows
+```
+
+别名路由(兼容性):
+```http
+GET /api/wechat/get-sub-windows
+```
+
+#### 开始监听
+```http
+POST /api/wechat/start-listening
+```
+
+#### 停止监听
+```http
+POST /api/wechat/stop-listening
+```
+
+请求体:
+```json
+{
+ "remove": true
+}
+```
+
+#### 切换到聊天页面
+```http
+POST /api/wechat/switch-to-chat
+```
+
+#### 切换到联系人页面
+```http
+POST /api/wechat/switch-to-contact
+```
+
+#### 检查在线状态 (Plus版)
+```http
+GET /api/wechat/is-online
+```
+
+#### 获取个人信息 (Plus版)
+```http
+GET /api/wechat/get-my-info
+```
+
+#### 保持程序运行
+```http
+POST /api/wechat/keep-running
+```
+
+请求体:
+```json
+{
+ "timeout": 0
+}
+```
+
+### 消息操作 (`/api/message/`)
+
+#### 点击消息
+```http
+POST /api/message/click
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID"
+}
+```
+
+#### 引用回复消息
+```http
+POST /api/message/quote
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID",
+ "reply_text": "回复内容"
+}
+```
+
+#### 转发消息
+```http
+POST /api/message/forward
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID",
+ "to_friends": ["好友1", "好友2"]
+}
+```
+
+#### 拍一拍 (Plus版)
+```http
+POST /api/message/tickle
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID"
+}
+```
+
+#### 删除消息 (Plus版)
+```http
+POST /api/message/delete
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID"
+}
+```
+
+#### 下载图片/文件
+```http
+POST /api/message/download
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID",
+ "save_path": "保存路径"
+}
+```
+
+#### 语音转文字
+```http
+POST /api/message/to-text
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "语音消息ID"
+}
+```
+
+#### 右键菜单操作
+```http
+POST /api/message/select-option
+```
+
+请求体:
+```json
+{
+ "who": "测试群",
+ "message_id": "消息ID",
+ "option": "菜单选项"
+}
+```
+
+### 朋友圈功能 (`/api/moments/`) - Plus版
+
+#### 进入朋友圈
+```http
+POST /api/moments/open
+```
+
+#### 获取朋友圈内容
+```http
+GET /api/moments/get-moments?n=10&timeout=10
+```
+
+#### 保存朋友圈图片
+```http
+POST /api/moments/save-images
+```
+
+请求体:
+```json
+{
+ "moment_index": 0,
+ "save_path": "保存路径"
+}
+```
+
+#### 点赞朋友圈
+```http
+POST /api/moments/like
+```
+
+请求体:
+```json
+{
+ "moment_index": 0
+}
+```
+
+### 辅助类功能 (`/api/auxiliary/`)
+
+#### 点击会话
+```http
+POST /api/auxiliary/session/click
+```
+
+请求体:
+```json
+{
+ "session_name": "会话名"
+}
+```
+
+#### 接受好友申请 (Plus版)
+```http
+POST /api/auxiliary/new-friend/accept
+```
+
+请求体:
+```json
+{
+ "friend_name": "申请人名",
+ "remark": "好友备注",
+ "tags": ["标签"]
+}
+```
+
+#### 拒绝好友申请 (Plus版)
+```http
+POST /api/auxiliary/new-friend/reject
+```
+
+请求体:
+```json
+{
+ "friend_name": "申请人名"
+}
+```
+
+#### 自动登录 (Plus版)
+```http
+POST /api/auxiliary/login/auto
+```
+
+自动登录微信,仅可自动登录的微信有效。
+
+请求体:
+```json
+{
+ "wxpath": "D:/path/to/WeChat.exe",
+ "timeout": 10
+}
+```
+
+参数说明:
+- `wxpath`: 微信客户端路径(可选,不提供时自动检测)
+- `timeout`: 登录超时时间,单位秒(默认10秒)
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "自动登录执行完成",
+ "data": {
+ "wxpath": "D:/path/to/WeChat.exe",
+ "timeout": 10,
+ "login_result": true,
+ "success": true
+ }
+}
+```
+
+#### 获取登录二维码 (Plus版)
+```http
+POST /api/auxiliary/login/qrcode
+```
+
+获取登录二维码,返回base64编码的二维码图片。
+
+请求体:
+```json
+{
+ "wxpath": "D:/path/to/WeChat.exe"
+}
+```
+
+参数说明:
+- `wxpath`: 微信客户端路径(可选,不提供时自动检测)
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取登录二维码成功",
+ "data": {
+ "wxpath": "D:/path/to/WeChat.exe",
+ "qrcode_path": "/path/to/qrcode.png",
+ "qrcode_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
+ "qrcode_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
+ "mime_type": "image/png"
+ }
+}
+```
+
+返回数据说明:
+- `qrcode_path`: 二维码图片文件路径
+- `qrcode_base64`: 二维码图片的base64编码
+- `qrcode_data_url`: 可直接在HTML中使用的data URL格式
+- `mime_type`: 图片MIME类型
+
+## 版本兼容性说明
+
+### 基础版本 (wxauto)
+- 支持所有基础消息功能
+- 支持监听功能
+- 支持基本的聊天窗口操作
+
+### Plus版本 (wxautox)
+- 支持所有基础功能
+- 支持群组管理功能
+- 支持好友管理功能
+- 支持朋友圈功能
+- 支持高级消息操作
+
+## 使用建议
+
+1. **功能检查**: 使用Plus版功能前,建议先检查当前库版本
+2. **错误处理**: 所有API都有完善的错误处理,请根据错误码进行相应处理
+3. **监听管理**: 合理使用监听功能,避免监听过多对象影响性能
+4. **文件操作**: 文件路径使用绝对路径,确保有足够的访问权限
+
+## 更新日志
+
+### v2.0 (2025-06-24)
+- 新增Chat类完整API支持
+- 新增群组管理功能
+- 新增好友管理功能
+- 新增朋友圈功能
+- 新增消息操作功能
+- 新增辅助类功能
+- 完善错误处理和文档
+
+
+
+
\ No newline at end of file
diff --git a/doc/wxauto_api/api_usage.md b/doc/wxauto_api/api_usage.md
new file mode 100644
index 0000000..21c4219
--- /dev/null
+++ b/doc/wxauto_api/api_usage.md
@@ -0,0 +1,317 @@
+# WxAuto HTTP API 使用文档
+
+## 接口认证
+
+所有API请求都需要在请求头中包含API密钥:
+```http
+X-API-Key: test-key-2
+```
+
+## API接口示例
+
+### 1. API密钥验证
+
+验证API密钥是否有效。
+
+```bash
+curl -X POST http://10.255.0.90:5000/api/auth/verify \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "验证成功",
+ "data": {
+ "valid": true
+ }
+}
+```
+
+### 2. 微信基础功能
+
+#### 2.1 初始化微信
+
+初始化微信实例,建议在使用其他接口前先调用此接口。
+
+```bash
+curl -X POST http://10.255.0.90:5000/api/wechat/initialize \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "初始化成功",
+ "data": {
+ "status": "connected"
+ }
+}
+```
+
+#### 2.2 获取微信状态
+
+检查微信连接状态。
+
+```bash
+curl -X GET http://10.255.0.90:5000/api/wechat/status \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "status": "online"
+ }
+}
+```
+
+### 3. 消息相关接口
+
+#### 3.1 发送普通文本消息
+
+发送普通文本消息到指定联系人或群组。
+
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/send \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "receiver": "文件传输助手",
+ "message": "这是一条测试消息",
+ "at_list": ["张三", "李四"],
+ "clear": true
+ }'
+```
+
+参数说明:
+- receiver: 接收者(联系人或群组名称)
+- message: 消息内容
+- at_list: (可选)需要@的群成员列表
+- clear: (可选)是否清除输入框,默认为true
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "message_id": "success"
+ }
+}
+```
+
+#### 3.2 发送打字机模式消息
+
+使用打字机模式发送消息(模拟真人打字效果)。
+
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/send-typing \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "receiver": "文件传输助手",
+ "message": "这是一条打字机模式消息\n这是第二行",
+ "at_list": ["张三"],
+ "clear": true
+ }'
+```
+
+参数说明同普通消息发送。
+
+#### 3.3 发送文件
+
+发送一个或多个文件。
+
+```bash
+curl -X POST http://10.255.0.90:5000/api/message/send-file \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "receiver": "文件传输助手",
+ "file_paths": [
+ "D:/test/file1.txt",
+ "D:/test/image.jpg"
+ ]
+ }'
+```
+
+参数说明:
+- receiver: 接收者
+- file_paths: 要发送的文件路径列表
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "发送成功",
+ "data": {
+ "success_count": 2,
+ "failed_files": []
+ }
+}
+```
+
+### 4. 群组相关接口
+
+#### 4.1 获取群列表
+
+获取当前账号的群聊列表。
+
+```bash
+curl -X GET http://10.255.0.90:5000/api/group/list \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "groups": [
+ {"name": "测试群1"},
+ {"name": "测试群2"}
+ ]
+ }
+}
+```
+
+#### 4.2 群组管理
+
+执行群组管理操作(如重命名、退群等)。
+
+```bash
+curl -X POST http://10.255.0.90:5000/api/group/manage \
+ -H "X-API-Key: test-key-2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "group_name": "测试群",
+ "action": "rename",
+ "params": {
+ "new_name": "新群名称"
+ }
+ }'
+```
+
+支持的操作类型:
+- rename: 重命名群组
+- quit: 退出群组
+
+### 5. 联系人相关接口
+
+#### 5.1 获取好友列表
+
+获取当前账号的好友列表。
+
+```bash
+curl -X GET http://10.255.0.90:5000/api/contact/list \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "获取成功",
+ "data": {
+ "friends": [
+ {"nickname": "张三"},
+ {"nickname": "李四"}
+ ]
+ }
+}
+```
+
+### 6. 健康检查接口
+
+获取服务和微信连接状态。
+
+```bash
+curl -X GET http://10.255.0.90:5000/api/health \
+ -H "X-API-Key: test-key-2"
+```
+
+响应示例:
+```json
+{
+ "code": 0,
+ "message": "服务正常",
+ "data": {
+ "status": "ok",
+ "wechat_status": "connected",
+ "uptime": 3600
+ }
+}
+```
+
+## 错误码说明
+
+- 0: 成功
+- 1001: 认证失败(API密钥无效)
+- 1002: 参数错误
+- 2001: 微信未初始化
+- 2002: 微信已掉线
+- 3001: 发送消息失败
+- 3002: 获取消息失败
+- 4001: 群操作失败
+- 5001: 好友操作失败
+- 5000: 服务器内部错误
+
+## 使用建议
+
+1. 在使用其他接口前,先调用初始化接口
+2. 使用健康检查接口监控服务状态
+3. 合理处理错误码,做好重试机制
+4. 注意文件发送时的路径正确性
+5. 群发消息时建议加入适当延时
+
+## PowerShell示例
+
+如果您使用PowerShell,可以使用以下格式发送请求:
+
+```powershell
+$headers = @{
+ "X-API-Key" = "test-key-2"
+ "Content-Type" = "application/json"
+}
+
+$body = @{
+ receiver = "文件传输助手"
+ message = "测试消息"
+} | ConvertTo-Json
+
+Invoke-RestMethod -Method Post -Uri "http://10.255.0.90:5000/api/message/send" -Headers $headers -Body $body
+```
+
+## Python示例
+
+使用Python requests库的示例:
+
+```python
+import requests
+
+API_KEY = "test-key-2"
+BASE_URL = "http://10.255.0.90:5000/api"
+
+headers = {
+ "X-API-Key": API_KEY,
+ "Content-Type": "application/json"
+}
+
+# 发送消息示例
+response = requests.post(
+ f"{BASE_URL}/message/send",
+ headers=headers,
+ json={
+ "receiver": "文件传输助手",
+ "message": "测试消息"
+ }
+)
+
+print(response.json())
\ No newline at end of file
diff --git a/doc/wxauto_api/wxauto_docs_organized.md b/doc/wxauto_api/wxauto_docs_organized.md
new file mode 100644
index 0000000..d21fe64
--- /dev/null
+++ b/doc/wxauto_api/wxauto_docs_organized.md
@@ -0,0 +1,1909 @@
+# wxauto 完整文档
+
+> 本文档抓取自 https://plus.wxauto.org/docs/
+> 抓取时间: 2025-06-24 10:16:49
+
+## 目录
+
+* [wxauto(x)](#wxautox)
+ * [什么是 wxauto?](#-wxauto)
+ * [wxauto 的主要功能](#wxauto-)
+ * [安装和使用](#)
+ * [会封号吗](#)
+ * [交流](#)
+* [一、环境配置和安装](#)
+ * [一、环境配置](#)
+ * [二、安装](#)
+ * [1. 开源版](#1-)
+ * [2. ✨Plus版](#2-plus)
+ * [三、测试运行](#)
+* [三、核心类概念](#)
+ * [Chat](#chat)
+ * [WeChat](#wechat)
+ * [初始化参数](#)
+ * [Message](#message)
+ * [WxResponse](#wxresponse)
+ * [WxParam](#wxparam)
+* [二、快速开始](#)
+ * [快速开始](#)
+ * [获取微信实例](#)
+ * [发送消息](#)
+ * [获取当前聊天窗口消息](#)
+* [五、使用示例](#)
+ * [1. 基本使用](#1-)
+ * [2. 监听消息](#2-)
+ * [3. 处理好友申请](#3-)
+ * [4. 使用打字机模式发送消息](#4-)
+ * [5. 获取多个微信客户端](#5-)
+ * [6. 自动登录](#6-)
+ * [7. 获取登录二维码](#7-)
+ * [8. 合并转发消息](#8-)
+* [六、常见问题](#)
+ * [不同获取消息的方法有什么区别](#)
+ * [监听模式](#)
+ * [优点](#)
+ * [缺点](#)
+ * [全局模式](#)
+ * [为什么会掉线](#)
+ * [plus版本会掉线吗](#plus)
+ * [如何规避](#)
+ * [掉线怎么办](#)
+ * [支持Linux/Mac吗](#linuxmac)
+ * [Plus版本后台模式是什么](#plus)
+ * [是否支持微信多开](#)
+ * [为什么安装成功但是无法导入](#)
+ * [支持企业微信吗](#)
+ * [提示](#)
+ * [有什么限制](#)
+ * [会持续更新吗](#)
+ * [可以最小化吗](#)
+* [四、核心类方法文档](#)
+* [Chat类](#chat)
+ * [Chat 类属性](#chat-)
+ * [聊天窗口类型 chat_type](#-chattype)
+ * [Chat 类方法](#chat-)
+ * [显示窗口 Show](#-show)
+ * [获取聊天窗口信息 ChatInfo](#-chatinfo)
+ * [✨@所有人 AtAll](#-atall)
+ * [发送消息 SendMsg](#-sendmsg)
+ * [✨发送文本消息(打字机模式)SendTypingText](#sendtypingtext)
+ * [发送文件 SendFiles](#-sendfiles)
+ * [✨发送自定义表情 SendEmotion](#-sendemotion)
+ * [获取当前聊天窗口的所有消息 GetAllMessage](#-getallmessage)
+ * [加载当前窗口更多聊天记录 LoadMoreMessage](#-loadmoremessage)
+ * [✨添加群成员 AddGroupMembers](#-addgroupmembers)
+ * [✨获取当前聊天群成员 GetGroupMembers](#-getgroupmembers)
+ * [✨移除群成员 RemoveGroupMembers](#-removegroupmembers)
+ * [✨从群聊中添加好友 AddFriendFromGroup](#-addfriendfromgroup)
+ * [✨修改好友备注名或标签 ManageFriend](#-managefriend)
+ * [✨管理当前群聊 ManageGroup](#-managegroup)
+ * [关闭窗口 Close](#-close)
+ * [✨合并转发消息 MergeForward](#-mergeforward)
+ * [✨获取对话框 GetDialog](#-getdialog)
+ * [✨移除置顶消息 GetTopMessage](#-gettopmessage)
+* [Message类](#message)
+ * [chat_info](#chatinfo)
+ * [✨ get_all_text](#-getalltext)
+ * [roll_into_view](#rollintoview)
+ * [SystemMessage](#systemmessage)
+ * [TickleMessage](#ticklemessage)
+ * [TimeMessage](#timemessage)
+ * [HumanMessage](#humanmessage)
+ * [click](#click)
+ * [select_option](#selectoption)
+ * [quote](#quote)
+ * [forward](#forward)
+ * [✨tickle](#tickle)
+ * [✨delete](#delete)
+ * [✨download_head_image](#downloadheadimage)
+ * [FriendMessage](#friendmessage)
+ * [✨sender_info](#senderinfo)
+ * [✨at](#at)
+ * [✨add_friend](#addfriend)
+ * [✨multi_select](#multiselect)
+ * [SelfMessage](#selfmessage)
+ * [TextMessage](#textmessage)
+ * [QuoteMessage](#quotemessage)
+ * [✨download_quote_image](#downloadquoteimage)
+ * [✨click_quote](#clickquote)
+ * [ImageMessage](#imagemessage)
+ * [download](#download)
+ * [VideoMessage](#videomessage)
+ * [VoiceMessage](#voicemessage)
+ * [to_text](#totext)
+ * [FileMessage](#filemessage)
+ * [✨LocationMessage](#locationmessage)
+ * [✨LinkMessage](#linkmessage)
+ * [✨get_url](#geturl)
+ * [✨EmotionMessage](#emotionmessage)
+ * [✨MergeMessage](#mergemessage)
+ * [✨get_messages](#getmessages)
+ * [✨PersonalCardMessage](#personalcardmessage)
+ * [✨NoteMessage](#notemessage)
+ * [✨get_content](#getcontent)
+ * [✨save_files](#savefiles)
+ * [✨to_markdown](#tomarkdown)
+ * [OtherMessage](#othermessage)
+* [WeChat类](#wechat)
+ * [WeChat 类方法](#wechat-)
+ * [概念](#)
+ * [保持程序运行 KeepRunning](#-keeprunning)
+ * [获取当前会话列表 GetSession](#-getsession)
+ * [✨发送链接卡片 SendUrlCard](#-sendurlcard)
+ * [打开聊天窗口 ChatWith](#-chatwith)
+ * [获取子窗口实例 GetSubWindow](#-getsubwindow)
+ * [获取所有子窗口实例 GetAllSubWindow](#-getallsubwindow)
+ * [添加监听聊天窗口 AddListenChat](#-addlistenchat)
+ * [移除监听聊天 RemoveListenChat](#-removelistenchat)
+ * [开始监听 StartListening](#-startlistening)
+ * [停止监听 StopListening](#-stoplistening)
+ * [✨进入朋友圈 Moments](#-moments)
+ * [获取下一个新消息 GetNextNewMessage](#-getnextnewmessage)
+ * [✨获取好友列表 GetFriendDetails](#-getfrienddetails)
+ * [✨获取新的好友申请列表 GetNewFriends](#-getnewfriends)
+ * [✨添加新的好友 AddNewFriend](#-addnewfriend)
+ * [✨获取最近群聊名称列表 GetAllRecentGroups](#-getallrecentgroups)
+ * [切换到聊天页面 SwitchToChat](#-switchtochat)
+ * [切换到联系人页面 SwitchToContact](#-switchtocontact)
+ * [✨是否在线 IsOnline](#-isonline)
+ * [✨获取我的信息 GetMyInfo](#-getmyinfo)
+ * [✨获取通讯录群聊列表 GetContactGroups](#-getcontactgroups)
+* [✨朋友圈类](#)
+ * [MomentsWnd](#momentswnd)
+ * [GetMoments](#getmoments)
+ * [Refresh](#refresh)
+ * [close](#close)
+ * [Moments](#moments)
+ * [获取朋友圈内容](#)
+ * [SaveImages](#saveimages)
+ * [Like](#like)
+ * [Comment](#comment)
+* [其他类](#)
+ * [SessionElement](#sessionelement)
+ * [double_click](#doubleclick)
+ * [✨hide](#hide)
+ * [✨select_option](#selectoption)
+ * [NewFriendElement](#newfriendelement)
+ * [accept](#accept)
+ * [✨reply](#reply)
+ * [✨get_account](#getaccount)
+ * [✨LoginWnd](#loginwnd)
+ * [login](#login)
+ * [get_qrcode](#getqrcode)
+ * [reopen](#reopen)
+ * [open](#open)
+ * [WeChatImage](#wechatimage)
+ * [ocr](#ocr)
+ * [save](#save)
+ * [✨WeChatDialog](#wechatdialog)
+
+---
+
+# wxauto(x)
+
+探索以下部分,了解如何使用 wxauto:
+
+文档中标题前缀为✨标志的,为Plus版本特有方法,开源版无法调用
+
+## 什么是 wxauto?
+
+wxauto 是我在2020年开发的一个基于 UIAutomation 的开源 Python 微信自动化库,最初只是一个简单的脚本,只能获取消息和发送消息,经历了2年多的停滞,期间很多网友留言说需要更多的功能,所以在2023年针对新版微信重新开发了 wxauto,增加了更多的功能,即使 Python 初学者也可以简单上手自动化微信操作。目前已实现很多日常的微信操作的自动化,如自动发送消息、自动添加好友、自动回复、自动获取聊天记录、图片、文件等功能,后续还会根据反馈更新更多功能。
+
+## wxauto 的主要功能
+
+* 消息发送:支持发送文字、图片、文件、@群好友、引用消息等功能
+* 聊天记录:可获取好友的聊天记录内容
+* 监听消息:实时获取指定监听好友(群)的新消息
+* 其他定制功能:根据需求定制自动化流程,满足各种特殊需求。
+
+## 安装和使用
+
+安装 wxauto 非常简单,在命令行输入以下命令即可:
+
+```bash
+pip install wxauto
+```
+
+接下来,可以按照以下步骤进行基本配置和使用:
+
+引入 wxauto 库:
+
+```python
+from wxauto import WeChat
+```
+
+初始化微信对象:
+
+```python
+wx = WeChat()
+```
+
+发送消息:
+
+```python
+# 给文件传输助手发送消息
+wx.SendMsg('这是通过wxauto发给你的消息!', '文件传输助手')
+```
+
+就这么简单几步,你就可以开始使用 wxauto 了!
+
+## 会封号吗
+
+不封号。
+
+该项目基于Windows官方API开发,不涉及任何侵入、破解、抓包微信客户端应用,完全以人操作微信的行为执行操作
+
+但是如果你有以下行为,即使手动操作也有风控的风险:
+
+* 曾用hook类或webhook类微信工具,如dll注入、itchat及其衍生产品
+* 频繁且大量的发送消息、添加好友等,导致风控
+* 高频率发送机器人特征明显的消息,导致被人举报,致使行为风控
+* 扫码手机与电脑客户端不在同一个城市,导致异地风控
+* 低权重账号做太多动作,低权重账号可能包括:新注册账号长期未登录或不活跃账号未实名认证账号未绑定银行卡账号曾被官方处罚的账号…
+* 新注册账号
+* 长期未登录或不活跃账号
+* 未实名认证账号
+* 未绑定银行卡账号
+* 曾被官方处罚的账号
+* …
+
+## 交流
+
+有任何问题或建议,欢迎加作者好友,备注wxauto
+
+# 一、环境配置和安装
+
+## 一、环境配置
+
+| 环境 | 版本 || --- | --- | --- | --- || Python | 3.9-3.12 || OS | Windows10+, Windows Server2016+ || 微信 | 3.9.8+(不支持4.0) |
+
+## 二、安装
+
+### 1. 开源版
+
+### 2. ✨Plus版
+
+```bash
+pip install wxautox
+
+# 或指定python版本安装:
+py -3.12 -m pip install wxautox
+```
+
+注意
+
+仅支持 Python3.9 至 3.12
+
+激活:
+
+```shell
+wxautox -a 激活码
+```
+
+## 三、测试运行
+
+```python
+from wxauto import WeChat # 开源版
+# from wxautox import WeChat # ✨Plus版
+
+# 初始化微信实例
+wx = WeChat()
+
+# 发送消息
+wx.SendMsg("你好", who="文件传输助手")
+
+# 获取当前聊天窗口消息
+msgs = wx.GetAllMessage()
+
+for msg in msgs:
+ print('==' * 30)
+ print(f"{msg.sender}: {msg.content}")
+```
+
+Success
+
+✅ 如果测试运行成功,恭喜您,环境配置完成!
+
+
+
+# 二、快速开始
+
+## 快速开始
+
+### 获取微信实例
+
+```python
+from wxauto import WeChat
+
+# 初始化微信实例
+wx = WeChat()
+```
+
+### 发送消息
+
+```python
+# 发送消息
+wx.SendMsg("你好", who="文件传输助手")
+```
+
+### 获取当前聊天窗口消息
+
+```python
+# 获取当前聊天窗口消息
+msgs = wx.GetAllMessage()
+
+for msg in msgs:
+ print('==' * 30)
+ print(f"{msg.sender}: {msg.content}")
+```
+
+✅ 恭喜,你已经成功进行了自动化操作,接下来你可以继续探索更多功能。
+
+# 五、使用示例
+
+### 1. 基本使用
+
+```python
+from wxautox import WeChat
+
+# 初始化微信实例
+wx = WeChat()
+
+# 发送消息
+wx.SendMsg("你好", who="张三")
+
+# 获取当前聊天窗口消息
+msgs = wx.GetAllMessage()
+for msg in msgs:
+ print(f"消息内容: {msg.content}, 消息类型: {msg.type}")
+```
+
+### 2. 监听消息
+
+```python
+from wxautox import WeChat
+from wxautox.msgs import FriendMessage
+import time
+
+wx = WeChat()
+
+# 消息处理函数
+def on_message(msg, chat):
+ # 示例1:将消息记录到本地文件
+ with open('msgs.txt', 'a', encoding='utf-8') as f:
+ f.write(msg.content + '\n')
+
+ # 示例2:自动下载图片和视频
+ if msg.type in ('image', 'video'):
+ print(msg.download())
+
+ # 示例3:自动回复收到
+ if isinstance(msg, FriendMessage):
+ time.sleep(len(msg.content))
+ msg.quote('收到')
+
+ ...# 其他处理逻辑,配合Message类的各种方法,可以实现各种功能
+
+# 添加监听,监听到的消息用on_message函数进行处理
+wx.AddListenChat(nickname="张三", callback=on_message)
+
+# 保持程序运行
+wx.KeepRunning()
+```
+
+```python
+# ... 程序运行一段时间后 ...
+
+# 移除监听
+wx.RemoveListenChat(nickname="张三")
+```
+
+### 3. 处理好友申请
+
+```python
+from wxautox import WeChat
+
+wx = WeChat()
+
+# 获取新的好友申请
+newfriends = wx.GetNewFriends(acceptable=True)
+
+# 处理好友申请
+tags = ['同学', '技术群']
+for friend in newfriends:
+ remark = f'备注_{friend.name}'
+ friend.accept(remark=remark, tags=tags) # 接受好友请求,并设置备注和标签
+```
+
+### 4. 使用打字机模式发送消息
+
+```python
+from wxautox import WeChat
+
+wx = WeChat()
+
+# 普通文本发送
+wx.SendTypingText("你好,这是一条测试消息", who="张三")
+
+# 使用@功能和换行
+wx.SendTypingText("各位好:\n{@张三} 请负责前端部分\n{@李四} 请负责后端部分", who="项目群")
+```
+
+### 5. 获取多个微信客户端
+
+```python
+from wxautox import get_wx_clients
+
+# 获取所有微信客户端
+clients = get_wx_clients()
+for client in clients:
+ print(f"微信客户端: {client}")
+```
+
+### 6. 自动登录
+
+```python
+from wxautox import LoginWnd
+
+wxpath = "D:/path/to/WeChat.exe"
+
+# 创建登录窗口
+loginwnd = LoginWnd(wxpath)
+
+# 登录微信
+loginwnd.login()
+```
+
+### 7. 获取登录二维码
+
+```python
+from wxautox import LoginWnd
+
+wxpath = "D:/path/to/WeChat.exe"
+
+# 创建登录窗口
+loginwnd = LoginWnd(wxpath)
+
+# 获取登录二维码图片路径
+qrcode_path = loginwnd.get_qrcode()
+print(qrcode)
+```
+
+### 8. 合并转发消息
+
+```python
+from wxautox import WeChat
+from wxautox.msgs import HumanMessage
+
+wx = WeChat()
+
+# 打开指定聊天窗口
+wx.ChatWith("工作群")
+
+# 获取消息列表
+msgs = wx.GetAllMessage()
+
+# 多选最后五条消息
+n = 0
+for msg in msgs[::-1]:
+ if n >= 5:
+ break
+ if isinstance(msg, HumanMessage):
+ n += 1
+ msg.multi_select()
+
+# 执行合并转发
+targets = [
+ '张三',
+ '李四
+]
+wx.MergeForward(targets)
+```
+
+# 六、常见问题
+
+## 不同获取消息的方法有什么区别
+
+wxauto中,有以下获取消息的方法,除GetAllMessage之外,其余方法均用于获取新消息
+
+| 方法 | 说明 || --- | --- | --- | --- || GetAllMessage | 获取当前聊天页面中已加载的消息 || GetNextNewMessage | 获取微信主窗口中,其中一个未设置消息免打扰窗口的新消息 || AddListenMessage | 获取监听模式下聊天窗口的新消息 |
+
+### 监听模式
+
+AddListenMessage
+
+调用AddListenMessage方法将目标聊天窗口独立出去加入监听列表,获取新消息,并触发回调函数来处理每一条消息
+
+#### 优点
+
+* 准确
+* 读取速度快
+
+#### 缺点
+
+* 数量限制,最多设置40个监听对象
+
+### 全局模式
+
+GetNextNewMessage
+
+获取所有微信主窗口中,未被设置为消息免打扰的窗口中的新消息
+
+* 没有数量限制,无差别获取所有窗口新消息
+
+* 必须进行UI操作,速度可能相较监听模式慢些该方法原理是获取会话列表中,聊天对象头像上的未读消息角标数字来判断新消息数,然后切换到该聊天窗口,获取新消息
+
+必须进行UI操作,速度可能相较监听模式慢些
+
+该方法原理是获取会话列表中,聊天对象头像上的未读消息角标数字来判断新消息数,然后切换到该聊天窗口,获取新消息
+
+## 为什么会掉线
+
+掉线是微信3.9.9及以后的版本中加入的机制,客户端频繁操作导致的
+
+##### plus版本会掉线吗
+
+会,手动操作频繁也会掉线,是微信客户端的机制
+
+##### 如何规避
+
+* 加延迟时间
+* 用3.9.8版本客户端
+* plus版本提供自动登录、获取二维码操作
+
+## 掉线怎么办
+
+掉线是微信客户端在3.9.9+版本以后新增的安全机制,主要发生在微信号在陌生电脑设备登录后触发,不会涉及封号,没有完美解决方案,以下提供两个思路:
+
+* 微信号在同一台电脑养至可快速登录,几乎不会掉线,再进行wxauto托管
+* 想办法使用3.9.8版本微信客户端,完全不掉线(绕过微信版本检测的风险自行承担)
+* plus版本提供掉线检测、二维码获取、自动登录等方法
+
+## 支持Linux/Mac吗
+
+不支持,基于windows官方API开发,只支持windows系统
+
+## Plus版本后台模式是什么
+
+后台模式即不依赖鼠标移动,绝大部分场景无需将微信调到前台窗口即可进行操作,但是有些操作必须要微信在前台才可以操作成功,例如获取发送者详情信息等;
+
+大部分场景下:
+
+* 不抢占鼠标
+* 执行速度快
+* 窗口不必在桌面顶部也能操作
+
+## 是否支持微信多开
+
+wxauto项目不支持一切违反官方用户协议的操作,不建议、不支持、不提供微信多开的方法或行为。
+
+但是如果你自行使用其他方法多开微信,plus版本可用WeChat(nickname='xxx')来区分,但wxauto不承担由你自行多开的行为导致的风险,也不保证所有功能的正常调用。
+
+## 为什么安装成功但是无法导入
+
+检查下安装wxautox的环境与你运行环境是否同一个python环境。
+
+PyCharm默认会给你的项目创建一个虚拟环境,需要在虚拟环境中安装才可以调用
+
+如果不清楚如何使用虚拟环境安装,可问 AI “怎么用pycharm的虚拟环境安装本地离线whl包”
+
+## 支持企业微信吗
+
+不支持。法律风险较高,影响腾讯收入,严抓
+
+如果你的企业开启了在个人微信中接受企业消息的功能,可以在个人微信手动将企业微信群拖出来使用wxauto监听模式进行操作
+
+## 提示
+
+该项目为模拟操作,即模拟用户鼠标键盘操作微信客户端的行为,系统、网络、硬件等个体差异较大,
+
+## 有什么限制
+
+* 不可以发布到公共平台
+* 不可以做违法的事情
+* 个人或内部使用,不允许商业软件厂商进行集成
+
+## 会持续更新吗
+
+订阅期为1年,订阅期内更新免费,订阅过期后不提供更新服务,已获取的版本仍可继续使用
+
+## 可以最小化吗
+
+可以但是不建议。
+
+wxauto项目本身是ui自动化,最小化会导致窗口ui绘制更新慢,自动化效率低
+
+# 三、核心类概念
+
+## Chat
+
+Chat 类代表一个微信聊天窗口实例,提供了与聊天相关的操作方法,用于对微信聊天窗口进行各种操作,后续文档以变量名chat作为该类对象。
+
+## WeChat
+
+WeChat 类是本项目的主要入口点,它继承自 Chat 类,代表微信主窗口实例,用于对微信主窗口进行各种操作,后续文档以变量名wx作为该类对象。
+
+### 初始化参数
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || nickname | str | None | 微信昵称,用于定位特定的微信窗口 || debug | bool | False | 是否开启调试模式 |
+
+```python
+wx = WeChat(nickname="张三")
+```
+
+## Message
+
+Message类代表微信聊天中的消息,分为两个概念:
+
+* 消息内容类型(type):文本消息、图片消息、文件消息、语音消息、卡片消息等等
+* 消息来源类型(attr):系统消息、时间消息、自己发送的消息、对方发来的消息
+
+```python
+# 导入你想要的消息类型
+from wxautox.msgs import (
+ Message,
+ TextMessage,
+ FriendMessage,
+ FriendTextMessage,
+ ...
+)
+
+# 假设你获取到了一个消息对象
+msg: Message = ...
+
+# 如果是对方发来的消息,则回复收到
+if isinstance(msg, FriendMessage):
+ msg.reply("收到")
+```
+
+| type↓ attr→ | 自己的消息SelfMessage | 对方的消息FriendMessage || --- | --- | --- | --- | --- || 文本消息TextMessage | SelfTextMessage | FriendTextMessage || 引用消息QuoteMessage | SelfQuoteMessage | FriendQuoteMessage || 语音消息VoiceMessage | SelfVoiceMessage | FriendVoiceMessage || 图片消息ImageMessage | SelfImageMessage | FriendImageMessage || 视频消息VideoMessage | SelfVideoMessage | FriendVideoMessage || 文件消息FileMessage | SelfFileMessage | FriendFileMessage || 位置消息LocationMessage | SelfLocationMessage | FriendLocationMessage || 链接消息LinkMessage | SelfLinkMessage | FriendLinkMessage || 表情消息EmotionMessage | SelfEmotionMessage | FriendEmotionMessage || 合并消息MergeMessage | SelfMergeMessage | FriendMergeMessage || 名片消息PersonalCardMessage | SelfPersonalCardMessage | FriendPersonalCardMessage || 其他消息OtherMessage | SelfOtherMessage | FriendOtherMessage |
+
+## WxResponse
+
+该类用于该项目多个方法的返回值
+
+```python
+# 这里假设result为某个方法的WxResponse类型返回值
+result: WxResponse = ...
+
+# 判断是否成功
+if result:
+ data = result['data'] # 成功,获取返回数据,大多数情况下为None
+else:
+ print(result['message']) # 该方法调用失败,打印错误信息
+```
+
+## WxParam
+
+* ENABLE_FILE_LOGGER ( bool ) :是否启用日志文件,默认True
+* DEFAULT_SAVE_PATH ( str ) :下载文件/图片默认保存路径,默认为当前工作目录下的wxautox文件下载文件夹
+* ✨MESSAGE_HASH ( bool ) :是否启用消息哈希值用于辅助判断消息,开启后会稍微影响性能,默认False
+* DEFAULT_MESSAGE_XBIAS ( int ) :头像到消息X偏移量,用于消息定位,点击消息等操作,默认51
+* FORCE_MESSAGE_XBIAS ( bool ) :是否强制重新自动获取X偏移量,如果设置为True,则每次启动都会重新获取,默认False
+* LISTEN_INTERVAL ( int ) :监听消息时间间隔,单位秒,默认1
+* ✨LISTENER_EXCUTOR_WORKERS ( int ) :监听执行器线程池大小,根据自身需求和设备性能设置,默认4
+* SEARCH_CHAT_TIMEOUT ( int ) :搜索聊天对象超时时间,单位秒,默认5
+
+```python
+from wxautox import WxParam
+
+WxParam.LISTENER_EXCUTOR_WORKERS = 8
+...
+```
+
+# 四、核心类方法文档
+
+## Chat类
+
+### Chat 类属性
+
+在了解Chat类的方法之前,我想先介绍一下为什么要做这个类。 wxauto(x)这个项目的原理是模拟人工对微信客户端的操作,拿取到的所有信息都是人眼可见的部分, 所以当我们想监听某个人或群消息的时候,需要把这个人的聊天窗口独立出来,以确保UI元素不会因为微信主窗口切换聊天而丢失, 同时也不需要每来一条信息都切换聊天窗口去获取。 所以,Chat类就是用来创建一个独立的聊天窗口,并获取这个聊天窗口的信息。
+
+### 聊天窗口类型 chat_type
+
+获取当前聊天窗口的类型,返回值为字符串,取值范围如下:
+
+* friend:好友
+* group:群聊
+* service:客服
+* official:公众号
+
+```python
+chat_type = chat.chat_type
+```
+
+### Chat 类方法
+
+### 显示窗口 Show
+
+```python
+chat.Show()
+```
+
+### 获取聊天窗口信息 ChatInfo
+
+```python
+info = chat.ChatInfo()
+```
+
+返回值:
+
+* 类型:dict
+* 描述:聊天窗口信息
+* 返回值示例:
+
+```python
+# 好友
+{'chat_type': 'friend', 'chat_name': '张三'}
+
+# 群聊
+{'group_member_count': 500, 'chat_type': 'group', 'chat_name': '工作群'}
+
+# 客服
+{'company': '@肯德基', 'chat_type': 'service', 'chat_name': '店长xxx'}
+
+# 公众号
+{'chat_type': 'official', 'chat_name': '肯德基'}
+```
+
+### ✨@所有人 AtAll
+
+```python
+group = '工作群'
+content = """
+通知:
+下午xxxx
+xxxx
+"""
+
+wx.AtAll(content, group)
+```
+
+msg (str): 发送的消息
+
+ who (str, optional): 发送给谁. Defaults to None.
+
+ exact (bool, optional): 是否精确匹配. Defaults to False.
+
+参数:
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || msg | str | None | 发送的消息 || who | str | None | 发送给谁 || exact | bool | False | 是否精确匹配 |
+
+* 类型:WxResponse
+* 描述:是否发送成功
+
+### 发送消息 SendMsg
+
+```python
+wx.SendMsg(msg="你好", who="张三", clear=True, at="李四", exact=False)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || msg | str | 必填 | 消息内容 || who | str | None | 发送对象,不指定则发送给当前聊天对象,当子窗口时,该参数无效 || clear | bool | True | 发送后是否清空编辑框 || at | Union[str, List[str]] | None | @对象,不指定则不@任何人 || exact | bool | False | 搜索who好友时是否精确匹配,当子窗口时,该参数无效 |
+
+### ✨发送文本消息(打字机模式)SendTypingText
+
+```python
+wx.SendTypingText(msg="你好", who="张三", clear=True, exact=False)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || msg | str | 必填 | 要发送的文本消息 || who | str | None | 发送对象,不指定则发送给当前聊天对象,当子窗口时,该参数无效 || clear | bool | True | 是否清除原本的内容 || exact | bool | False | 搜索who好友时是否精确匹配,当子窗口时,该参数无效 |
+
+示例:
+
+```python
+# 换行及@功能
+wx.SendTypingText('各位下午好\n{@张三}负责xxx\n{@李四}负责xxxx', who='工作群')
+```
+
+### 发送文件 SendFiles
+
+```python
+wx.SendFiles(filepath="C:/文件.txt", who="张三", exact=False)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || filepath | str|list | 必填 | 要复制文件的绝对路径 || who | str | None | 发送对象,不指定则发送给当前聊天对象,当子窗口时,该参数无效 || exact | bool | False | 搜索who好友时是否精确匹配,当子窗口时,该参数无效 |
+
+### ✨发送自定义表情 SendEmotion
+
+```python
+wx.SendEmotion(emotion_index=0, who="张三", exact=False)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || emotion_index | str | 必填 | 表情索引,从0开始 || who | str | None | 发送对象,不指定则发送给当前聊天对象,当子窗口时,该参数无效 || exact | bool | False | 搜索who好友时是否精确匹配,当子窗口时,该参数无效 |
+
+### 获取当前聊天窗口的所有消息 GetAllMessage
+
+```python
+messages = wx.GetAllMessage()
+```
+
+* 类型:List[Message]
+* 描述:当前聊天窗口的所有消息
+
+### 加载当前窗口更多聊天记录 LoadMoreMessage
+
+```python
+wx.LoadMoreMessage()
+```
+
+### ✨添加群成员 AddGroupMembers
+
+```python
+wx.AddGroupMembers(group="技术交流群", members=["张三", "李四"], reason="交流技术")
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || group | str | None | 群名 || members | Union[str, List[str]] | None | 成员名或成员名列表 || reason | str | None | 申请理由,当群主开启验证时需要,不填写则取消申请 |
+
+* 描述:是否添加成功
+
+### ✨获取当前聊天群成员 GetGroupMembers
+
+```python
+members = wx.GetGroupMembers()
+```
+
+* 类型:List[str]
+* 描述:当前聊天群成员列表
+
+### ✨移除群成员 RemoveGroupMembers
+
+```python
+wx.RemoveGroupMembers(group="群名", members=["成员名1", "成员名2"])
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || group | str | None | 群名 || members | str | None | 成员名 |
+
+* 描述:是否移除成功
+
+### ✨从群聊中添加好友 AddFriendFromGroup
+
+```python
+index = 5 # 申请群里索引值为5的成员为好友
+remark = "备注名"
+tags = ["标签1", "标签2"]
+result = wx.AddFriendFromGroup(index=index, remark=remark, tags=tags)
+if result:
+ print("成功发起申请")
+else:
+ print(f"申请失败:{result['message']}")
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || index | int | None | 群聊索引 || who | str | None | 群名,当Chat对象时该参数无效,仅WeChat对象有效 || addmsg | str | None | 申请理由,当群主开启验证时需要,不填写则取消申请 || remark | str | None | 添加好友后的备注名 || tags | list | None | 添加好友后的标签 || permission | Literal[‘朋友圈’, ‘仅聊天’] | ‘仅聊天’ | 添加好友后的权限 || exact | bool | False | 是否精确匹配群聊名 |
+
+### ✨修改好友备注名或标签 ManageFriend
+
+```python
+wx.ManageFriend(remark="新备注名")
+wx.ManageFriend(tags=["标签1", "标签2"])
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || remark | str | None | 备注名 || tags | List[str] | None | 标签列表 |
+
+* 描述:是否成功修改备注名或标签
+
+### ✨管理当前群聊 ManageGroup
+
+```python
+wx.ManageGroup(name="新群名")
+wx.ManageGroup(remark="新备注名")
+wx.ManageGroup(myname="新群昵称")
+wx.ManageGroup(notice="新群公告")
+wx.ManageGroup(quit=True) # 谨慎使用
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || name | str | None | 群名称 || remark | str | None | 备注名 || myname | str | None | 我的群昵称 || notice | str | None | 群公告 || quit | bool | False | 是否退出群,当该项为True时,其他参数无效 |
+
+### 关闭窗口 Close
+
+```python
+wx.Close()
+```
+
+### ✨合并转发消息 MergeForward
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || targets | Union[List[str], str] | None | 要转发的对象 |
+
+* 描述:是否成功转发
+
+### ✨获取对话框 GetDialog
+
+```python
+if dialog := wx.GetDialog():
+ dialog.click_button("确定")
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || wait | int | 3 | 隐性等待时间 |
+
+* 类型:WeChatDialog
+* 描述:对话框对象,如果不存在则返回None
+
+### ✨移除置顶消息 GetTopMessage
+
+```python
+if top_messages := wx.GetTopMessage():
+ for top_message in top_messages:
+ print(f"移除置顶消息: {top_message.content}")
+ top_message.remove()
+```
+
+参数:无
+
+* 类型:List[TopMsg]
+
+## Message类
+
+消息类中,有两个固定属性:
+
+* attr:消息属性,即消息的来源属性system:系统消息time:时间消息tickle:拍一拍消息self:自己发送的消息friend:好友消息other:其他消息
+* system:系统消息
+* time:时间消息
+* tickle:拍一拍消息
+* self:自己发送的消息
+* friend:好友消息
+* other:其他消息
+* type:消息类型,即消息的内容属性text:文本消息quote:引用消息voice:语音消息image:图片消息video:视频消息file:文件消息location:位置消息link:链接消息emotion:表情消息merge:合并转发消息personal_card:个人名片消息note: 笔记消息other:其他消息
+* text:文本消息
+* quote:引用消息
+* voice:语音消息
+* image:图片消息
+* video:视频消息
+* file:文件消息
+* location:位置消息
+* link:链接消息
+* emotion:表情消息
+* merge:合并转发消息
+* personal_card:个人名片消息
+* note: 笔记消息
+
+而self和friend又可以跟消息类型所组合,所以所有消息类别如下:
+
+| | 自己发送的消息SelfMessage | 对方发来的消息FriendMessage || --- | --- | --- | --- | --- || 文本消息TextMessage | SelfTextMessage | FriendTextMessage || 引用消息QuoteMessage | SelfQuoteMessage | FriendQuoteMessage || 语音消息VoiceMessage | SelfVoiceMessage | FriendVoiceMessage || 图片消息ImageMessage | SelfImageMessage | FriendImageMessage || 视频消息VideoMessage | SelfVideoMessage | FriendVideoMessage || 文件消息FileMessage | SelfFileMessage | FriendFileMessage || ✨位置消息LocationMessage | SelfLocationMessage | FriendLocationMessage || ✨链接消息LinkMessage | SelfLinkMessage | FriendLinkMessage || ✨表情消息EmotionMessage | SelfEmotionMessage | FriendEmotionMessage || ✨合并消息MergeMessage | SelfMergeMessage | FriendMergeMessage || ✨名片消息PersonalCardMessage | SelfPersonalCardMessage | FriendPersonalCardMessage || ✨笔记消息NoteMessage | SelfNoteMessage | FriendNoteMessage || 其他消息OtherMessage | SelfOtherMessage | FriendOtherMessage |
+
+简单的使用示例:
+
+```python
+from wxautox.msgs import *
+
+... # 省略获取消息对象的过程
+
+# 假设你获取到了一个消息对象
+msg = ...
+
+# 当消息为好友消息时,回复收到
+# 方法一:
+if msg.attr == 'friend':
+ msg.reply('收到')
+
+# 方法二:
+if isinstance(msg, FriendMessage):
+ msg.reply('收到')
+```
+
+消息基类,所有消息类型都继承自该类
+
+属性(所有消息类型都包含以下属性):
+
+| 属性名 | 类型 | 描述 || --- | --- | --- | --- | --- || type | str | 消息内容类型 || attr | str | 消息来源类型 || info | Dict | 消息的详细信息 || id | str | 消息UI ID(不重复,切换UI后会变) || ✨hash | str | 消息hash值(可能重复,切换UI后不变) || sender | str | 消息发送者 || content | str | 消息内容 |
+
+### chat_info
+
+获取该消息所属聊天窗口的信息
+
+```python
+chat_info = msg.chat_info()
+```
+
+### ✨ get_all_text
+
+获取消息中所有文本内容
+
+```python
+text_list = msg.get_all_text()
+```
+
+### roll_into_view
+
+将消息滚动到视野内
+
+```python
+msg.roll_into_view()
+```
+
+### SystemMessage
+
+系统消息,没有特殊用法
+
+固定属性:
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || attr | str | system | 消息属性 |
+
+### TickleMessage
+
+拍一拍消息,继承自SystemMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || attr | str | tickle | 消息属性 |
+
+特有属性:
+
+| 属性 | 类型 | 描述 || --- | --- | --- | --- | --- || tickle_list | str | 拍一拍消息列表 |
+
+### TimeMessage
+
+时间消息
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || attr | str | time | 消息属性 |
+
+| 属性 | 类型 | 描述 || --- | --- | --- | --- | --- || time | str | 时间 YYYY-MM-DD HH:MM:SS |
+
+### HumanMessage
+
+人发送的消息,即自己或好友、群友发送的消息
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || attr | str | friend | 消息属性 |
+
+### click
+
+点击该消息,一般特殊消息才会有作用,比如图片消息、视频消息等
+
+```python
+msg.click()
+```
+
+### select_option
+
+右键该消息,弹出右键菜单,并选择指定选项
+
+```python
+msg.select_option("复制")
+```
+
+* 描述:操作结果
+
+### quote
+
+引用该消息,并回复
+
+```python
+msg.quote("回复内容")
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || text | str | 无 | 引用内容 || at | Union[List[str], str] | 无 | @用户列表 || timeout | int | 3 | 超时时间,单位为秒 |
+
+### forward
+
+转发该消息
+
+```python
+msg.forward("转发对象名称")
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || targets | Union[List[str], str] | 无 | 转发对象名称 || timeout | int | 3 | 超时时间,单位为秒 |
+
+### ✨tickle
+
+拍一拍该消息发送人
+
+```python
+msg.tickle()
+```
+
+### ✨delete
+
+删除该消息
+
+```python
+msg.delete()
+```
+
+### ✨download_head_image
+
+下载该消息发送人的头像
+
+```python
+msg.download_head_image()
+```
+
+* 类型:Path
+* 描述:下载路径Path对象
+
+### FriendMessage
+
+好友、群友发送的消息,即聊天页面中,左侧人员发送的消息。继承自HumanMessage
+
+### ✨sender_info
+
+获取发送人信息
+
+```python
+msg.sender_info()
+```
+
+* 类型:Dict[str, str]
+
+### ✨at
+
+@该消息发送人
+
+```python
+msg.at('xxxxxx')
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || content | str | 必填 | 要发送的内容 || quote | bool | False | 是否引用该消息 |
+
+### ✨add_friend
+
+添加该消息的发送人为好友
+
+```python
+msg.add_friend()
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || addmsg | str | None | 添加好友时的附加消息,默认为None || remark | str | None | 添加好友后的备注,默认为None || tags | list | None | 添加好友后的标签,默认为None || permission | Literal[‘朋友圈’, ‘仅聊天’] | ‘朋友圈’ | 添加好友后的权限,默认为’朋友圈’ || timeout | int | 3 | 搜索好友的超时时间,默认为3秒 |
+
+### ✨multi_select
+
+多选该消息,仅作合并转发使用,如果不进行合并转发,请勿调用该方法
+
+```python
+msg.multi_select()
+```
+
+返回值:无
+
+### SelfMessage
+
+自己发送的消息,即聊天页面中,右侧自己发送的消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || attr | str | self | 消息属性 |
+
+### TextMessage
+
+文本消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | text | 消息属性 |
+
+### QuoteMessage
+
+引用消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | quote | 消息属性 |
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || quote_content | str | 引用消息内容 | 引用消息内容 |
+
+### ✨download_quote_image
+
+下载引用消息中的图片,返回图片路径
+
+```python
+msg.download_quote_image()
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || dir_path | str | None | 下载路径,默认为None || timeout | int | 10 | 超时时间,默认为10秒 |
+
+返回值: Path: 视频路径,成功时返回该类型
+
+### ✨click_quote
+
+点击引用框体
+
+```python
+msg.click_quote()
+```
+
+### ImageMessage
+
+图片消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | image | 消息属性 |
+
+### download
+
+下载图片,返回图片路径
+
+```python
+msg.download()
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || dir_path | Union[str, Path] | None | 下载图片的目录,不填则默认WxParam.DEFAULT_SAVE_PATH || timeout | int | 10 | 下载超时时间 |
+
+* Path: 图片路径,成功时返回该类型
+* WxResponse: 下载结果,失败时返回该类型
+
+### VideoMessage
+
+视频消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | video | 消息属性 |
+
+下载视频,返回视频路径
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || dir_path | Union[str, Path] | None | 下载视频的目录,不填则默认WxParam.DEFAULT_SAVE_PATH || timeout | int | 10 | 下载超时时间 |
+
+* Path: 视频路径,成功时返回该类型
+
+### VoiceMessage
+
+### to_text
+
+将语音消息转换为文本,返回文本内容
+
+```python
+msg.to_text()
+```
+
+### FileMessage
+
+文件消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | file | 消息属性 |
+
+下载文件,返回文件路径
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || dir_path | Union[str, Path] | None | 下载文件的目录,不填则默认WxParam.DEFAULT_SAVE_PATH || timeout | int | 10 | 下载超时时间 |
+
+* Path: 文件路径,成功时返回该类型
+
+### ✨LocationMessage
+
+位置消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | location | 消息属性 |
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || ✨address | str | 地址信息 | 该消息卡片的地址信息 |
+
+### ✨LinkMessage
+
+链接消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | link | 消息属性 |
+
+### ✨get_url
+
+获取链接地址
+
+```python
+msg.get_url()
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || timeout | int | 10 | 下载超时时间 |
+
+* str: 链接地址
+
+### ✨EmotionMessage
+
+表情消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | emotion | 消息属性 |
+
+### ✨MergeMessage
+
+合并消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | merge | 消息属性 |
+
+### ✨get_messages
+
+获取合并消息中的所有消息
+
+```python
+msg.get_messages()
+```
+
+* List[str]: 合并消息中的所有消息
+
+### ✨PersonalCardMessage
+
+名片消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | personal_card | 消息属性 |
+
+添加好友
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || addmsg | str | None | 添加好友时的附加消息 || remark | str | None | 添加好友后的备注 || tags | List[str] | None | 添加好友后的标签 || permission | Literal[‘朋友圈’, ‘仅聊天’] | ‘朋友圈’ | 添加好友后的权限 || timeout | int | 3 | 搜索好友的超时时间 |
+
+* WxResponse: 是否添加成功
+
+### ✨NoteMessage
+
+笔记消息。继承自HumanMessage
+
+| 属性名 | 类型 | 属性值 | 描述 || --- | --- | --- | --- | --- | --- || type | str | note | 消息属性 |
+
+### ✨get_content
+
+获取笔记内容
+
+```python
+from pathlib import Path
+
+note_content_list = msg.get_content()
+for content in note_content_list:
+ if isintance(content, str):
+ # 文本内容
+ print(content)
+ elif isintance(content, Path):
+ # 文件路径,文件、视频、图片等
+ print('文件路径:', content)
+```
+
+### ✨save_files
+
+保存笔记中的文件
+
+```python
+msg.save_files()
+```
+
+| 参数名 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || dir_path | Union[str, Path] | None | 保存路径 |
+
+* WxResponse: 是否保存成功,若成功则data为保存的文件路径列表
+
+### ✨to_markdown
+
+将笔记转换为Markdown格式
+
+```python
+msg.to_markdown()
+```
+
+* Path: markdown文件路径
+
+### OtherMessage
+
+其他暂未支持解析的消息类型
+
+## WeChat类
+
+## WeChat 类方法
+
+### 概念
+
+为确保您可以理解该文档的一些内容,这里先简单介绍一下 wxauto(x) 的设计思路,如下图所示,wxauto(x) 将微信窗口拆解为三部分:
+
+* 导航栏(NavigationBox):下图蓝色框内部分
+* 会话列表(SessionBox):下图绿色框内部分会话列表项(SessionElement):会话列表中每一个会话的元素,如好友、群聊、公众号等
+* 会话列表项(SessionElement):会话列表中每一个会话的元素,如好友、群聊、公众号等
+* 聊天框(Chat):下图红色框内部分
+
+```python
+from wxautox import WeChat
+
+wx = WeChat()
+```
+
+### 保持程序运行 KeepRunning
+
+由于wxautox使用守护线程来监听消息,当程序仅用于监听模式时,主线程会退出,因此需要调用此方法来保持程序运行
+
+```python
+from wxautox import WeChat
+
+wx = WeChat()
+wx.AddListenChat('张三', callback=lambda msg, chat: ...)
+
+# 保持程序运行,确保正常监听
+wx.KeepRunning()
+```
+
+### 获取当前会话列表 GetSession
+
+```python
+sessions = wx.GetSession()
+for session in sessions:
+ print(session.info)
+```
+
+* 类型:List[SessionElement]
+* 描述:当前会话列表
+
+### ✨发送链接卡片 SendUrlCard
+
+```python
+wx.SendUrlCard(url="https://example.com", friends="张三", timeout=10)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || url | str | 必填 | 链接地址 || friends | Union[str, List[str]] | None | 发送对象,可以是单个用户名或用户名列表 || timeout | int | 10 | 等待时间(秒) |
+
+* 描述:发送结果
+
+### 打开聊天窗口 ChatWith
+
+```python
+wx.ChatWith(who="张三", exact=False)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || who | str | 必填 | 要聊天的对象 || exact | bool | False | 搜索好友时是否精确匹配 |
+
+### 获取子窗口实例 GetSubWindow
+
+```python
+chat = wx.GetSubWindow(nickname="张三")
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || nickname | str | 必填 | 要获取的子窗口的昵称 |
+
+* 类型:Chat
+* 描述:子窗口实例
+
+### 获取所有子窗口实例 GetAllSubWindow
+
+```python
+chats = wx.GetAllSubWindow()
+```
+
+* 类型:List[Chat]
+* 描述:所有子窗口实例的列表
+
+### 添加监听聊天窗口 AddListenChat
+
+```python
+def on_message(msg, chat):
+ print(f"收到来自 {chat} 的消息: {msg.content}")
+
+wx.AddListenChat(nickname="张三", callback=on_message)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || nickname | str | 必填 | 要监听的聊天对象 || callback | Callable[[Message, Chat], None] | 必填 | 回调函数,参数为(Message对象, Chat对象) |
+
+* 成功时:类型:Chat描述:该监的听子窗口实例
+* 描述:该监的听子窗口实例
+* 失败时:类型:WxResponse描述:执行结果,成功时包含监听名称
+* 描述:执行结果,成功时包含监听名称
+
+成功时:
+
+失败时:
+
+### 移除监听聊天 RemoveListenChat
+
+```python
+wx.RemoveListenChat(nickname="张三")
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || nickname | str | 必填 | 要移除的监听聊天对象 |
+
+* 描述:执行结果
+
+### 开始监听 StartListening
+
+```python
+wx.StartListening()
+```
+
+### 停止监听 StopListening
+
+```python
+wx.StopListening()
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || remove | bool | True | 是否移出所有子窗口 |
+
+### ✨进入朋友圈 Moments
+
+```python
+moments = wx.Moments(timeout=3)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || timeout | int | 3 | 等待时间(秒) |
+
+* 类型:MomentsWnd
+* 描述:朋友圈窗口实例
+
+### 获取下一个新消息 GetNextNewMessage
+
+```python
+messages = wx.GetNextNewMessage(filter_mute=False)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || filter_mute | bool | False | 是否过滤掉免打扰消息 |
+
+* 类型:Dict[str, List[Message]
+* 描述:消息列表,键为聊天名称,值为消息列表
+* 示例:{'chat_name': 'wxauto交流', 'chat_type': 'group', 'msg': [ , , , , ... ] }
+
+```python
+{'chat_name': 'wxauto交流',
+ 'chat_type': 'group',
+ 'msg': [
+ ,
+ ,
+ ,
+ ,
+ ...
+ ]
+}
+```
+
+### ✨获取好友列表 GetFriendDetails
+
+```python
+# 获取前10个好友详情信息
+messages = wx.GetFriendDetails(n=10)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || n | int | None | 获取前n个好友详情信息 || tag | str | None | 从指定拼音首字母开始 || timeout | int | 0xFFFFF | 获取超时时间(秒) |
+
+* 类型:List[dict]
+* 描述:好友详情信息列表
+
+Warning
+
+* 该方法运行时间较长,约0.5~1秒一个好友的速度,好友多的话可将n设置为一个较小的值,先测试一下
+* 如果遇到企业微信的好友且为已离职状态,可能导致微信卡死,需重启(此为微信客户端BUG)
+* 该方法未经过大量测试,可能存在未知问题,如有问题请微信群内反馈
+
+### ✨获取新的好友申请列表 GetNewFriends
+
+```python
+newfriends = wx.GetNewFriends(acceptable=True)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || acceptable | bool | True | 是否过滤掉已接受的好友申请 |
+
+* 类型:List[NewFriendElement]
+* 描述:新的好友申请列表
+
+```python
+newfriends = wx.GetNewFriends(acceptable=True)
+tags = ['标签1', '标签2']
+for friend in newfriends:
+ remark = f'备注{friend.name}'
+ friend.accept(remark=remark, tags=tags) # 接受好友请求,并设置备注和标签
+```
+
+### ✨添加新的好友 AddNewFriend
+
+```python
+wx.AddNewFriend(keywords="张三", addmsg="我是小明", remark="老张", tags=["同学"], permission="朋友圈", timeout=5)
+```
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || keywords | str | 必填 | 搜索关键词,可以是昵称、微信号、手机号等 || addmsg | str | None | 添加好友时的附加消息 || remark | str | None | 添加好友后的备注 || tags | List[str] | None | 添加好友后的标签 || permission | Literal[‘朋友圈’, ‘仅聊天’] | ‘朋友圈’ | 添加好友后的权限 || timeout | int | 5 | 搜索好友的超时时间(秒) |
+
+* 描述:添加好友的结果
+
+### ✨获取最近群聊名称列表 GetAllRecentGroups
+
+```python
+groups = wx.GetAllRecentGroups()
+if groups:
+ print(groups)
+else:
+ print('获取失败')
+```
+
+* 类型:WxResponse | List[str]: 失败时返回WxResponse,成功时返回所有最近群聊列表
+
+### 切换到聊天页面 SwitchToChat
+
+```python
+wx.SwitchToChat()
+```
+
+### 切换到联系人页面 SwitchToContact
+
+```python
+wx.SwitchToContact()
+```
+
+### ✨是否在线 IsOnline
+
+```python
+wx.IsOnline()
+```
+
+* 类型:bool
+
+### ✨获取我的信息 GetMyInfo
+
+获取自己的微信号等信息
+
+```python
+wx.GetMyInfo()
+```
+
+### ✨获取通讯录群聊列表 GetContactGroups
+
+获取通讯录中的群聊列表
+
+```python
+wx.GetContactGroups()
+```
+
+Note
+
+自动化操作个体差异较大,根据实际情况调整以下参数,速度不合适可能导致漏掉部分群聊
+
+| 参数 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || speed | int | 1 | 滚动速度 || interval | float | 0.1 | 滚动时间间隔 |
+
+## ✨朋友圈类
+
+## MomentsWnd
+
+朋友圈窗口对象,即的是朋友圈的窗口对象,提供对朋友圈窗口的各种操作,如获取朋友圈内容、刷新、关闭等功能。
+
+```python
+from wxautox import WeChat
+
+wx = WeChat()
+pyq = wx.Moments() # 打开朋友圈并获取朋友圈窗口对象(如果为None则说明你没开启朋友圈,需要在手机端设置)
+```
+
+### GetMoments
+
+获取朋友圈内容
+
+```python
+# 获取当前页面的朋友圈内容
+moments = pyq.GetMoments()
+
+# 通过`next_page`参数获取下一页的朋友圈内容
+moments = pyq.GetMoments(next_page=True)
+```
+
+| 参数 | 类型 | 说明 | 说明 || --- | --- | --- | --- | --- | --- || next_page | bool | False | 是否翻页后再获取 || speed1 | int | 3 | 翻页时的滚动速度,根据自己的情况进行调整,建议3-10自行调整 || speed2 | int | 1 | 翻页最后时的速度,避免翻页过多导致遗漏所以一般比speed1慢,建议1-3 |
+
+返回值:List[Moments]
+
+### Refresh
+
+刷新朋友圈
+
+```python
+pyq.Refresh()
+```
+
+### close
+
+关闭朋友圈
+
+```python
+pyq.close()
+```
+
+## Moments
+
+朋友圈内容对象,即的是朋友圈的内容对象,提供对朋友圈的各种操作,如获取朋友圈内容、点赞、评论等功能。
+
+```python
+# 获取朋友圈对象
+moments = pyq.GetMoments()
+
+# 获取第一条朋友圈
+moment = moments[0]
+```
+
+### 获取朋友圈内容
+
+```python
+# 获取朋友圈内容
+info = moment.info
+# {
+# 'type': 'moment', # 类型,分为`朋友圈`和`广告`
+# 'id': '4236572776458165', # ID
+# 'sender': '天天鲜花2号客服', # 发送者
+# 'content': '客订花束', # 内容,就是朋友圈的文字内容,如果没有文字内容则为空字符串
+# 'time': '4分钟前', # 发送时间
+# 'img_count': 3, # 图片数量
+# 'comments': [], # 评论
+# 'addr': '', # 发送位置
+# 'likes': [] # 点赞
+# }
+
+moment.sender
+# '天天鲜花2号客服'
+
+moment.content
+# '客订花束'
+
+moment.time
+# '4分钟前'
+
+# info中所有的键值对都可以通过对象的属性来获取,就不一一列举了
+...
+```
+
+### SaveImages
+
+保存朋友圈图片
+
+| 参数 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || save_index | int | list | None | 保存图片的索引,可以是一个整数或者一个列表,如果为None则保存所有图片 || savepath | str | None | 绝对路径,包括文件名和后缀,例如:“D:/Images/微信图片_xxxxxx.jpg”,如果为None则保存到默认路径 |
+
+返回值:List[str],保存的图片的绝对路径列表
+
+```python
+# 获取朋友圈图片
+images = moment.SaveImages()
+# [
+# 'D:/Images/微信图片_xxxxxx1.jpg',
+# 'D:/Images/微信图片_xxxxxx2.jpg',
+# 'D:/Images/微信图片_xxxxxx3.jpg',
+# ...
+# ]
+```
+
+### Like
+
+点赞朋友圈
+
+| 参数 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || like | bool | True | True点赞,False取消赞 |
+
+```python
+# 点赞
+moment.Like()
+# 取消赞
+moment.Like(False)
+```
+
+### Comment
+
+评论朋友圈
+
+| 参数 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || text | str | 必填 | 评论内容 |
+
+```python
+# 评论
+moment.Comment('评论内容')
+```
+
+## 其他类
+
+该类用于该项目的一些参数,在获取WeChat实例前,可以通过修改该类的属性来修改默认参数
+
+| 属性 | 类型 | 默认值 | 描述 || --- | --- | --- | --- | --- | --- || ENABLE_FILE_LOGGER | bool | True | 是否启用日志文件 || DEFAULT_SAVE_PATH | str | ./wxautox | 下载文件/图片默认保存路径 || ✨MESSAGE_HASH | bool | False | 是否启用消息哈希值用于辅助判断消息,开启后会稍微影响性能 || DEFAULT_MESSAGE_XBIAS | int | 51 | 头像到消息X偏移量,用于消息定位,点击消息等操作 || FORCE_MESSAGE_XBIAS | bool | False | 是否强制重新自动获取X偏移量,如果设置为True,则每次启动都会重新获取,系统设置了分辨率缩放时开启 || LISTEN_INTERVAL | int | 1 | 监听消息时间间隔,单位秒 || ✨LISTENER_EXCUTOR_WORKERS | int | 4 | 监听执行器线程池大小,根据自身需求和设备性能设置 || SEARCH_CHAT_TIMEOUT | int | 5 | 搜索聊天对象超时时间,单位秒 || ✨NOTE_LOAD_TIMEOUT | int | 30 | 微信笔记加载超时时间,单位秒 |
+
+```python
+from wxautox import WxParam
+
+# 设置8个监听线程
+WxParam.LISTENER_EXCUTOR_WORKERS = 8
+...
+```
+
+### SessionElement
+
+| 属性 | 类型 | 描述(以上图为例) || --- | --- | --- | --- | --- || name | str | 会话名(wxauto三群) || time | str | 时间(2025-05-14 14:41) || content | str | 消息内容([10条]天道酬勤:这..) || ismute | bool | 是否消息免打扰(True) || isnew | bool | 是否有新消息(True) || new_count | int | 新消息数量(10) || info | Dict[str, Any] | 会话信息(包含了上述所有属性的dict) |
+
+```python
+from wxauto import WeChat
+
+wx = WeChat()
+sessions = wx.GetSession()
+session = sessions[0] # 获取第一个会话
+```
+
+点击会话,即切换到这个聊天窗口
+
+```python
+session.click()
+```
+
+#### double_click
+
+双击会话,即将这个聊天窗口独立出去
+
+```python
+session.double_click()
+```
+
+删除会话
+
+返回值:WxResponse
+
+```python
+session.delete()
+```
+
+#### ✨hide
+
+隐藏会话
+
+```python
+session.hide()
+```
+
+#### ✨select_option
+
+选择会话选项,即右键点击会话,然后选择某个选项
+
+| 参数名 | 类型 | 说明 || --- | --- | --- | --- | --- || option | str | 选项名称,例如“置顶”、“标为未读”等 |
+
+### NewFriendElement
+
+| 属性 | 类型 | 描述(以上图为例) || --- | --- | --- | --- | --- || name | str | 对方名(诸葛孔明) || msg | str | 申请信息(wxautox) || acceptable | bool | 是否可接受(True) |
+
+#### accept
+
+同意添加好友
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || remark | str | None | 备注 || tags | list | None | 标签 || permission | str | ‘朋友圈’ | 朋友圈权限,可选值:‘全部’、‘仅聊天’ |
+
+删除好友申请
+
+#### ✨reply
+
+回复好友申请
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || text | str | 必填 | 回复信息 |
+
+#### ✨get_account
+
+获取申请添加的好友的账号信息
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || wait | int | 5 | 等待时间 |
+
+返回值:str
+
+### ✨LoginWnd
+
+该类用于微信登录、获取二维码等操作
+
+```python
+from wxautox import LoginWnd
+
+wxlogin = LoginWnd(app_path="...")
+```
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || app_path | str | None | 微信客户端路径 |
+
+属性:无
+
+#### login
+
+登录微信
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || timeout | int | 10 | 登录超时时间 |
+
+#### get_qrcode
+
+获取二维码
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || path | str | None | 二维码图片的保存路径,None即本地目录下的wxauto_qrcode文件夹 |
+
+返回值:str,二维码图片的路径
+
+#### reopen
+
+重新打开微信,为了避免各种弹窗影响操作,建议调用该方法后再执行login或get_qrcode
+
+#### open
+
+启动微信,建议在初始化的时候传入app_path参数,否则可能会启动失败
+
+### WeChatImage
+
+```python
+from wxautox.ui.component import WeChatImage
+
+imgwnd = WeChatImage()
+```
+
+微信图片/视频窗口类,用于处理微信图片或图片窗口的各种操作
+
+#### ocr
+
+识别图片中的文字,仅支持图片,不支持视频
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || wait | int | 10 | 隐性等待时间 |
+
+返回值:str,识别到的文字
+
+#### save
+
+保存图片/视频
+
+| 参数名 | 类型 | 默认值 | 说明 || --- | --- | --- | --- | --- | --- || dir_path | str | None | 保存的目录路径,None即本地路径下自动生成 || timeout | int | 10 | 保存超时时间 |
+
+返回值:Path,保存的文件路径
+
+关闭图片/视频窗口
+
+### ✨WeChatDialog
+
+微信对话框对象,用于处理微信对话框的各种操作
+
+选择对话框中的选项,如“确定”、“取消”等
+
+返回值:WxResponse对象
+
+关闭对话框
+
diff --git a/doc/wxauto_center/API.md b/doc/wxauto_center/API.md
new file mode 100644
index 0000000..5e45d4b
--- /dev/null
+++ b/doc/wxauto_center/API.md
@@ -0,0 +1,344 @@
+# ///
+# API.md
+# 描述:WXAuto Center API 接口文档
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# 更新日期:2026-04-06
+# ///
+
+# WXAuto Center API 接口文档
+
+## 概述
+
+本系统提供完整的 RESTful API,支持节点管理、消息发送、插件管理、外部程序接入、Webhook 管理等功能。所有 API 均使用 JSON 格式进行请求和响应。
+
+## 基础信息
+
+| 项目 | 说明 |
+|------|------|
+| Base URL | `http://{host}:{port}/api/v1` |
+| 默认端口 | 8080 |
+| 内部 API 认证 | Header: `X-API-Key: your-external-api-key` |
+| 外部 API 认证 | Header: `X-ExternalKey: your-external-api-key` |
+| 响应格式 | JSON |
+
+## API 列表
+
+### 节点管理 `/api/v1/nodes`
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/nodes/` | 获取所有节点 |
+| GET | `/nodes/{node_id}` | 获取指定节点信息 |
+| POST | `/nodes/` | 添加节点 |
+| PUT | `/nodes/{node_id}` | 更新节点信息 |
+| DELETE | `/nodes/{node_id}` | 删除节点 |
+| GET | `/nodes/{node_id}/status` | 检查节点状态 |
+| POST | `/nodes/{node_id}/enable` | 启用节点 |
+| POST | `/nodes/{node_id}/disable` | 禁用节点 |
+| GET | `/nodes/{node_id}/friends` | 获取节点好友列表 |
+| GET | `/nodes/{node_id}/groups` | 获取节点群列表 |
+| GET | `/nodes/{node_id}/messages/{chat_name}` | 获取聊天消息 |
+
+### 消息管理 `/api/v1/messages`
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/messages/send` | 发送消息 |
+| POST | `/messages/send_group` | 发送群消息 |
+| POST | `/messages/batch` | 批量发送消息 |
+| POST | `/messages/template/{node_id}/{who}` | 发送模板消息 |
+| GET | `/messages/history/{node_id}/{chat_name}` | 获取消息历史 |
+
+### 插件管理 `/api/v1/plugins`
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/plugins/` | 获取所有插件 |
+| GET | `/plugins/{plugin_name}` | 获取插件详情 |
+| GET | `/plugins/{plugin_name}/schema` | 获取插件表单Schema |
+| POST | `/plugins/{plugin_name}/enable` | 启用插件 |
+| POST | `/plugins/{plugin_name}/disable` | 禁用插件 |
+| PUT | `/plugins/{plugin_name}/config` | 更新插件配置 |
+| POST | `/plugins/{plugin_name}/execute` | 执行插件 |
+| GET | `/plugins/{plugin_name}/logs` | 获取插件日志 |
+| POST | `/plugins/{plugin_name}/logs/clear` | 清空插件日志 |
+| GET | `/plugins/type/{plugin_type}` | 按类型获取插件 |
+
+### 回调接口 `/api/v1/callback`
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/callback/node/reset` | 重置节点微信状态 |
+| POST | `/callback/node/check` | 检测节点微信状态 |
+| GET | `/callback/node/{node_id}/status` | 获取节点回调状态 |
+
+### Webhook 管理 `/api/v1/webhook`
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/webhook/` | 获取所有Webhook |
+| GET | `/webhook/{webhook_id}` | 获取指定Webhook |
+| POST | `/webhook/` | 添加Webhook |
+| PUT | `/webhook/{webhook_id}` | 更新Webhook |
+| DELETE | `/webhook/{webhook_id}` | 删除Webhook |
+| POST | `/webhook/{webhook_id}/enable` | 启用Webhook |
+| POST | `/webhook/{webhook_id}/disable` | 禁用Webhook |
+
+### 外部接口 `/api/v1/external`
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/external/send` | 外部接口-发送消息 |
+| POST | `/external/trigger` | 外部接口-触发数据处理 |
+| POST | `/external/plugin/execute` | 外部接口-执行插件 |
+| GET | `/external/plugins` | 外部接口-获取可用插件列表 |
+| GET | `/external/nodes` | 外部接口-获取可用节点 |
+| POST | `/external/ai/process` | 外部接口-AI处理消息 |
+| POST | `/external/webhook/{event}` | 外部接口-Webhook接收 |
+
+---
+
+## 节点管理 API 详解
+
+### 获取所有节点
+
+```
+GET /api/v1/nodes/
+```
+
+**响应示例:**
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.20.84:5000",
+ "enabled": true,
+ "description": "",
+ "group": "wechat",
+ "status": "error",
+ "wechat_status": "online",
+ "is_healthy": false
+ }
+ ],
+ "count": 1
+}
+```
+
+### 添加节点
+
+```
+POST /api/v1/nodes/
+```
+
+**请求体:**
+```json
+{
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.20.84:5000",
+ "api_key": "your-node-api-key",
+ "description": "测试节点",
+ "group": "wechat"
+}
+```
+
+### 更新节点
+
+```
+PUT /api/v1/nodes/{node_id}
+```
+
+**请求体:**
+```json
+{
+ "api_url": "http://192.168.20.84:5000",
+ "api_key": "updated-key"
+}
+```
+
+---
+
+## 消息发送 API 详解
+
+### 发送消息
+
+```
+POST /api/v1/messages/send
+```
+
+**请求体:**
+```json
+{
+ "node_id": "wx1",
+ "who": "文件传输助手",
+ "msg": "测试消息",
+ "msg_type": "text"
+}
+```
+
+**响应示例:**
+```json
+{
+ "success": true,
+ "message": "消息发送成功"
+}
+```
+
+### 发送群消息
+
+```
+POST /api/v1/messages/send_group
+```
+
+**请求体:**
+```json
+{
+ "node_id": "wx1",
+ "group_name": "测试群",
+ "msg": "群消息测试",
+ "msg_type": "text"
+}
+```
+
+---
+
+## 插件管理 API 详解
+
+### 获取所有插件
+
+```
+GET /api/v1/plugins/
+```
+
+**响应示例:**
+```json
+{
+ "success": true,
+ "plugins": [
+ {
+ "name": "http_scheduled_sender",
+ "version": "1.0.0",
+ "type": "scheduled_task",
+ "description": "定时HTTP数据源推送",
+ "enabled": true,
+ "config": {
+ "node_id": "wx1",
+ "receiver": "asq",
+ "cron": "0 8 * * *"
+ }
+ }
+ ],
+ "count": 1
+}
+```
+
+### 配置插件
+
+```
+PUT /api/v1/plugins/{plugin_name}/config
+```
+
+**请求体:**
+```json
+{
+ "name": "http_scheduled_sender",
+ "config": {
+ "node_id": "wx1",
+ "receiver": "文件传输助手",
+ "cron": "0 8 * * *",
+ "http_url": "https://api.example.com/news",
+ "message_template": "📰 {title}\n{url}"
+ }
+}
+```
+
+### 执行插件
+
+```
+POST /api/v1/plugins/{plugin_name}/execute
+```
+
+**请求体:**
+```json
+{
+ "action": "preview"
+}
+```
+
+**action 选项:**
+- `send` - 执行完整流程
+- `fetch` - 仅请求API
+- `parse` - 请求并解析数据
+- `preview` - 预览格式化后的消息
+- `test` - 测试cron表达式
+
+---
+
+## 通用响应格式
+
+**成功:**
+```json
+{
+ "success": true,
+ "data": {},
+ "message": "操作成功"
+}
+```
+
+**失败:**
+```json
+{
+ "success": false,
+ "error": "错误描述"
+}
+```
+
+---
+
+## 插件类型
+
+| 类型 | 值 | 说明 |
+|------|-----|------|
+| `message_handler` | MESSAGE_HANDLER | 消息处理器 |
+| `data_source` | DATA_SOURCE | 数据源 |
+| `action_trigger` | ACTION_TRIGGER | 动作触发器 |
+| `ai_agent` | AI_AGENT | AI智能体 |
+| `scheduled_task` | SCHEDULED_TASK | 定时任务 |
+| `http_scheduled_sender` | HTTP_SCHEDULED_SENDER | 定时HTTP推送 |
+| `custom` | CUSTOM | 自定义 |
+
+---
+
+## 节点数据模型
+
+```json
+{
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.20.84:5000",
+ "api_key": "your-key",
+ "enabled": true,
+ "description": "",
+ "group": "wechat",
+ "status": "active",
+ "wechat_status": "online",
+ "is_healthy": true
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| node_id | string | 节点唯一标识 |
+| name | string | 节点显示名称 |
+| api_url | string | 节点API地址 |
+| api_key | string | 节点API密钥 |
+| enabled | bool | 是否启用 |
+| description | string | 节点描述 |
+| group | string | 节点分组 |
+| status | string | API状态 (active/inactive/error) |
+| wechat_status | string | 微信状态 (online/offline) |
+| is_healthy | bool | 健康状态 |
diff --git a/doc/wxauto_center/ARCHITECTURE.md b/doc/wxauto_center/ARCHITECTURE.md
new file mode 100644
index 0000000..5630e6a
--- /dev/null
+++ b/doc/wxauto_center/ARCHITECTURE.md
@@ -0,0 +1,235 @@
+# ///
+# ARCHITECTURE.md
+# 描述:WXAuto Center 系统架构文档
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# 更新日期:2026-04-06
+# ///
+
+# WXAuto Center 系统架构文档
+
+## 概述
+
+WXAuto Center 是一个模块化的微信多节点中控系统,采用前后端分离架构,支持多节点管理、消息发送、插件扩展、定时任务等功能。
+
+## 系统架构图
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ WXAuto Center │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ Frontend (Static) │ │
+│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
+│ │ │Dashboard │ │ Nodes │ │ Messages │ │ Plugins │ │ │
+│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ FastAPI Backend │ │
+│ │ │ │
+│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
+│ │ │nodes router │ │messages router│ │plugins router│ │webhook router│ │ │
+│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
+│ │ │ │
+│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
+│ │ │callback router│ │external router│ │webhook service│ │queue service│ │ │
+│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │ │
+│ ┌────────────────────────────┼────────────────────────────┐ │
+│ ▼ ▼ ▼ │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ Redis │ │ PostgreSQL │ │ Nodes │ │
+│ │ (Queue) │ │ (Storage) │ │ (External) │ │
+│ └─────────────┘ └─────────────┘ └─────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 核心组件
+
+### 1. 前端 (Frontend)
+
+静态文件服务,基于原生 JavaScript 开发。
+
+**页面模块:**
+| 页面 | 文件 | 说明 |
+|------|------|------|
+| 首页/仪表盘 | app.js | 系统概览 |
+| 节点管理 | app.js | 节点 CRUD |
+| 快捷消息 | app.js | 快速发送消息 |
+| 监控告警 | app.js | Webhook 配置 |
+| 日志查看 | app.js | 系统日志 |
+| 外部接入 | app.js | 外部 API 文档 |
+| 插件管理 | app.js | 插件配置 |
+
+### 2. 后端 (Backend)
+
+基于 FastAPI 的 RESTful API 服务。
+
+**API 模块:**
+| 模块 | 路径 | 说明 |
+|------|------|------|
+| nodes | /api/v1/nodes | 节点管理 |
+| messages | /api/v1/messages | 消息发送 |
+| plugins | /api/v1/plugins | 插件管理 |
+| webhook | /api/v1/webhook | Webhook 管理 |
+| callback | /api/v1/callback | 回调接口 |
+| external | /api/v1/external | 外部接口 |
+
+### 3. 服务层 (Services)
+
+| 服务 | 说明 |
+|------|------|
+| node_manager | 节点管理器,内存缓存 |
+| monitor_service | 节点健康监控服务 |
+| scheduler_service | 定时任务调度服务 |
+| log_service | 日志服务 |
+| queue_service | 消息队列服务 |
+| webhook_service | Webhook 通知服务 |
+
+### 4. 插件系统 (Plugins)
+
+| 插件 | 类型 | 说明 |
+|------|------|------|
+| http_data_source | DATA_SOURCE | HTTP 数据源 |
+| database_query | DATA_SOURCE | 数据库查询 |
+| webhook_trigger | ACTION_TRIGGER | Webhook 触发 |
+| simple_ai_agent | AI_AGENT | AI 智能体 |
+| http_scheduled_sender | SCHEDULED_TASK | 定时 HTTP 推送 |
+
+### 5. 数据层 (Data)
+
+| 组件 | 说明 |
+|------|------|
+| PostgreSQL | 主数据库,存储节点、Webhook、日志、插件配置 |
+| Redis | 消息队列、缓存 |
+
+---
+
+## 目录结构
+
+```
+wxauto_api/
+├── main.py # 应用入口
+├── config.py # 配置管理
+├── models.py # 数据模型
+├── database_service.py # 数据库服务
+├── requirements.txt # Python 依赖
+├── Dockerfile # Docker 镜像
+├── docker-compose.yml # Docker 编排
+│
+├── api/ # API 路由
+│ ├── __init__.py
+│ ├── nodes.py # 节点管理
+│ ├── messages.py # 消息发送
+│ ├── plugins.py # 插件管理
+│ ├── webhook.py # Webhook 管理
+│ ├── callback.py # 回调接口
+│ └── external.py # 外部接口
+│
+├── services/ # 业务服务
+│ ├── __init__.py
+│ ├── node_manager.py # 节点管理
+│ ├── monitor_service.py # 健康监控
+│ ├── scheduler_service.py # 定时调度
+│ ├── log_service.py # 日志服务
+│ ├── queue_service.py # 队列服务
+│ └── webhook_service.py # Webhook 服务
+│
+├── plugins/ # 插件系统
+│ ├── __init__.py
+│ ├── base.py # 插件基类
+│ ├── builtin.py # 内置插件
+│ └── http_scheduled_sender.py # 定时推送插件
+│
+├── utils/ # 工具函数
+│ └── auth.py # 认证中间件
+│
+├── static/ # 静态文件
+│ ├── index.html # 主页面
+│ └── js/ # JavaScript
+│ ├── api.js # API 封装
+│ ├── app.js # 主应用
+│ └── pages/
+│ └── templates.js # 页面模板
+│
+└── doc/ # 文档
+ └── wxauto_center/
+ ├── ARCHITECTURE.md # 架构文档
+ ├── API.md # API 文档
+ ├── DATABASE.md # 数据库文档
+ ├── FLOWS.md # 流程文档
+ └── PLUGIN.md # 插件文档
+```
+
+---
+
+## 技术栈
+
+| 层级 | 技术 | 说明 |
+|------|------|------|
+| 前端 | HTML/CSS/JavaScript | 原生开发,无框架依赖 |
+| 后端 | Python 3.11 + FastAPI | 高性能异步框架 |
+| 数据库 | PostgreSQL | 关系型数据库 |
+| 缓存/队列 | Redis | 内存数据库、消息队列 |
+| 容器 | Docker + Docker Compose | 容器化部署 |
+| ORM | SQLAlchemy | Python ORM |
+| HTTP 客户端 | httpx | 异步 HTTP 客户端 |
+
+---
+
+## 部署架构
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Docker Network │
+│ │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ wxauto-center│ │ wxauto-redis │ │wxauto-postgres│ │
+│ │ :8080 │ │ :6379 │ │ :5432 │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+ │ │
+ ▼ ▼
+ ┌──────────────────┐ ┌──────────────────┐
+ │ External Nodes │ │ External │
+ │ (wxauto nodes) │ │ Webhook URLs │
+ │ :5000 │ │ │
+ └──────────────────┘ └──────────────────┘
+```
+
+### 环境变量
+
+| 变量 | 说明 | 默认值 |
+|------|------|--------|
+| `POSTGRES_USER` | PostgreSQL 用户 | wxauto |
+| `POSTGRES_PASSWORD` | PostgreSQL 密码 | wxauto_password |
+| `POSTGRES_DB` | 数据库名 | wxauto |
+| `REDIS_PASSWORD` | Redis 密码 | wxauto_redis |
+| `API_KEY` | API 认证密钥 | your-external-api-key |
+| `EXTERNAL_KEY` | 外部接口密钥 | your-external-api-key |
+
+---
+
+## 安全机制
+
+### 1. API 认证
+- 内部 API:Header `X-API-Key`
+- 外部 API:Header `X-ExternalKey`
+
+### 2. 异常处理
+- 全局异常处理器
+- 所有 API 调用包裹 try/except
+- 10 秒 HTTP 超时
+
+### 3. 数据一致性
+- 数据库优先原则
+- 启动时从数据库加载配置
+- 操作时同时更新内存和数据库
diff --git a/doc/wxauto_center/CONFIG.md b/doc/wxauto_center/CONFIG.md
new file mode 100644
index 0000000..d087603
--- /dev/null
+++ b/doc/wxauto_center/CONFIG.md
@@ -0,0 +1,618 @@
+# ///
+# CONFIG.md
+# 描述:配置文件说明
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# 最后更新:2026-04-06
+# ///
+
+# WXAuto Center 配置文件说明
+
+## 概述
+
+系统配置支持多种配置源,按优先级从高到低依次为:
+1. 环境变量(`.env` 文件)
+2. 配置文件(`config.json`)
+3. 代码默认值
+
+## 配置文件结构
+
+### config.json
+
+主配置文件,仅包含应用配置。**节点配置和 Webhook 配置已迁移到数据库**。
+
+```json
+{
+ "app_name": "WXAuto Center",
+ "host": "0.0.0.0",
+ "port": 8080,
+ "debug": true,
+ "secret_key": "wxauto-center-secret-key-change-in-production",
+ "workers": 1,
+ "cors_origins": ["*"],
+ "api_prefix": "/api/v1",
+ "external_api_key": "your-external-api-key",
+ "monitor": {
+ "enabled": true,
+ "check_interval": 30
+ }
+}
+```
+
+### .env 环境变量文件
+
+Docker 环境变量配置文件。
+
+```
+# ///
+# .env
+# 描述:环境变量配置(用于 Docker 环境)
+# ///
+
+# 数据库配置
+DATABASE_URL=postgresql://wxauto:wxauto_password@postgres:5432/wxauto
+
+# Redis 配置
+REDIS_URL=redis://redis:6379/0
+
+# 应用配置
+APP_NAME=WXAuto Center
+HOST=0.0.0.0
+PORT=8080
+DEBUG=true
+SECRET_KEY=wxauto-center-secret-key-change-in-production
+WORKERS=1
+
+# API 配置
+API_PREFIX=/api/v1
+EXTERNAL_API_KEY=your-external-api-key
+CORS_ORIGINS=["*"]
+
+# 监控配置
+MONITOR_ENABLED=true
+MONITOR_CHECK_INTERVAL=30
+```
+
+---
+
+## 配置项详细说明
+
+### 应用配置
+
+#### app_name
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | string |
+| 默认值 | "WXAuto Center" |
+| 环境变量 | APP_NAME |
+| 说明 | 应用名称,显示在页面标题和日志中 |
+
+#### host
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | string |
+| 默认值 | "0.0.0.0" |
+| 环境变量 | HOST |
+| 说明 | 服务监听地址,0.0.0.0 表示监听所有网络接口 |
+
+#### port
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | int |
+| 默认值 | 8080 |
+| 环境变量 | PORT |
+| 说明 | 服务监听端口 |
+
+#### debug
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | bool |
+| 默认值 | false |
+| 环境变量 | DEBUG |
+| 说明 | 调试模式,启用时支持热重载 |
+
+#### secret_key
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | string |
+| 默认值 | "wxauto-center-secret-key-change-in-production" |
+| 环境变量 | SECRET_KEY |
+| 说明 | 应用密钥,用于会话加密等安全用途,生产环境必须修改 |
+
+---
+
+### API 配置
+
+#### api_prefix
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | string |
+| 默认值 | "/api/v1" |
+| 环境变量 | API_PREFIX |
+| 说明 | API 路由前缀,所有 API 路径都会此前缀开头 |
+
+#### external_api_key
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | string |
+| 默认值 | "your-external-api-key" |
+| 环境变量 | EXTERNAL_API_KEY |
+| 说明 | 外部 API 密钥,用于外部系统调用时的认证 |
+
+#### cors_origins
+
+| 属性 | 说明 |
+|------|------|
+| 类型 | array[string] |
+| 默认值 | ["*"] |
+| 环境变量 | CORS_ORIGINS |
+| 说明 | 允许的 CORS 来源,* 表示允许所有来源(仅限开发环境使用) |
+
+---
+
+## 节点配置 (NodeConfig)
+
+**重要更新**:节点配置已从 config.json 迁移到 PostgreSQL 数据库的 `nodes` 表。
+
+### 配置存储
+
+| 存储位置 | 说明 |
+|----------|------|
+| PostgreSQL | nodes 表(生产环境推荐) |
+| config.json | 已废弃,不再支持 |
+
+### 节点数据模型
+
+```json
+{
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.1.100:5000",
+ "api_key": "your-node-api-key",
+ "enabled": true,
+ "description": "主节点",
+ "group": "default",
+ "status": "active",
+ "wechat_status": "online",
+ "is_healthy": true
+}
+```
+
+### 节点标识说明
+
+每个节点有两个标识:
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| node_id | 节点唯一标识,用于 API 调用 | "wx1", "node_001" |
+| name | 节点显示名称,用于界面展示 | "微信1", "测试节点" |
+
+### 配置项说明
+
+| 配置项 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| node_id | string | 是 | - | 节点唯一标识(不可重复) |
+| name | string | 否 | 等于 node_id | 节点显示名称 |
+| api_url | string | 是 | - | 节点 API 服务地址 |
+| api_key | string | 是 | - | 节点 API 密钥 |
+| enabled | bool | 否 | true | 是否启用该节点 |
+| description | string | 否 | "" | 节点描述信息 |
+| group | string | 否 | "default" | 节点分组,用于分类管理 |
+
+### API 管理节点
+
+通过 API 管理节点配置:
+
+```bash
+# 添加节点
+curl -X POST http://localhost:8080/api/v1/nodes \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.1.100:5000",
+ "api_key": "your-node-api-key",
+ "description": "主节点",
+ "group": "production"
+ }'
+
+# 获取所有节点
+curl http://localhost:8080/api/v1/nodes \
+ -H "X-API-Key: your-external-api-key"
+
+# 更新节点
+curl -X PUT http://localhost:8080/api/v1/nodes/wx1 \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{"enabled": false}'
+
+# 删除节点
+curl -X DELETE http://localhost:8080/api/v1/nodes/wx1 \
+ -H "X-API-Key: your-external-api-key"
+```
+
+### 节点分组
+
+节点分组功能允许将节点按业务线或用途进行分类管理:
+
+```bash
+# 查询特定分组的节点
+curl "http://localhost:8080/api/v1/nodes?group=production" \
+ -H "X-API-Key: your-external-api-key"
+```
+
+---
+
+## Webhook 配置 (WebhookConfig)
+
+**重要更新**:Webhook 配置已从 config.json 迁移到 PostgreSQL 数据库的 `webhook_urls` 表。
+
+### 配置存储
+
+| 存储位置 | 说明 |
+|----------|------|
+| PostgreSQL | webhook_urls 表(推荐) |
+| config.json | 已废弃,不再支持 |
+
+### Webhook 数据模型
+
+```json
+{
+ "id": 1,
+ "name": "企业微信告警",
+ "url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx",
+ "format": "wechat",
+ "enabled": true,
+ "event_types": "wechat_offline,wechat_online,node_offline,node_online",
+ "created_at": "2026-04-06T10:00:00",
+ "updated_at": "2026-04-06T10:00:00"
+}
+```
+
+### 配置项说明
+
+| 配置项 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| name | string | 是 | - | Webhook 名称 |
+| url | string | 是 | - | Webhook URL 地址 |
+| format | string | 否 | "bark" | 消息格式:bark 或 wechat |
+| enabled | bool | 否 | true | 是否启用 |
+| event_types | string | 否 | 全部事件 | 需要触发 Webhook 的事件类型 |
+
+### 支持的事件类型
+
+| 事件类型 | 说明 | 触发时机 |
+|----------|------|----------|
+| api_error | API 错误 | 调用节点 API 失败 |
+| wechat_offline | 微信掉线 | 微信状态变为 offline |
+| wechat_online | 微信上线 | 微信状态变为 online/connected |
+| node_offline | 节点离线 | 节点 API 不可达 |
+| node_online | 节点在线 | 节点 API 恢复可用 |
+
+### 消息格式
+
+#### 企业微信格式 (format: "wechat")
+
+```json
+{
+ "msgtype": "text",
+ "text": {
+ "content": "【WXAuto Center 告警】\n事件类型: 微信掉线\n节点: wx1\n状态: offline\n时间: 2026-04-05 10:00:00"
+ }
+}
+```
+
+#### Bark 格式 (format: "bark")
+
+```json
+{
+ "title": "WXAuto告警 - 微信掉线",
+ "body": "wx1\n状态: offline\n时间: 2026-04-05 10:00:00"
+}
+```
+
+### API 管理 Webhook
+
+```bash
+# 添加 Webhook
+curl -X POST http://localhost:8080/api/v1/webhook \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{
+ "name": "企业微信告警",
+ "url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx",
+ "format": "wechat",
+ "enabled": true,
+ "event_types": "wechat_offline,wechat_online,node_offline,node_online"
+ }'
+
+# 获取所有 Webhook
+curl http://localhost:8080/api/v1/webhook \
+ -H "X-API-Key: your-external-api-key"
+
+# 更新 Webhook
+curl -X PUT http://localhost:8080/api/v1/webhook/1 \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{"enabled": false}'
+
+# 删除 Webhook
+curl -X DELETE http://localhost:8080/api/v1/webhook/1 \
+ -H "X-API-Key: your-external-api-key"
+
+# 启用/禁用 Webhook
+curl -X POST http://localhost:8080/api/v1/webhook/1/enable \
+ -H "X-API-Key: your-external-api-key"
+
+curl -X POST http://localhost:8080/api/v1/webhook/1/disable \
+ -H "X-API-Key: your-external-api-key"
+```
+
+---
+
+## 监控配置 (MonitorConfig)
+
+监控服务负责定时检测节点和微信状态。
+
+```json
+{
+ "monitor": {
+ "enabled": true,
+ "check_interval": 30
+ }
+}
+```
+
+### 配置项说明
+
+| 配置项 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| enabled | bool | 否 | true | 是否启用监控服务 |
+| check_interval | int | 否 | 30 | 检测间隔(秒) |
+
+### 检测逻辑
+
+1. **API 检测**:定期调用节点 `/health` 接口检查节点是否在线
+2. **微信检测**:定期调用 `/api/wechat/initialize` 检测微信连接状态
+3. **状态变化**:当检测到状态变化时,触发 Webhook 通知
+
+---
+
+## 数据库配置
+
+数据库配置通过环境变量设置:
+
+| 环境变量 | 说明 | 默认值 |
+|----------|------|--------|
+| DATABASE_URL | PostgreSQL 连接 URL | postgresql://wxauto:wxauto_password@postgres:5432/wxauto |
+
+### Docker 环境
+
+```bash
+DATABASE_URL=postgresql://wxauto:wxauto_password@postgres:5432/wxauto
+```
+
+### 本地环境
+
+```bash
+DATABASE_URL=postgresql://wxauto:wxauto_password@localhost:5432/wxauto
+```
+
+---
+
+## Redis 配置
+
+Redis 用于消息队列和缓存支持。
+
+| 环境变量 | 说明 | 默认值 |
+|----------|------|--------|
+| REDIS_URL | Redis 连接 URL | redis://redis:6379/0 |
+
+### Docker 环境
+
+```bash
+REDIS_URL=redis://redis:6379/0
+```
+
+### 本地环境
+
+```bash
+REDIS_URL=redis://localhost:6379/0
+```
+
+---
+
+## Workers 配置
+
+Workers 用于多进程部署,提高并发处理能力。
+
+```json
+{
+ "workers": 1
+}
+```
+
+### 配置项说明
+
+| 配置项 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| workers | int | 否 | 1 | 工作进程数,1 表示单进程模式 |
+
+### 启动方式
+
+| 模式 | workers | 说明 |
+|------|---------|------|
+| 单进程 | 1 | 默认模式,不启用多进程 |
+| 多进程 | >1 | 启用多进程模式,需要 Redis 支持 |
+
+**注意**:多进程模式需要 Redis 支持,队列服务依赖 Redis 进行进程间通信。
+
+### 启动命令
+
+```bash
+# 单进程
+python main.py
+
+# 多进程
+python main.py --workers 4
+```
+
+---
+
+## 完整配置示例
+
+### config.json
+
+```json
+{
+ "app_name": "WXAuto Center",
+ "host": "0.0.0.0",
+ "port": 8080,
+ "debug": false,
+ "secret_key": "your-production-secret-key",
+ "api_prefix": "/api/v1",
+ "external_api_key": "your-external-api-key",
+ "cors_origins": ["http://localhost:3000"],
+ "workers": 1,
+ "monitor": {
+ "enabled": true,
+ "check_interval": 30
+ }
+}
+```
+
+### .env
+
+```bash
+# 数据库配置
+DATABASE_URL=postgresql://wxauto:wxauto_password@postgres:5432/wxauto
+
+# Redis 配置
+REDIS_URL=redis://redis:6379/0
+
+# 应用配置
+APP_NAME=WXAuto Center
+HOST=0.0.0.0
+PORT=8080
+DEBUG=false
+SECRET_KEY=your-production-secret-key
+WORKERS=1
+
+# API 配置
+API_PREFIX=/api/v1
+EXTERNAL_API_KEY=your-external-api-key
+CORS_ORIGINS=["http://localhost:3000"]
+
+# 监控配置
+MONITOR_ENABLED=true
+MONITOR_CHECK_INTERVAL=30
+```
+
+---
+
+## 环境变量配置优先级
+
+某些敏感配置项可以通过环境变量覆盖配置文件:
+
+| 环境变量 | 对应配置项 |
+|----------|------------|
+| APP_NAME | app_name |
+| HOST | host |
+| PORT | port |
+| DEBUG | debug |
+| SECRET_KEY | secret_key |
+| API_PREFIX | api_prefix |
+| EXTERNAL_API_KEY | external_api_key |
+| CORS_ORIGINS | cors_origins |
+| WORKERS | workers |
+| DATABASE_URL | database.url |
+| REDIS_URL | redis.url |
+| MONITOR_ENABLED | monitor.enabled |
+| MONITOR_CHECK_INTERVAL | monitor.check_interval |
+
+---
+
+## 配置加载与保存流程
+
+```
+启动时 (startup_event)
+ │
+ ├──► load_config_from_file("config.json")
+ │ │
+ │ ▼
+ │ 解析 JSON 配置
+ │ │
+ │ ▼
+ │ 设置 Settings 各属性
+ │ │
+ │ ▼
+ │ 初始化数据库连接
+ │ │
+ │ ▼
+ │ 从数据库加载节点配置 (nodes 表)
+ │ │
+ │ ▼
+ │ NodeManager.add_node() 注册节点
+ │
+ └──► register_builtin_plugins()
+
+关闭时 (shutdown_event)
+ │
+ ├──► monitor_service.stop()
+ │
+ └──► 保存配置到文件 (如需要)
+```
+
+---
+
+## 数据库表结构
+
+### nodes 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| node_id | VARCHAR(50) | 节点唯一标识符 |
+| name | VARCHAR(100) | 节点显示名称 |
+| api_url | VARCHAR(255) | 节点 API 地址 |
+| api_key | VARCHAR(255) | 节点 API 密钥 |
+| enabled | BOOLEAN | 是否启用 |
+| description | TEXT | 节点描述 |
+| group | VARCHAR(50) | 节点分组 |
+| status | VARCHAR(20) | API 状态 |
+| wechat_status | VARCHAR(20) | 微信状态 |
+| is_healthy | BOOLEAN | 健康状态 |
+| created_at | TIMESTAMP | 创建时间 |
+| updated_at | TIMESTAMP | 更新时间 |
+
+### webhook_urls 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | SERIAL | Webhook ID |
+| name | VARCHAR(100) | Webhook 名称 |
+| url | VARCHAR(500) | Webhook URL 地址 |
+| format | VARCHAR(20) | 消息格式 (bark/wechat) |
+| enabled | BOOLEAN | 是否启用 |
+| event_types | TEXT | 支持的事件类型 |
+| created_at | TIMESTAMP | 创建时间 |
+| updated_at | TIMESTAMP | 更新时间 |
+
+### logs 表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | SERIAL | 日志 ID |
+| timestamp | TIMESTAMP | 日志时间 |
+| level | VARCHAR(20) | 日志级别 |
+| source | VARCHAR(50) | 日志来源 |
+| message | TEXT | 日志消息 |
+
+详细表结构请参阅 [DATABASE.md](./DATABASE.md)。
diff --git a/doc/wxauto_center/DATABASE.md b/doc/wxauto_center/DATABASE.md
new file mode 100644
index 0000000..f02e5be
--- /dev/null
+++ b/doc/wxauto_center/DATABASE.md
@@ -0,0 +1,282 @@
+# ///
+# DATABASE.md
+# 描述:WXAuto Center 数据库设计文档
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# 更新日期:2026-04-06
+# ///
+
+# WXAuto Center 数据库设计文档
+
+## 概述
+
+本系统使用 PostgreSQL 数据库,通过 SQLAlchemy ORM 进行数据访问。数据库包含以下表:
+
+| 表名 | 说明 |
+|------|------|
+| `nodes` | 节点配置表 |
+| `webhook_urls` | Webhook URL 表 |
+| `logs` | 日志表 |
+| `plugin_configs` | 插件配置表 |
+
+## ER 图
+
+```
+┌─────────────────┐ ┌─────────────────┐
+│ nodes │ │ webhook_urls │
+├─────────────────┤ ├─────────────────┤
+│ node_id (PK) │ │ id (PK) │
+│ name │ │ name │
+│ api_url │ │ url │
+│ api_key │ │ format │
+│ enabled │ │ enabled │
+│ description │ │ event_types │
+│ group │ │ created_at │
+│ status │ │ updated_at │
+│ wechat_status │ └─────────────────┘
+│ is_healthy │
+│ health_message │
+│ created_at │
+│ updated_at │
+└─────────────────┘
+
+┌─────────────────┐ ┌─────────────────┐
+│ logs │ │ plugin_configs │
+├─────────────────┤ ├─────────────────┤
+│ id (PK) │ │ id (PK) │
+│ timestamp │ │ plugin_name │
+│ level │ │ config_json │
+│ source │ │ enabled │
+│ message │ │ created_at │
+└─────────────────┘ │ updated_at │
+ └─────────────────┘
+```
+
+---
+
+## 表结构详解
+
+### 1. nodes - 节点配置表
+
+存储所有微信节点的信息。
+
+| 字段 | 类型 | 约束 | 说明 |
+|------|------|------|------|
+| `node_id` | VARCHAR(50) | **PK** | 节点唯一标识 |
+| `name` | VARCHAR(100) | NOT NULL | 节点显示名称 |
+| `api_url` | VARCHAR(255) | NOT NULL | 节点 API 地址 |
+| `api_key` | VARCHAR(255) | NOT NULL | 节点 API 密钥 |
+| `enabled` | BOOLEAN | DEFAULT true | 是否启用 |
+| `description` | TEXT | DEFAULT '' | 节点描述 |
+| `group` | VARCHAR(50) | DEFAULT 'default' | 节点分组 |
+| `status` | VARCHAR(20) | DEFAULT 'inactive' | API 状态 |
+| `wechat_status` | VARCHAR(20) | DEFAULT 'online' | 微信状态 |
+| `is_healthy` | BOOLEAN | DEFAULT false | 健康检查状态 |
+| `health_message` | VARCHAR(255) | DEFAULT '' | 健康检查消息 |
+| `created_at` | TIMESTAMP | DEFAULT now | 创建时间 |
+| `updated_at` | TIMESTAMP | DEFAULT now | 更新时间 |
+
+**状态值说明:**
+
+| status 值 | 说明 |
+|------------|------|
+| `active` | API 正常响应 |
+| `inactive` | API 未响应 |
+| `error` | API 返回错误 |
+
+| wechat_status 值 | 说明 |
+|------------------|------|
+| `online` | 微信已登录 |
+| `offline` | 微信未登录 |
+
+**SQL:**
+```sql
+CREATE TABLE nodes (
+ node_id VARCHAR(50) PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ api_url VARCHAR(255) NOT NULL,
+ api_key VARCHAR(255) NOT NULL,
+ enabled BOOLEAN DEFAULT true,
+ description TEXT DEFAULT '',
+ group VARCHAR(50) DEFAULT 'default',
+ status VARCHAR(20) DEFAULT 'inactive',
+ wechat_status VARCHAR(20) DEFAULT 'online',
+ is_healthy BOOLEAN DEFAULT false,
+ health_message VARCHAR(255) DEFAULT '',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+---
+
+### 2. webhook_urls - Webhook URL 表
+
+存储告警 webhook 配置。
+
+| 字段 | 类型 | 约束 | 说明 |
+|------|------|------|------|
+| `id` | INTEGER | **PK**, AUTO | 主键 |
+| `name` | VARCHAR(100) | NOT NULL | Webhook 名称 |
+| `url` | VARCHAR(500) | NOT NULL | Webhook URL |
+| `format` | VARCHAR(20) | DEFAULT 'bark' | 消息格式 |
+| `enabled` | BOOLEAN | DEFAULT true | 是否启用 |
+| `event_types` | TEXT | DEFAULT '' | 触发事件类型(逗号分隔) |
+| `created_at` | TIMESTAMP | DEFAULT now | 创建时间 |
+| `updated_at` | TIMESTAMP | DEFAULT now | 更新时间 |
+
+**format 支持的格式:**
+- `bark` - Bark 推送格式
+- `wecom` - 企业微信
+- `dingtalk` - 钉钉
+- `feishu` - 飞书
+- `telegram` - Telegram
+- `serverchan` - Server酱
+
+**event_types 支持的类型:**
+- `node_offline` - 节点离线
+- `node_error` - 节点错误
+- `wechat_offline` - 微信离线
+- `all` - 所有事件
+
+---
+
+### 3. logs - 日志表
+
+存储系统运行日志。
+
+| 字段 | 类型 | 约束 | 说明 |
+|------|------|------|------|
+| `id` | INTEGER | **PK**, AUTO | 主键 |
+| `timestamp` | TIMESTAMP | DEFAULT now | 日志时间 |
+| `level` | VARCHAR(20) | NOT NULL | 日志级别 |
+| `source` | VARCHAR(50) | NOT NULL | 日志来源 |
+| `message` | TEXT | NOT NULL | 日志消息 |
+
+**level 值:**
+- `INFO` - 信息
+- `WARNING` - 警告
+- `ERROR` - 错误
+- `SUCCESS` - 成功
+- `DEBUG` - 调试
+
+---
+
+### 4. plugin_configs - 插件配置表
+
+存储插件的配置和启用状态。
+
+| 字段 | 类型 | 约束 | 说明 |
+|------|------|------|------|
+| `id` | INTEGER | **PK**, AUTO | 主键 |
+| `plugin_name` | VARCHAR(100) | **UNIQUE**, NOT NULL | 插件名称 |
+| `config_json` | TEXT | DEFAULT '{}' | 插件配置(JSON格式) |
+| `enabled` | BOOLEAN | DEFAULT false | 是否启用 |
+| `created_at` | TIMESTAMP | DEFAULT now | 创建时间 |
+| `updated_at` | TIMESTAMP | DEFAULT now | 更新时间 |
+
+**config_json 示例:**
+```json
+{
+ "node_id": "wx1",
+ "receiver": "文件传输助手",
+ "cron": "0 8 * * *",
+ "http_url": "https://api.example.com/news",
+ "message_template": "📰 {title}\n{url}"
+}
+```
+
+---
+
+## 数据库操作
+
+### DatabaseService 主要方法
+
+#### 节点操作
+```python
+# 获取所有节点
+db_service.get_all_nodes() -> List[Node]
+
+# 获取指定节点
+db_service.get_node(node_id: str) -> Optional[Node]
+
+# 添加节点
+db_service.add_node(node_id, name, api_url, api_key, description, group)
+
+# 更新节点
+db_service.update_node(node_id, **kwargs) -> bool
+
+# 删除节点
+db_service.delete_node(node_id) -> bool
+
+# 获取启用的节点
+db_service.get_enabled_nodes() -> List[Node]
+```
+
+#### Webhook 操作
+```python
+# 获取所有 Webhook
+db_service.get_all_webhooks() -> List[WebhookUrl]
+
+# 获取启用的 Webhook
+db_service.get_enabled_webhooks() -> List[WebhookUrl]
+
+# 添加 Webhook
+db_service.add_webhook(name, url, format, event_types) -> bool
+
+# 更新 Webhook
+db_service.update_webhook(webhook_id, **kwargs) -> bool
+
+# 删除 Webhook
+db_service.delete_webhook(webhook_id) -> bool
+```
+
+#### 插件配置操作
+```python
+# 获取插件配置
+db_service.get_plugin_config(plugin_name: str) -> Optional[PluginConfig]
+
+# 获取所有插件配置
+db_service.get_all_plugin_configs() -> List[PluginConfig]
+
+# 保存插件配置
+db_service.save_plugin_config(plugin_name, config, enabled) -> bool
+
+# 更新插件启用状态
+db_service.update_plugin_enabled(plugin_name, enabled) -> bool
+```
+
+---
+
+## 索引
+
+```sql
+-- nodes 表索引
+CREATE INDEX idx_nodes_enabled ON nodes(enabled);
+CREATE INDEX idx_nodes_group ON nodes(group);
+CREATE INDEX idx_nodes_status ON nodes(status);
+
+-- webhook_urls 表索引
+CREATE INDEX idx_webhook_enabled ON webhook_urls(enabled);
+
+-- logs 表索引
+CREATE INDEX idx_logs_timestamp ON logs(timestamp);
+CREATE INDEX idx_logs_level ON logs(level);
+```
+
+---
+
+## 迁移注意
+
+如果数据库已存在,新增 `plugin_configs` 表:
+
+```sql
+CREATE TABLE IF NOT EXISTS plugin_configs (
+ id SERIAL PRIMARY KEY,
+ plugin_name VARCHAR(100) UNIQUE NOT NULL,
+ config_json TEXT DEFAULT '{}',
+ enabled BOOLEAN DEFAULT false,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
diff --git a/doc/wxauto_center/DEVELOPMENT.md b/doc/wxauto_center/DEVELOPMENT.md
new file mode 100644
index 0000000..de1ffe3
--- /dev/null
+++ b/doc/wxauto_center/DEVELOPMENT.md
@@ -0,0 +1,770 @@
+# WXAuto Center 开发指南
+
+## 开发环境搭建
+
+### 环境要求
+
+- Python 3.8+
+- Node.js 14+ (可选,用于前端开发)
+- Git
+
+### 克隆与安装
+
+```bash
+# 克隆项目
+git clone
+cd wxauto_api/wxauto_center
+
+# 创建虚拟环境(推荐)
+python -m venv venv
+source venv/bin/activate # Linux/Mac
+# 或 venv\Scripts\activate # Windows
+
+# 安装依赖
+pip install -r requirements.txt
+```
+
+### 配置开发环境
+
+```bash
+# 复制环境变量示例文件
+cp .env.example .env
+
+# 编辑 .env 文件
+vim .env
+```
+
+### 启动开发服务器
+
+```bash
+# 方式1:使用 run.py(推荐)
+python run.py
+
+# 方式2:单进程开发模式
+python run.py --reload
+
+# 方式3:使用 uvicorn
+uvicorn main:app --host 0.0.0.0 --port 8080 --reload
+
+# 方式4:多进程模式(需要 Redis)
+python run.py --workers 4
+```
+
+### 访问服务
+
+- 主界面:http://localhost:8080/static/index.html
+- API 文档:http://localhost:8080/docs
+- ReDoc:http://localhost:8080/redoc
+
+---
+
+## 项目结构详解
+
+### 目录结构
+
+```
+wxauto_center/
+├── main.py # FastAPI 应用入口
+├── run.py # 多进程启动脚本
+├── config.py # 配置管理模块
+├── api/ # API 路由目录
+│ ├── __init__.py
+│ ├── nodes.py # 节点管理 API
+│ ├── messages.py # 消息发送 API
+│ ├── external.py # 外部扩展 API
+│ ├── callback.py # 回调接口 API
+│ └── plugins.py # 插件管理 API
+├── services/ # 业务逻辑目录
+│ ├── __init__.py
+│ ├── node_manager.py # 节点管理器
+│ ├── monitor_service.py # 监控服务
+│ ├── log_service.py # 日志服务(支持Redis+文件持久化)
+│ └── queue_service.py # 消息队列服务(支持Redis后端)
+├── plugins/ # 插件目录
+│ ├── __init__.py
+│ ├── base.py # 插件基类
+│ └── builtin.py # 内置插件
+├── models/ # 数据模型目录
+├── utils/ # 工具函数目录
+├── static/ # 静态文件目录
+│ ├── index.html # 主页面
+│ ├── css/ # 样式目录
+│ └── js/ # JavaScript 目录
+├── logs/ # 日志目录
+│ └── app.log # 应用日志
+├── config.json # 配置文件
+└── requirements.txt # 依赖列表
+```
+
+---
+
+## 代码架构说明
+
+### 主入口 (main.py)
+
+`main.py` 是 FastAPI 应用的入口文件,负责:
+
+1. 初始化 FastAPI 应用
+2. 配置 CORS 中间件
+3. 注册路由(nodes, messages, external, plugins)
+4. 处理启动/关闭事件
+
+关键代码流程:
+
+```python
+# 启动事件
+@app.on_event("startup")
+async def startup_event():
+ load_config_from_file("config.json") # 加载配置
+ register_builtin_plugins() # 注册内置插件
+ for node_data in config_manager.get_all_nodes():
+ await node_manager.add_node(...) # 添加节点
+ await monitor_service.start() # 启动监控服务
+
+# 关闭事件
+@app.on_event("shutdown")
+async def shutdown_event():
+ await monitor_service.stop() # 停止监控
+ save_config_to_file("config.json") # 保存配置
+```
+
+### 配置管理 (config.py)
+
+采用 Pydantic Settings 进行配置管理:
+
+```python
+# 配置模型
+Settings # 主配置类
+NodeConfig # 节点配置
+PluginConfig # 插件配置
+WebhookConfig # Webhook 配置
+MonitorConfig # 监控配置
+
+# 配置管理器
+ConfigManager # 提供节点配置的增删改查
+
+# 配置加载/保存
+load_config_from_file() # 从文件加载
+save_config_to_file() # 保存到文件
+```
+
+### 节点管理器 (services/node_manager.py)
+
+`NodeManager` 是核心服务类,负责与 WXAuto-HTTP-API 节点通信。
+
+```python
+# 节点状态
+class NodeStatus:
+ ACTIVE = "active" # API 可用
+ INACTIVE = "inactive" # API 不可用
+ ERROR = "error" # API 错误
+
+# 节点类
+class Node:
+ node_id: str # 节点唯一标识
+ name: str # 节点显示名称
+ api_url: str
+ api_key: str
+ description: str
+ group: str
+ enabled: bool
+ status: str # NodeStatus
+ wechat_status: str
+ is_healthy: bool
+ health_message: str
+
+# NodeManager 方法
+class NodeManager:
+ async def add_node(...) # 添加节点
+ async def remove_node(node_id) # 删除节点
+ async def get_node(node_id) # 获取单个节点
+ async def get_all_nodes() # 获取所有节点
+ async def get_enabled_nodes() # 获取已启用节点
+ async def get_nodes_by_group() # 按分组获取节点
+ async def update_node(node_id, **kwargs) # 更新节点
+ async def check_node_status(node_id) # 检查节点状态
+ async def is_node_healthy(node_id) # 检查节点健康状态
+ async def send_message(node_id, who, msg, msg_type) # 发送消息
+ async def send_message_to_group(node_id, group_name, msg, msg_type) # 发送群消息
+ async def get_friends_list(node_id) # 获取好友列表
+ async def get_groups_list(node_id) # 获取群列表
+ async def get_messages(node_id, chat_name, limit) # 获取消息历史
+
+**注意**:已移除 `broadcast_message()` 方法,广播功能不再支持。
+
+### 监控服务 (services/monitor_service.py)
+
+`MonitorService` 负责定时检测节点和微信状态。
+
+```python
+class MonitorService:
+ async def start() # 启动监控
+ async def stop() # 停止监控
+ async def _monitor_loop() # 主监控循环
+ async def _check_all_nodes() # 检查所有节点
+ async def _check_node(node_id) # 检查单个节点
+ async def _send_webhook(event, data) # 发送Webhook通知
+ def get_last_status(node_id) # 获取最后状态
+ def get_all_status() # 获取所有状态
+```
+
+### 日志服务 (services/log_service.py)
+
+`LogService` 提供日志记录和实时推送功能。
+
+```python
+class LogService:
+ def __init__(self, max_logs=500):
+ self.max_logs = max_logs
+ self._logs: deque = deque(maxlen=max_logs) # 限制500条
+ self._subscribers: List[asyncio.Queue] = []
+
+ # 日志方法
+ def info/warning/error/success(message, source)
+
+ # SSE 订阅
+ async def subscribe() -> asyncio.Queue # 订阅日志流
+ def unsubscribe(queue) # 取消订阅
+
+ # 日志查询
+ def get_recent_logs(count) # 获取最近日志
+ def get_all_logs() # 获取所有日志
+```
+
+---
+
+## 插件开发指南
+
+### 插件架构
+
+系统采用插件化设计,支持扩展:
+
+```
+PluginBase (抽象基类)
+├── MessageHandlerPlugin
+├── DataSourcePlugin
+├── ActionTriggerPlugin
+└── AIAgentPlugin
+```
+
+### 开发自定义插件
+
+#### 1. 创建插件类
+
+```python
+# my_plugin.py
+from plugins.base import PluginBase, PluginType
+
+class MyCustomPlugin(PluginBase):
+ plugin_name = "my_custom_plugin"
+ plugin_version = "1.0.0"
+ plugin_type = PluginType.CUSTOM
+ plugin_description = "我的自定义插件"
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ # 初始化逻辑
+ return True
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ # 插件逻辑
+ return {"success": True, "result": "done"}
+```
+
+#### 2. 注册插件
+
+```python
+# 在 builtin.py 或 main.py 中注册
+from plugins.base import PluginRegistry
+
+my_plugin = MyCustomPlugin()
+my_plugin.initialize({"key": "value"})
+PluginRegistry.register(my_plugin)
+```
+
+### 插件类型详解
+
+#### DataSourcePlugin
+
+用于从外部数据源获取数据:
+
+```python
+class MyDataSourcePlugin(DataSourcePlugin):
+ plugin_name = "my_data_source"
+
+ def fetch_data(self, query: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
+ # 获取数据逻辑
+ return [...]
+```
+
+#### AIAgentPlugin
+
+用于 AI 消息处理:
+
+```python
+class MyAIAgentPlugin(AIAgentPlugin):
+ plugin_name = "my_ai_agent"
+
+ def process(self, input_text: str, context: Dict[str, Any]) -> str:
+ # 处理输入,返回 AI 回复
+ return "AI 回复"
+
+ def get_response(self, messages: List[Dict[str, str]]) -> str:
+ # 获取对话响应
+ return "AI 回复"
+```
+
+---
+
+## API 调用示例
+
+### 使用 Python 调用
+
+```python
+import httpx
+
+API_KEY = "your-external-api-key"
+BASE_URL = "http://localhost:8080/api/v1"
+
+headers = {
+ "X-API-Key": API_KEY,
+ "Content-Type": "application/json"
+}
+
+# 添加节点
+response = httpx.post(
+ f"{BASE_URL}/nodes",
+ json={
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.1.100:5000",
+ "api_key": "node-api-key"
+ },
+ headers=headers
+)
+print(response.json())
+
+# 发送消息
+response = httpx.post(
+ f"{BASE_URL}/messages/send",
+ json={
+ "node_id": "wx1",
+ "who": "wxid_friend",
+ "msg": "Hello!"
+ },
+ headers=headers
+)
+print(response.json())
+```
+
+### 使用 curl 调用
+
+```bash
+# 添加节点
+curl -X POST http://localhost:8080/api/v1/nodes \
+ -H "X-API-Key: your-external-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"node_id": "wx1", "name": "微信1", "api_url": "http://192.168.1.100:5000", "api_key": "key"}'
+
+# 发送消息
+curl -X POST http://localhost:8080/api/v1/messages/send \
+ -H "X-API-Key: your-external-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"node_id": "wx1", "who": "wxid_friend", "msg": "Hello"}'
+
+# 获取节点状态
+curl http://localhost:8080/api/v1/nodes/wx1/status \
+ -H "X-API-Key: your-external-api-key"
+
+# 获取监控状态
+curl http://localhost:8080/api/v1/monitor/status \
+ -H "X-API-Key: your-external-api-key"
+
+# 获取最近日志
+curl "http://localhost:8080/api/v1/logs/recent?count=20" \
+ -H "X-API-Key: your-external-api-key"
+
+# 重置节点微信状态(回调接口)
+curl -X POST http://localhost:8080/api/v1/callback/node/reset \
+ -H "X-API-Key: your-external-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"node_id": "wx1"}'
+```
+
+### 使用 JavaScript 调用
+
+```javascript
+const API_KEY = 'your-external-api-key';
+const BASE_URL = 'http://localhost:8080/api/v1';
+
+async function sendMessage(nodeId, who, msg) {
+ const response = await fetch(`${BASE_URL}/messages/send`, {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': API_KEY,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ node_id: nodeId,
+ who: who,
+ msg: msg
+ })
+ });
+ return await response.json();
+}
+
+// 使用示例
+sendMessage('wx1', 'wxid_friend', 'Hello!')
+ .then(result => console.log(result));
+```
+
+---
+
+## 前端开发
+
+### 前端架构
+
+前端采用纯 JavaScript 模块化设计:
+
+```
+static/
+├── index.html # 主页面
+├── css/
+│ └── style.css # 样式文件
+└── js/
+ ├── api.js # API 调用封装
+ ├── app.js # 主逻辑
+ └── pages/
+ └── templates.js # 页面模板
+```
+
+### 页面模块
+
+系统前端包含以下页面:
+
+| 页面 | 功能 |
+|------|------|
+| dashboard | 系统概览 |
+| nodes | 节点管理 |
+| monitor | 监控状态 |
+| send | 消息发送 |
+| logs | 日志查看 |
+| external | 外部接口 |
+| docs | 文档 |
+| settings | 设置 |
+
+### API 封装 (api.js)
+
+```javascript
+// 基础配置
+const API_BASE = '/api/v1';
+const API_KEY = 'your-api-key';
+
+// 通用请求函数
+async function apiRequest(endpoint, options = {}) {
+ const url = `${API_BASE}${endpoint}`;
+ const headers = {
+ 'X-API-Key': API_KEY,
+ 'Content-Type': 'application/json',
+ ...options.headers
+ };
+
+ const response = await fetch(url, {
+ ...options,
+ headers
+ });
+
+ return response.json();
+}
+
+// API 方法封装
+const API = {
+ // 节点管理
+ getNodes: (group) => apiRequest(`/nodes${group ? `?group=${group}` : ''}`),
+ addNode: (data) => apiRequest('/nodes', { method: 'POST', body: JSON.stringify(data) }),
+ deleteNode: (name) => apiRequest(`/nodes/${name}`, { method: 'DELETE' }),
+
+ // 消息发送
+ sendMessage: (data) => apiRequest('/messages/send', { method: 'POST', body: JSON.stringify(data) }),
+ sendGroupMessage: (data) => apiRequest('/messages/send_group', { method: 'POST', body: JSON.stringify(data) }),
+ batchSendMessage: (data) => apiRequest('/messages/batch', { method: 'POST', body: JSON.stringify(data) }),
+
+ // 监控状态
+ getMonitorStatus: () => apiRequest('/monitor/status'),
+ getRecentLogs: (count) => apiRequest(`/logs/recent?count=${count || 50}`)
+};
+```
+
+### SSE 日志订阅
+
+```javascript
+async function subscribeLogs(callback) {
+ const eventSource = new EventSource('/api/v1/logs/stream');
+
+ eventSource.onmessage = (event) => {
+ const logEntry = JSON.parse(event.data);
+ callback(logEntry);
+ };
+
+ eventSource.onerror = (error) => {
+ console.error('SSE Error:', error);
+ eventSource.close();
+ };
+
+ return eventSource; // 使用完毕后调用 close()
+}
+
+// 使用示例
+const es = subscribeLogs((log) => {
+ console.log(`[${log.level}] ${log.message}`);
+});
+// 关闭时: es.close();
+```
+
+---
+
+## 测试
+
+### 运行测试
+
+```bash
+# 安装测试依赖
+pip install pytest pytest-asyncio httpx
+
+# 运行所有测试
+pytest
+
+# 运行指定测试文件
+pytest tests/test_nodes.py
+
+# 带详细输出
+pytest -v
+```
+
+### 编写测试
+
+```python
+# tests/test_nodes.py
+import pytest
+from httpx import AsyncClient
+from main import app
+
+@pytest.mark.asyncio
+async def test_add_node():
+ async with AsyncClient(app=app, base_url="http://test") as client:
+ response = await client.post(
+ "/api/v1/nodes",
+ json={
+ "name": "test_node",
+ "api_url": "http://localhost:5000",
+ "api_key": "test_key"
+ },
+ headers={"X-API-Key": "test-key"}
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+```
+
+---
+
+## 调试技巧
+
+### 启用调试模式
+
+```bash
+# 方式1:环境变量
+export DEBUG=true
+python main.py
+
+# 方式2:命令行参数(如果支持)
+uvicorn main:app --reload --log-level debug
+
+# 方式3:修改 .env
+DEBUG=true
+```
+
+### 查看详细日志
+
+系统内置日志服务,可通过 API 查看:
+
+```bash
+# 获取最近日志
+curl http://localhost:8080/api/v1/logs/recent?count=100 \
+ -H "X-API-Key: your-api-key"
+
+# 订阅实时日志流
+curl -N http://localhost:8080/api/v1/logs/stream \
+ -H "X-API-Key: your-api-key"
+```
+
+### API 测试工具
+
+- FastAPI 内置 docs:http://localhost:8080/docs
+- ReDoc:http://localhost:8080/redoc
+- Postman / Insomnia
+
+---
+
+## 代码规范
+
+### Python 代码规范
+
+遵循 PEP 8 和项目规则:
+
+- 使用 4 空格缩进
+- 类名使用 PascalCase
+- 函数/变量使用 camelCase
+- 常量使用 UPPER_SNAKE_CASE
+- 每个文件包含头部注释
+
+### 注释要求
+
+```python
+# ///
+# filename.py
+# 描述:该文件负责 [核心功能]
+# 作者:AI Generated
+# 创建日期:YYYY-MM-DD
+# ///
+
+def function_name(param1, param2):
+ """
+ 函数简要描述
+
+ @param {type} param1 - 参数说明
+ @return {type} 返回值说明
+ @throws {异常类型} 异常情况说明
+ """
+ pass
+```
+
+---
+
+## 部署注意事项
+
+### 生产环境配置
+
+1. 修改 `secret_key` 为强密码
+2. 设置 `debug: false`
+3. 配置正确的 `cors_origins`
+4. 使用 HTTPS
+5. 配置防火墙规则
+
+### 使用 Nginx 反向代理
+
+```nginx
+server {
+ listen 80;
+ server_name your-domain.com;
+
+ location / {
+ proxy_pass http://127.0.0.1:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /api {
+ proxy_pass http://127.0.0.1:8080;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_cache_bypass $http_upgrade;
+ }
+}
+```
+
+### 使用 systemd 服务
+
+```ini
+# /etc/systemd/system/wxauto-center.service
+[Unit]
+Description=WXAuto Center Service
+After=network.target
+
+[Service]
+Type=simple
+User=your-user
+WorkingDirectory=/path/to/wxauto_center
+ExecStart=/path/to/venv/bin/python main.py
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+```
+
+```bash
+sudo systemctl enable wxauto-center
+sudo systemctl start wxauto-center
+```
+
+---
+
+## 关键实现细节
+
+### 1. 健康检测机制 (is_node_healthy)
+
+每次发送消息前,系统会调用此方法检测节点健康状态:
+
+```python
+async def is_node_healthy(self, name: str) -> tuple[bool, str]:
+ # 1. 检查 API 状态
+ api_status = await self.check_node_status(name)
+ if api_status != NodeStatus.ACTIVE:
+ return False, f"API status: {api_status}"
+
+ # 2. 调用 /api/wechat/initialize 检测微信
+ init_result = await self.call_node_api(name, "/api/wechat/initialize", method="POST")
+
+ # 3. 解析返回状态
+ if not init_result.get("success"):
+ return False, f"WeChat check failed: {init_result.get('error')}"
+
+ # 4. 检查 code 和 status
+ wx_code = init_result.get("code") or init_result.get("data", {}).get("code")
+ wx_status = init_result.get("data", {}).get("data", {}).get("status")
+
+ if wx_code != 0:
+ return False, "WeChat error: ..."
+
+ if wx_status not in ["connected", "online"]:
+ return False, f"WeChat status: {wx_status}"
+
+ return True, "OK"
+```
+
+### 2. 消息发送流程
+
+```python
+async def send_message(self, name: str, who: str, msg: str, msg_type: str = "text"):
+ # 1. 先初始化微信
+ await self.call_node_api(name, "/api/wechat/initialize", method="POST")
+
+ # 2. 发送消息
+ return await self.call_node_api(
+ name,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": who, "message": msg}
+ )
+```
+
+### 3. 监控告警流程
+
+```python
+async def _check_node(self, node_id: str):
+ # 检测 API 状态变化
+ api_status = await node_manager.check_node_status(node_id)
+ if old_api_status != api_status:
+ event = "node_online" if api_status == "active" else "node_offline"
+ await self._send_webhook(event, {...})
+
+ # 检测微信状态变化(通过发送消息检测)
+ wechat_status = node.wechat_status # 从节点对象获取
+ if old_wechat_status != wechat_status:
+ event = "wechat_online" if wechat_status == "online" else "wechat_offline"
+ await self._send_webhook(event, {...})
+```
+
+**注意**:微信状态现在通过消息发送结果自动更新,不需要主动检测。
diff --git a/doc/wxauto_center/FLOWS.md b/doc/wxauto_center/FLOWS.md
new file mode 100644
index 0000000..1d05534
--- /dev/null
+++ b/doc/wxauto_center/FLOWS.md
@@ -0,0 +1,662 @@
+# WXAuto Center 业务流程文档
+
+## 概述
+
+本文档详细描述 WXAuto Center 的核心业务流程,包括消息发送流程、回调重置流程、监控告警流程、健康检测流程和日志发布订阅流程。
+
+---
+
+## 1. 节点标识系统
+
+### 1.1 node_id 与 name 的区别
+
+每个节点有两个标识:
+
+| 字段 | 说明 | 示例 | 用途 |
+|------|------|------|------|
+| node_id | 节点唯一标识 | "wx1", "node_001" | API 调用、系统内部引用 |
+| name | 节点显示名称 | "微信1", "测试节点" | 界面展示、用户友好提示 |
+
+### 1.2 节点数据模型
+
+```json
+{
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.20.18:5000",
+ "api_key": "key",
+ "enabled": true,
+ "description": "",
+ "group": "wechat",
+ "status": "active",
+ "wechat_status": "online",
+ "is_healthy": true
+}
+```
+
+---
+
+## 2. 消息发送流程
+
+### 2.1 流程图
+
+```
+外部请求
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ POST /api/v1/messages/send │
+│ 或 │
+│ POST /api/v1/external/send │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ check_node_health(node_id) │
+│ 调用 /api/wechat/initialize 检测微信状态 │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ├──► 微信状态异常?
+ │ │
+ │ ▼ (是)
+ │ 返回 HTTP 503
+ │ "Node 'wx1' is not healthy..."
+ │ │
+ │ ▼
+ │ 流程结束
+ │
+ ▼ (否)
+┌─────────────────────────────────────────────────────────────┐
+│ node_manager.send_message(node_id, who, msg, msg_type) │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ├──► POST /api/wechat/initialize (初始化微信)
+ │
+ └──► POST /api/message/send (发送消息)
+ │
+ ▼
+ 发送成功?
+ │
+ ├──► 是 → wechat_status = "online"
+ │ │
+ │ ▼
+ │ 返回成功响应
+ │
+ └──► 否 → wechat_status = "offline"
+ │
+ ▼
+ 返回错误响应
+```
+
+### 2.2 详细步骤说明
+
+| 步骤 | 操作 | 说明 |
+|------|------|------|
+| 1 | 接收发送消息请求 | 解析 node_id, who, msg, msg_type |
+| 2 | 健康检测 | 调用 is_node_healthy() 检测节点 |
+| 3 | 初始化微信 | 调用 /api/wechat/initialize |
+| 4 | 发送消息 | 调用 /api/message/send |
+| 5 | 更新状态 | 根据发送结果更新 wechat_status |
+| 6 | 返回响应 | 返回发送结果给客户端 |
+
+### 2.3 代码逻辑
+
+```python
+async def send_message(node_id: str, who: str, msg: str, msg_type: str = "text"):
+ # 1. 检查节点是否健康
+ is_healthy, health_msg = await node_manager.is_node_healthy(node_id)
+ if not is_healthy:
+ raise HTTPException(
+ status_code=503,
+ detail=f"Node '{node_id}' is not healthy: {health_msg}"
+ )
+
+ # 2. 初始化微信
+ await node_manager.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+
+ # 3. 发送消息
+ result = await node_manager.call_node_api(
+ node_id,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": who, "message": msg}
+ )
+
+ # 4. 更新状态
+ if result.get("code") == 0:
+ node_manager.update_node_status(node_id, wechat_status="online")
+ else:
+ node_manager.update_node_status(node_id, wechat_status="offline")
+
+ return result
+```
+
+---
+
+## 3. 回调重置流程
+
+### 3.1 流程图
+
+```
+外部节点回调
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ POST /api/v1/callback/node/reset │
+│ 请求体: {"node_id": "wx1"} │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 调用 /api/wechat/initialize 初始化微信 │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 调用 /api/message/send 发送检测消息到"文件传输助手" │
+│ receiver: "filehelper" │
+│ message: "WXAuto Center 检测消息" │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+检测结果
+ │
+ ├──► 发送成功
+ │ │
+ │ ▼
+ │ wechat_status = "online"
+ │ │
+ │ ▼
+ │ 返回 {"success": true, "wechat_status": "online"}
+ │
+ └──► 发送失败
+ │
+ ▼
+ wechat_status = "offline"
+ │
+ ▼
+ 返回 {"success": false, "wechat_status": "offline"}
+ │
+ ▼
+ MonitorService 检测到状态变化
+ │
+ ▼
+ 触发 Webhook 告警 (wechat_offline)
+```
+
+### 3.2 详细步骤说明
+
+| 步骤 | 操作 | 说明 |
+|------|------|------|
+| 1 | 接收重置请求 | 解析 node_id |
+| 2 | 初始化微信 | 调用 /api/wechat/initialize |
+| 3 | 发送检测消息 | 发送消息到文件传输助手(filehelper) |
+| 4 | 判断结果 | 根据发送是否成功设置状态 |
+| 5 | 触发告警 | 状态变化时触发 Webhook |
+
+### 3.3 回调接口说明
+
+**POST /api/v1/callback/node/reset**
+
+用于重置节点的微信状态。当微信掉线后,可以通过此接口重新检测微信状态。
+
+**请求体**:
+```json
+{
+ "node_id": "wx1"
+}
+```
+
+**响应示例 - 成功**:
+```json
+{
+ "success": true,
+ "message": "微信状态已重置",
+ "data": {
+ "node_id": "wx1",
+ "wechat_status": "online",
+ "check_result": "success"
+ }
+}
+```
+
+**响应示例 - 失败**:
+```json
+{
+ "success": false,
+ "message": "微信状态检测失败",
+ "data": {
+ "node_id": "wx1",
+ "wechat_status": "offline",
+ "check_result": "failed",
+ "error": "Unknown error"
+ }
+}
+```
+
+---
+
+## 4. 监控告警流程
+
+### 4.1 流程图
+
+```
+MonitorService 启动
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ _monitor_loop() 定时循环 │
+│ 间隔: check_interval (默认 60 秒) │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ _check_all_nodes() │
+│ 遍历所有节点 │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+对每个节点执行 _check_node(node_id):
+ │
+ ├──► api_check_enabled?
+ │ │
+ │ ▼ (是)
+ │ check_node_status(node_id)
+ │ │
+ │ ▼
+ │ API 状态变化?
+ │ │
+ │ ├──► 是 → 触发 Webhook (node_online/node_offline)
+ │ │
+ │ └──► 否 → 无操作
+ │
+ └──► wechat_check_enabled?
+ │
+ ▼ (是)
+ initialize_wechat(node_id)
+ │
+ ▼
+ get_wechat_status(node_id)
+ │
+ ▼
+ 微信状态变化?
+ │
+ ├──► 是 → 触发 Webhook (wechat_online/wechat_offline)
+ │
+ └──► 否 → 无操作
+```
+
+### 4.2 支持的事件类型
+
+| 事件类型 | 触发条件 | 消息内容 |
+|----------|----------|----------|
+| api_error | API 调用失败 | "API Error: {error}" |
+| wechat_offline | 微信状态变为 offline | "微信掉线" |
+| wechat_online | 微信状态变为 online/connected | "微信上线" |
+| node_offline | 节点 API 不可用 | "节点离线" |
+| node_online | 节点 API 恢复可用 | "节点在线" |
+
+### 4.3 Webhook 消息格式
+
+**企业微信格式**:
+```json
+{
+ "msgtype": "text",
+ "text": {
+ "content": "【WXAuto Center 告警】\n事件类型: 微信掉线\n节点: wx1 (微信1)\n状态: offline\n时间: 2026-04-05 10:00:00"
+ }
+}
+```
+
+**Bark 格式**:
+```json
+{
+ "title": "WXAuto告警 - 微信掉线",
+ "body": "wx1\n状态: offline\n时间: 2026-04-05 10:00:00",
+ "icon": "https://..."
+}
+```
+
+---
+
+## 5. 健康检测流程
+
+### 5.1 流程图
+
+```
+is_node_healthy(node_id) 调用
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ check_node_status(node_id) │
+│ 调用 GET /health 检测 API 可用性 │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+API 状态检查
+ │
+ ├──► API 不可用 → 返回 (False, "API status: {status}")
+ │
+ ▼ (API 可用)
+┌─────────────────────────────────────────────────────────────┐
+│ /api/wechat/initialize │
+│ 调用 POST /api/wechat/initialize 初始化微信 │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+解析返回状态 (code, message, status)
+ │
+ ├──► code != 0?
+ │ │
+ │ ├──► "无效"/"1400"/"拒绝访问"
+ │ │ │
+ │ │ ▼
+ │ │ 返回 (False, "WeChat window invalid")
+ │ │
+ │ └──► 其他错误消息
+ │ │
+ │ ▼
+ │ 返回 (False, "WeChat error: {message}")
+ │
+ ├──► status not in ["connected", "online"]?
+ │ │
+ │ └──► 是 → 返回 (False, "WeChat status: {status}")
+ │
+ └──► 其他情况
+ │
+ ▼
+ 返回 (True, "OK")
+```
+
+### 5.2 健康检测判断逻辑
+
+```python
+async def is_node_healthy(node_id: str) -> tuple[bool, str]:
+ # 1. 检查 API 状态
+ api_status = await self.check_node_status(node_id)
+ if api_status != NodeStatus.ACTIVE:
+ return False, f"API status: {api_status}"
+
+ # 2. 调用 /api/wechat/initialize 检测微信
+ init_result = await self.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+
+ # 3. 解析返回状态
+ if not init_result.get("success"):
+ return False, f"WeChat check failed: {init_result.get('error')}"
+
+ wx_code = init_result.get("code") or init_result.get("data", {}).get("code")
+ wx_status = init_result.get("data", {}).get("data", {}).get("status")
+
+ if wx_code != 0:
+ return False, "WeChat error: ..."
+
+ if wx_status not in ["connected", "online"]:
+ return False, f"WeChat status: {wx_status}"
+
+ return True, "OK"
+```
+
+---
+
+## 6. 微信状态机
+
+### 6.1 状态定义
+
+| 状态 | 说明 | 可转换状态 |
+|------|------|------------|
+| online | 微信正常在线(默认/成功状态) | offline |
+| offline | 微信掉线或不可用 | online |
+
+### 6.2 状态转换图
+
+```
+ ┌──────────────┐
+ │ online │
+ │ (默认/成功) │
+ └──────┬───────┘
+ │
+ 发送消息失败
+ 或微信掉线检测
+ │
+ ▼
+ ┌──────────────┐
+ │ offline │
+ └──────┬───────┘
+ │
+ 回调重置成功
+ (文件传输助手检测)
+ │
+ ▼
+ ┌──────────────┐
+ │ online │
+ └──────────────┘
+```
+
+### 6.3 状态转换场景
+
+| 场景 | 原状态 | 新状态 | 触发条件 |
+|------|--------|--------|----------|
+| 消息发送成功 | online | online | code == 0 |
+| 消息发送失败 | online | offline | code != 0 |
+| 微信掉线 | online | offline | 监控检测到异常 |
+| 回调重置成功 | offline | online | 检测消息发送成功 |
+| 回调重置失败 | offline | offline | 检测消息发送失败 |
+
+---
+
+## 7. 日志发布订阅流程
+
+### 7.1 流程图
+
+```
+业务操作调用
+ │
+ ├──► log_service.info(message, source)
+ ├──► log_service.warning(message, source)
+ ├──► log_service.error(message, source)
+ └──► log_service.success(message, source)
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ LogEntry 创建 │
+│ {timestamp, level, source, message} │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ├──► 存入 deque (本地缓存,最多 500 条)
+ │
+ └──► 推送到 SSE Queue
+ │
+ ▼
+ 所有订阅者收到消息
+ │
+ ▼
+ unsubscribe() 取消订阅
+```
+
+### 7.2 SSE 订阅示例
+
+```javascript
+// 前端订阅日志流
+const eventSource = new EventSource('/api/v1/logs/stream');
+
+eventSource.onmessage = (event) => {
+ const logEntry = JSON.parse(event.data);
+ console.log(`[${logEntry.level}] ${logEntry.message}`);
+};
+
+eventSource.onerror = (error) => {
+ console.error('SSE Error:', error);
+ eventSource.close();
+};
+```
+
+### 7.3 日志级别
+
+| 级别 | 说明 | 使用场景 |
+|------|------|----------|
+| INFO | 信息 | 一般操作日志 |
+| WARNING | 警告 | 异常但可恢复 |
+| ERROR | 错误 | 操作失败 |
+| SUCCESS | 成功 | 操作成功确认 |
+
+---
+
+## 8. 批量操作流程
+
+### 8.1 广播消息流程
+
+```
+广播请求
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ POST /api/v1/messages/broadcast │
+│ 请求体: {node_ids, who, msg, msg_type} │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+node_ids 为空?
+ │
+ ├──► 是 → 获取所有已启用节点
+ │
+ ▼ (否)
+使用指定的 node_ids
+ │
+ ▼
+对每个节点并行发送消息
+ │
+ ├──► 节点1 → send_message() → 结果1
+ ├──► 节点2 → send_message() → 结果2
+ └──► 节点N → send_message() → 结果N
+ │
+ ▼
+ 汇总结果
+ │
+ ▼
+ 返回批量结果
+```
+
+### 8.2 批量发送流程
+
+```
+批量请求
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ POST /api/v1/messages/batch │
+│ 请求体: {messages: [{node_id, who, msg, msg_type}, ...]} │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+验证消息列表
+ │
+ ▼
+对每条消息并行发送
+ │
+ ├──► 消息1 → send_message() → 结果1
+ ├──► 消息2 → send_message() → 结果2
+ └──► 消息N → send_message() → 结果N
+ │
+ ▼
+ 汇总结果
+ │
+ ▼
+ 返回批量结果
+```
+
+---
+
+## 9. 插件执行流程
+
+### 9.1 外部触发流程
+
+```
+外部请求
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ POST /api/v1/external/trigger │
+│ 请求体: {source, event, data, auto_send, target, node_id} │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+获取插件
+ │
+ ▼
+执行插件
+ │
+ ├──► plugin.execute(event, data)
+ │
+ ▼
+auto_send = true?
+ │
+ ├──► 是 → 发送消息到 target
+ │ │
+ │ ▼
+ │ 返回 {plugin_result, send_result}
+ │
+ └──► 否 → 返回 {plugin_result}
+```
+
+---
+
+## 10. 完整状态流转图
+
+```
+ ┌─────────────────────────────────────┐
+ │ 系统启动 │
+ │ 节点创建 (wechat_status=online) │
+ └─────────────────┬───────────────────┘
+ │
+ ▼
+ ┌─────────────────────────────────────┐
+ │ 正常运行 │
+ │ wechat_status=online │
+ └─────────────────┬───────────────────┘
+ │
+ ┌─────────────────┴─────────────────┐
+ │ │
+ ▼ ▼
+ ┌───────────────────────┐ ┌───────────────────────┐
+ │ 消息发送成功 │ │ 消息发送失败 │
+ │ wechat_status= │ │ wechat_status= │
+ │ online │ │ offline │
+ └───────────────────────┘ └───────────┬───────────┘
+ │
+ ▼
+ ┌───────────────────────┐
+ │ 调用回调重置接口 │
+ │ POST /callback/node/ │
+ │ reset │
+ └───────────┬───────────┘
+ │
+ ┌───────────────┴───────────────┐
+ │ │
+ ▼ ▼
+ ┌───────────────────────┐ ┌───────────────────────┐
+ │ 重置成功 │ │ 重置失败 │
+ │ 发送检测消息成功 │ │ 发送检测消息失败 │
+ │ wechat_status= │ │ wechat_status= │
+ │ online │ │ offline │
+ └───────────────────────┘ └───────────────────────┘
+ │
+ ▼
+ ┌───────────────────────┐
+ │ 触发 Webhook 告警 │
+ │ wechat_offline │
+ └───────────────────────┘
+```
+
+---
+
+## 更新日志
+
+| 日期 | 更新内容 |
+|------|----------|
+| 2026-04-05 | 创建 FLOWS.md 业务流程文档 |
+| 2026-04-05 | 添加消息发送流程图 |
+| 2026-04-05 | 添加回调重置流程图 |
+| 2026-04-05 | 添加监控告警流程图 |
+| 2026-04-05 | 添加健康检测流程图 |
+| 2026-04-05 | 添加微信状态机说明 |
+| 2026-04-05 | 添加日志发布订阅流程图 |
\ No newline at end of file
diff --git a/doc/wxauto_center/MODULES.md b/doc/wxauto_center/MODULES.md
new file mode 100644
index 0000000..ba7f5fc
--- /dev/null
+++ b/doc/wxauto_center/MODULES.md
@@ -0,0 +1,615 @@
+# WXAuto Center 模块索引文档
+
+## 1. 模块概览
+
+WXAuto Center 采用分层架构,主要分为以下模块:
+
+| 层级 | 模块 | 路径 | 职责 |
+|------|------|------|------|
+| 接入层 | API Routes | `api/` | 处理 HTTP 请求,提供 RESTful 接口 |
+| 业务逻辑层 | Services | `services/` | 核心业务逻辑处理 |
+| 公共工具层 | Utils | `utils/` | 公共函数和依赖注入 |
+| 插件系统 | Plugins | `plugins/` | 扩展功能插件 |
+| 配置层 | Config | `config.py` | 系统配置管理 |
+| 入口 | Main | `main.py` | FastAPI 应用入口 |
+
+---
+
+## 2. 模块详细说明
+
+### 2.1 接入层 (API Layer)
+
+#### api/nodes.py - 节点管理 API
+
+**文件路径**:[nodes.py](../../wxauto_center/api/nodes.py)
+
+**职责**:
+- 节点的增删改查 (CRUD)
+- 节点状态检测
+- 微信初始化
+- 节点启用/禁用
+- 获取好友列表、群列表、聊天消息
+
+**主要端点**:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /nodes | 获取所有节点 |
+| GET | /nodes/{node_id} | 获取指定节点 |
+| POST | /nodes | 添加节点 |
+| PUT | /nodes/{node_id} | 更新节点 |
+| DELETE | /nodes/{node_id} | 删除节点 |
+| POST | /nodes/{node_id}/enable | 启用节点 |
+| POST | /nodes/{node_id}/disable | 禁用节点 |
+| GET | /nodes/{node_id}/status | 检查节点状态 |
+| GET | /nodes/{node_id}/wechat/status | 获取微信状态 |
+| POST | /nodes/{node_id}/wechat/initialize | 初始化微信 |
+
+**依赖**:`services.node_manager`, `utils.require_api_key`
+
+**节点标识说明**:所有接口使用 `node_id` 作为节点唯一标识。
+
+---
+
+#### api/messages.py - 消息管理 API
+
+**文件路径**:[messages.py](../../wxauto_center/api/messages.py)
+
+**职责**:
+- 发送消息
+- 发送群消息
+- 批量发送消息
+- 发送模板消息
+- 获取消息历史
+
+**主要端点**:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /messages/send | 发送消息 |
+| POST | /messages/send_group | 发送群消息 |
+| POST | /messages/batch | 批量发送 |
+| POST | /messages/template/{node_id}/{who} | 发送模板消息 |
+| GET | /messages/history/{node_id}/{chat_name} | 获取消息历史 |
+
+**依赖**:`services.node_manager`, `utils.require_api_key`, `utils.check_node_health`
+
+**说明**:已移除广播功能。
+
+---
+
+#### api/external.py - 外部扩展 API
+
+**文件路径**:[external.py](../../wxauto_center/api/external.py)
+
+**职责**:
+- 供外部系统调用的接口
+- 外部消息发送
+- 数据处理触发
+- 插件执行
+- AI 消息处理
+- Webhook 接收
+
+**主要端点**:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /external/send | 外部接口-发送消息 |
+| POST | /external/trigger | 外部接口-触发数据处理 |
+| POST | /external/plugin/execute | 外部接口-执行插件 |
+| GET | /external/plugins | 外部接口-获取插件列表 |
+| GET | /external/nodes | 外部接口-获取可用节点 |
+| POST | /external/ai/process | 外部接口-AI处理消息 |
+| POST | /external/webhook/{event} | 外部接口-Webhook接收 |
+
+**认证方式**:`X-ExternalKey` Header
+
+**依赖**:`services.node_manager`, `plugins.base.PluginRegistry`, `utils.require_external_key`
+
+---
+
+#### api/plugins.py - 插件管理 API
+
+**文件路径**:[plugins.py](../../wxauto_center/api/plugins.py)
+
+**职责**:
+- 插件注册和管理
+- 插件启用/禁用
+- 插件配置更新
+- 按类型获取插件
+
+**主要端点**:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | /plugins | 获取所有插件 |
+| GET | /plugins/{plugin_name} | 获取插件详情 |
+| POST | /plugins/{plugin_name}/enable | 启用插件 |
+| POST | /plugins/{plugin_name}/disable | 禁用插件 |
+| PUT | /plugins/{plugin_name}/config | 更新插件配置 |
+| POST | /plugins/{plugin_name}/execute | 执行插件 |
+| GET | /plugins/type/{plugin_type} | 按类型获取插件 |
+
+**依赖**:`plugins.base.PluginRegistry`, `utils.require_api_key`
+
+---
+
+#### api/callback.py - 回调接口 API
+
+**文件路径**:[callback.py](../../wxauto_center/api/callback.py)
+
+**职责**:
+- 外部节点回调
+- 节点微信状态重置
+- 节点微信状态检测
+- 节点状态查询
+
+**主要端点**:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /callback/node/reset | 重置节点微信状态 |
+| POST | /callback/node/check | 检测节点微信状态 |
+| GET | /callback/node/{node_id}/status | 获取节点回调状态 |
+
+**业务流程**:
+```
+1. 发送消息 → 失败 → wechat_status = offline
+2. 回调重置接口 → 发送检测消息 → 成功 → wechat_status = online
+3. 监控服务检测状态变化 → 触发Webhook告警
+```
+
+**依赖**:`services.node_manager`, `services.log_service`, `utils.require_api_key`
+
+---
+
+### 2.2 业务逻辑层 (Service Layer)
+
+#### services/node_manager.py - 节点管理服务
+
+**文件路径**:[node_manager.py](../../wxauto_center/services/node_manager.py)
+
+**职责**:
+- 节点注册和注销
+- 节点通信管理
+- API 调用封装
+- 消息发送
+- 节点状态检测
+
+**核心类**:
+
+| 类名 | 说明 |
+|------|------|
+| NodeStatus | 节点状态枚举 (ACTIVE, INACTIVE, ERROR) |
+| Node | 节点数据模型(包含 node_id 和 name) |
+| NodeManager | 节点管理服务类 |
+
+**主要方法**:
+
+| 方法 | 说明 |
+|------|------|
+| add_node() | 添加节点 |
+| remove_node(node_id) | 删除节点 |
+| get_node(node_id) | 获取节点 |
+| get_all_nodes() | 获取所有节点 |
+| get_enabled_nodes() | 获取已启用节点 |
+| check_node_status(node_id) | 检测节点状态 |
+| is_node_healthy(node_id) | 判断节点是否健康 |
+| call_node_api() | 调用节点 API |
+| send_message(node_id, who, msg, msg_type) | 发送消息 |
+| send_message_to_group(node_id, group_name, msg, msg_type) | 发送群消息 |
+
+**内部方法**:
+
+| 方法 | 说明 |
+|------|------|
+| _send_message_internal() | 发送消息的内部方法 (复用逻辑) |
+
+**依赖**:`services.log_service`
+
+---
+
+#### services/monitor_service.py - 监控服务
+
+**文件路径**:[monitor_service.py](../../wxauto_center/services/monitor_service.py)
+
+**职责**:
+- 定时检测节点状态
+- 检测 API 状态变化
+- 检测微信状态变化
+- 触发 Webhook 告警
+
+**主要方法**:
+
+| 方法 | 说明 |
+|------|------|
+| start() | 启动监控服务 |
+| stop() | 停止监控服务 |
+| get_last_status() | 获取节点最后状态 |
+| get_all_status() | 获取所有节点状态 |
+
+**内部方法**:
+
+| 方法 | 说明 |
+|------|------|
+| _monitor_loop() | 监控主循环 |
+| _check_all_nodes() | 检测所有节点 |
+| _check_node() | 检测单个节点 |
+| _send_webhook() | 发送 Webhook 通知 |
+
+**依赖**:`config.get_settings`, `services.node_manager`, `services.log_service`
+
+---
+
+#### services/log_service.py - 日志服务
+
+**文件路径**:[log_service.py](../../wxauto_center/services/log_service.py)
+
+**职责**:
+- 日志记录
+- 日志缓存 (deque,最多 500 条)
+- SSE 实时推送
+- 发布订阅模式
+- Redis 缓存支持
+- 文件持久化 (logs/app.log)
+
+**日志级别**:INFO, WARNING, ERROR, SUCCESS
+
+**主要方法**:
+
+| 方法 | 说明 |
+|------|------|
+| info() | 记录 INFO 日志 |
+| warning() | 记录 WARNING 日志 |
+| error() | 记录 ERROR 日志 |
+| success() | 记录 SUCCESS 日志 |
+| subscribe() | 订阅日志流 |
+| unsubscribe() | 取消订阅 |
+| get_recent_logs() | 获取最近日志 |
+| get_all_logs() | 获取所有日志 |
+
+---
+
+#### services/queue_service.py - 消息队列服务
+
+**文件路径**:[queue_service.py](../../wxauto_center/services/queue_service.py)
+
+**职责**:
+- 消息队列管理
+- Redis 后端支持(可选)
+- 本地队列回退
+- 异步任务处理
+
+**队列名称**:
+
+| 队列名 | 说明 |
+|--------|------|
+| MESSAGE_SEND | 消息发送队列 |
+| MESSAGE_BROADCAST | 消息广播队列(已废弃) |
+
+**主要方法**:
+
+| 方法 | 说明 |
+|------|------|
+| enqueue() | 入队操作 |
+| dequeue() | 出队操作(异步) |
+| get_queue_length() | 获取队列长度 |
+| start_worker() | 启动工作进程 |
+| stop_worker() | 停止工作进程 |
+
+**依赖**:Redis (可选),若 Redis 不可用则使用本地 asyncio.Queue
+
+---
+
+### 2.3 公共工具层 (Utils Layer)
+
+#### utils/common.py - 公共函数
+
+**文件路径**:[common.py](../../wxauto_center/utils/common.py)
+
+**职责**:
+- API Key 验证依赖 (require_api_key)
+- External Key 验证依赖 (require_external_key)
+
+**主要函数**:
+
+| 函数名 | 说明 |
+|--------|------|
+| require_api_key | API Key 验证依赖,用于 X-API-Key Header |
+| require_external_key | External Key 验证依赖,用于 X-ExternalKey Header |
+
+**使用示例**:
+
+```python
+from utils import require_api_key
+
+@router.post("/endpoint")
+async def endpoint(_=Depends(require_api_key)):
+ # 处理请求
+ pass
+```
+
+---
+
+#### utils/helpers.py - 业务辅助函数
+
+**文件路径**:[helpers.py](../../wxauto_center/utils/helpers.py)
+
+**职责**:
+- 节点健康检查
+- 响应构建
+- 结果解析
+
+**主要函数**:
+
+| 函数名 | 说明 |
+|--------|------|
+| check_node_health() | 检查节点健康状态,不健康则抛 503 异常 |
+| build_success_response() | 构建成功响应 |
+| build_error_response() | 构建错误响应 |
+| parse_node_result() | 解析节点 API 调用结果 |
+
+---
+
+### 2.4 插件系统 (Plugin Layer)
+
+#### plugins/base.py - 插件基类
+
+**文件路径**:[base.py](../../wxauto_center/plugins/base.py)
+
+**职责**:
+- 定义插件接口
+- 插件注册表
+
+**核心类**:
+
+| 类名 | 说明 |
+|------|------|
+| PluginType | 插件类型枚举 |
+| PluginBase | 插件基类 |
+| MessageHandlerPlugin | 消息处理插件基类 |
+| DataSourcePlugin | 数据源插件基类 |
+| ActionTriggerPlugin | 动作触发插件基类 |
+| AIAgentPlugin | AI 智能体插件基类 |
+| PluginRegistry | 插件注册表 |
+
+**PluginType 枚举值**:
+- MESSAGE_HANDLER = "message_handler"
+- DATA_SOURCE = "data_source"
+- ACTION_TRIGGER = "action_trigger"
+- AI_AGENT = "ai_agent"
+- CUSTOM = "custom"
+
+---
+
+#### plugins/builtin.py - 内置插件
+
+**文件路径**:[builtin.py](../../wxauto_center/plugins/builtin.py)
+
+**内置插件**:
+
+| 插件名 | 类名 | 说明 |
+|--------|------|------|
+| http_data_source | HTTPDataSourcePlugin | HTTP 数据源插件,从外部 API 获取数据 |
+| database_query | DatabaseQueryPlugin | 数据库查询插件 (预留) |
+| webhook_trigger | WebhookTriggerPlugin | Webhook 触发器 |
+| simple_ai_agent | SimpleAIAgentPlugin | 简单 AI 智能体 |
+
+**注册函数**:`register_builtin_plugins()`
+
+---
+
+### 2.5 配置层 (Config Layer)
+
+#### config.py - 配置管理
+
+**文件路径**:[config.py](../../wxauto_center/config.py)
+
+**职责**:
+- 系统配置管理
+- 配置文件读写
+- 多环境支持
+
+**核心配置类**:
+
+| 类名 | 说明 |
+|------|------|
+| NodeConfig | 节点配置 |
+| PluginConfig | 插件配置 |
+| WebhookConfig | Webhook 配置 |
+| MonitorConfig | 监控配置 |
+| Settings | 应用设置 (继承 BaseSettings) |
+| ConfigManager | 配置管理器 |
+
+**主要函数**:
+
+| 函数名 | 说明 |
+|--------|------|
+| get_settings() | 获取全局设置单例 |
+| update_settings() | 更新设置 |
+| load_config_from_file() | 从文件加载配置 |
+| save_config_to_file() | 保存配置到文件 |
+
+**ConfigManager 方法**:
+
+| 方法 | 说明 |
+|------|------|
+| get_node() | 获取节点配置 |
+| get_all_nodes() | 获取所有节点配置 |
+| get_enabled_nodes() | 获取已启用节点配置 |
+| add_node() | 添加节点配置 |
+| update_node() | 更新节点配置 |
+| remove_node() | 删除节点配置 |
+| save_config() | 保存配置 |
+| load_config() | 加载配置 |
+
+---
+
+### 2.6 入口 (Main)
+
+#### main.py - 应用入口
+
+**文件路径**:[main.py](../../wxauto_center/main.py)
+
+**职责**:
+- FastAPI 应用初始化
+- 中间件配置
+- 路由注册
+- 启动/关闭事件处理
+- 配置管理 API
+- 监控状态 API
+- 日志流 API
+
+---
+
+## 3. 模块依赖关系
+
+```
+ ┌─────────────┐
+ │ main.py │
+ └──────┬──────┘
+ │
+ ┌─────────────────┼─────────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
+ │ config │ │ api/* │ │ services│
+ └────┬────┘ └────┬────┘ └────┬────┘
+ │ │ │
+ │ ▼ ▼
+ │ ┌───────────┐ ┌─────────┐
+ │ │ utils/* │ │plugins/*│
+ │ └───────────┘ └─────────┘
+ │ │ │
+ └─────────────────┼─────────────────┘
+ │
+ ▼
+ ┌─────────────┐
+ │ external │
+ │ (WXAuto-HTTP │
+ │ -API) │
+ └─────────────┘
+```
+
+**依赖详解**:
+
+| 模块 | 被依赖 | 依赖 |
+|------|--------|------|
+| main.py | - | config, api, services, plugins |
+| config.py | api, services, main | pydantic, pydantic_settings |
+| api/* | main | services, plugins, utils, config |
+| services/* | api | config, log_service |
+| utils/* | api | config |
+| plugins/* | api, main | httpx |
+| log_service | services | - |
+
+---
+
+## 4. 代码导览
+
+### 4.1 新增代码路径
+
+重构后新增的公共模块:
+
+| 文件 | 说明 |
+|------|------|
+| [utils/__init__.py](../../wxauto_center/utils/__init__.py) | 工具模块导出 |
+| [utils/common.py](../../wxauto_center/utils/common.py) | API Key 验证依赖 |
+| [utils/helpers.py](../../wxauto_center/utils/helpers.py) | 业务辅助函数 |
+
+### 4.2 重构变更
+
+#### 删除的重复代码
+
+以下文件中删除了重复定义的 `require_api_key` 函数:
+
+- `api/nodes.py` - 现在从 `utils` 导入
+- `api/messages.py` - 现在从 `utils` 导入
+- `api/external.py` - 现在从 `utils` 导入
+- `api/plugins.py` - 现在从 `utils` 导入
+- `api/callback.py` - 现在从 `utils` 导入
+
+#### 重构的方法
+
+| 文件 | 原方法 | 新方法 |
+|------|--------|--------|
+| services/node_manager.py | send_message() 和 send_message_to_group() 重复代码 | 新增 _send_message_internal() 内部方法 |
+| api/callback.py | reset_node_wechat_status() 和 check_node_wechat_status() 重复逻辑 | 新增 _check_wechat_and_update_status() 公共函数 |
+
+### 4.3 代码审计指南
+
+#### 审计要点
+
+1. **API Key 验证**
+ - 所有 API 端点必须使用 `Depends(require_api_key)` 或 `Depends(require_external_key)`
+ - 检查 `X-API-Key` 和 `X-ExternalKey` 是否正确使用
+
+2. **节点健康检查**
+ - 发送消息前必须调用 `check_node_health()`
+ - 使用 `utils.check_node_health` 而非直接调用 `node_manager.is_node_healthy`
+
+3. **配置保存**
+ - 修改节点配置后必须调用 `save_nodes_config()` 保存
+ - 修改全局配置后必须调用 `save_config_to_file()` 保存
+
+4. **错误处理**
+ - 使用 `build_success_response()` 和 `build_error_response()` 构建响应
+ - 节点不存在时抛出 `HTTPException(status_code=404)`
+
+5. **异步处理**
+ - 所有 `node_manager` 方法都是异步的,使用 `await`
+ - API 端点需要 `async def`
+
+---
+
+## 5. 模块索引
+
+### 5.1 文件快速查找
+
+| 功能 | 文件路径 |
+|------|----------|
+| API Key 验证 | [utils/common.py](../../wxauto_center/utils/common.py) |
+| 节点健康检查 | [utils/helpers.py](../../wxauto_center/utils/helpers.py) |
+| 节点管理 | [services/node_manager.py](../../wxauto_center/services/node_manager.py) |
+| 状态监控 | [services/monitor_service.py](../../wxauto_center/services/monitor_service.py) |
+| 日志服务 | [services/log_service.py](../../wxauto_center/services/log_service.py) |
+| 节点 API | [api/nodes.py](../../wxauto_center/api/nodes.py) |
+| 消息 API | [api/messages.py](../../wxauto_center/api/messages.py) |
+| 外部 API | [api/external.py](../../wxauto_center/api/external.py) |
+| 插件 API | [api/plugins.py](../../wxauto_center/api/plugins.py) |
+| 回调 API | [api/callback.py](../../wxauto_center/api/callback.py) |
+| 插件基类 | [plugins/base.py](../../wxauto_center/plugins/base.py) |
+| 内置插件 | [plugins/builtin.py](../../wxauto_center/plugins/builtin.py) |
+| 配置管理 | [config.py](../../wxauto_center/config.py) |
+| 应用入口 | [main.py](../../wxauto_center/main.py) |
+
+### 5.2 类和函数查找
+
+| 类/函数 | 文件 | 行号 |
+|---------|------|------|
+| NodeStatus | [node_manager.py](../../wxauto_center/services/node_manager.py#L15) | L15 |
+| Node | [node_manager.py](../../wxauto_center/services/node_manager.py#L21) | L21 |
+| NodeManager | [node_manager.py](../../wxauto_center/services/node_manager.py#L46) | L46 |
+| MonitorService | [monitor_service.py](../../wxauto_center/services/monitor_service.py#L18) | L18 |
+| LogService | [log_service.py](../../wxauto_center/services/log_service.py#L14) | L14 |
+| PluginBase | [base.py](../../wxauto_center/plugins/base.py#L21) | L21 |
+| PluginRegistry | [base.py](../../wxauto_center/plugins/base.py#L92) | L92 |
+| require_api_key | [common.py](../../wxauto_center/utils/common.py#L10) | L10 |
+| require_external_key | [common.py](../../wxauto_center/utils/common.py#L30) | L30 |
+| check_node_health | [helpers.py](../../wxauto_center/utils/helpers.py#L20) | L20 |
+| ConfigManager | [config.py](../../wxauto_center/config.py#L148) | L148 |
+
+---
+
+## 6. 更新日志
+
+| 日期 | 更新内容 |
+|------|----------|
+| 2026-04-05 | 更新 API 路径,将 node_name 替换为 node_id |
+| 2026-04-05 | 重构代码,新增 utils 模块,抽取公共函数 |
+| 2026-04-05 | 新增 MODULES.md 模块索引文档 |
+| 2026-04-05 | 更新 API.md,添加回调接口文档 |
+| 2026-04-05 | 更新 ARCHITECTURE.md,添加回调流程图和微信状态机 |
+| 2026-04-05 | 新增 QueueService 消息队列服务模块 |
+| 2026-04-05 | 更新 LogService 支持 Redis 缓存和文件持久化 |
+| 2026-04-05 | 移除广播功能相关文档 |
\ No newline at end of file
diff --git a/doc/wxauto_center/PLUGIN.md b/doc/wxauto_center/PLUGIN.md
new file mode 100644
index 0000000..e586ece
--- /dev/null
+++ b/doc/wxauto_center/PLUGIN.md
@@ -0,0 +1,615 @@
+# ///
+# PLUGIN.md
+# 描述:插件系统开发指南
+# 作者:User
+# 创建日期:2026-04-05
+# 更新日期:2026-04-07
+# ///
+
+# WXAuto Center 插件系统开发指南
+
+## 目录
+
+1. [概述](#概述)
+2. [插件类型](#插件类型)
+3. [创建插件](#创建插件)
+4. [表单化配置系统](#表单化配置系统)
+5. [插件通知系统](#插件通知系统)
+6. [热加载插件](#热加载插件)
+7. [内置插件](#内置插件)
+8. [高级多节点推送插件](#高级多节点推送插件)
+9. [插件管理API](#插件管理api)
+10. [前端使用](#前端使用)
+11. [最佳实践](#最佳实践)
+12. [目录结构](#目录结构)
+13. [生命周期](#生命周期)
+
+---
+
+## 概述
+
+WXAuto Center 采用模块化插件架构,支持热插拔功能扩展。开发者可以创建自定义插件来处理消息、数据源、定时任务等。
+
+### 核心组件
+
+| 组件 | 说明 |
+|------|------|
+| `PluginBase` | 所有插件的基类 |
+| `ScheduledTaskPlugin` | 定时任务插件基类(含通知功能) |
+| `PluginRegistry` | 插件注册表,管理所有插件 |
+| `PluginType` | 插件类型枚举 |
+| `PluginConfigSchema` | 表单化配置Schema系统 |
+| `HotReloadPluginLoader` | 热加载服务,监控插件目录变化 |
+| `plugin_notification_service` | 插件通知服务 |
+
+---
+
+## 插件类型
+
+| 类型 | 值 | 说明 |
+|------|-----|------|
+| `MESSAGE_HANDLER` | `message_handler` | 消息处理器,用于自动回复、消息过滤 |
+| `DATA_SOURCE` | `data_source` | 数据源,用于获取天气、新闻、股票等 |
+| `ACTION_TRIGGER` | `action_trigger` | 动作触发器,用于Webhook回调、定时任务 |
+| `AI_AGENT` | `ai_agent` | AI智能体,用于LLM对话、智能回复 |
+| `SCHEDULED_TASK` | `scheduled_task` | 定时任务,执行周期性操作 |
+| `HTTP_SCHEDULED_SENDER` | `http_scheduled_sender` | 定时HTTP推送,定时请求API并发送结果 |
+| `CUSTOM` | `custom` | 自定义类型 |
+
+---
+
+## 创建插件
+
+### 基础结构
+
+```python
+# ///
+# my_plugin.py
+# 描述:我的自定义插件
+# ///
+
+from plugins.base import PluginBase, PluginType
+
+class MyPlugin(PluginBase):
+ plugin_name = "my_plugin"
+ plugin_version = "1.0.0"
+ plugin_type = PluginType.CUSTOM
+ plugin_description = "我的自定义插件"
+ plugin_author = "开发者名称"
+
+ def initialize(self, config: dict) -> bool:
+ self.config = config
+ return True
+
+ def execute(self, params: dict) -> dict:
+ return {"success": True, "result": "done"}
+```
+
+### 定时任务插件
+
+```python
+# ///
+# my_scheduled_plugin.py
+# 描述:定时任务插件示例
+# ///
+
+from plugins.base import ScheduledTaskPlugin
+from plugins.plugin_config_schema import (
+ PluginConfigSchema, ConfigSection, ConfigField, FieldType
+)
+
+def get_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("my_scheduled_plugin")
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(
+ name="cron",
+ label="执行周期",
+ field_type=FieldType.CRON,
+ default="*/5 * * * *"
+ ),
+ ConfigField(
+ name="enabled",
+ label="启用插件",
+ field_type=FieldType.BOOLEAN,
+ default=False
+ )
+ ]
+ ))
+ return schema
+
+class MyScheduledPlugin(ScheduledTaskPlugin):
+ plugin_name = "my_scheduled_plugin"
+ plugin_version = "1.0.0"
+ plugin_description = "定时任务插件"
+ plugin_author = "开发者"
+ config_schema = staticmethod(get_config_schema)
+
+ def get_cron_expression(self) -> str:
+ return self.cron_expression
+
+ def should_run_and_get_next(self) -> tuple:
+ return True, None
+
+ def run_task(self) -> dict:
+ self.notify("任务完成", "定时任务执行成功")
+ return {"success": True}
+```
+
+---
+
+## 表单化配置系统
+
+### 字段类型
+
+| 类型 | 值 | 说明 | 对应UI |
+|------|-----|------|--------|
+| `STRING` | `string` | 文本输入 | input[type=text] |
+| `PASSWORD` | `password` | 密码输入 | input[type=password] |
+| `NUMBER` | `number` | 数字输入 | input[type=number] |
+| `BOOLEAN` | `boolean` | 开关 | checkbox |
+| `SELECT` | `select` | 下拉选择 | select |
+| `TEXTAREA` | `textarea` | 多行文本 | textarea |
+| `CRON` | `cron` | Cron表达式 | input[type=text] |
+
+### 配置示例
+
+```python
+from plugins.plugin_config_schema import (
+ PluginConfigSchema, ConfigSection, ConfigField, FieldType
+)
+
+def get_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("my_plugin")
+
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(
+ name="cron",
+ label="执行周期",
+ field_type=FieldType.CRON,
+ required=True,
+ default="*/5 * * * *",
+ description="Cron表达式,如 */5 * * * *"
+ ),
+ ConfigField(
+ name="node_id",
+ label="节点ID",
+ field_type=FieldType.STRING,
+ required=True,
+ default="wx1"
+ ),
+ ConfigField(
+ name="receiver",
+ label="接收人",
+ field_type=FieldType.STRING,
+ required=True
+ ),
+ ConfigField(
+ name="timeout",
+ label="超时时间",
+ field_type=FieldType.NUMBER,
+ default=15,
+ min_value=5,
+ max_value=60
+ ),
+ ConfigField(
+ name="enabled",
+ label="启用插件",
+ field_type=FieldType.BOOLEAN,
+ default=False
+ )
+ ]
+ ))
+
+ return schema
+```
+
+### API 调用
+
+```bash
+# 获取插件表单Schema
+curl http://localhost:8080/api/v1/plugins/{plugin_name}/schema \
+ -H "X-API-Key: your-external-api-key"
+
+# 响应示例
+{
+ "success": true,
+ "schema": {
+ "plugin_name": "my_plugin",
+ "sections": [
+ {
+ "name": "basic",
+ "label": "基本设置",
+ "fields": [
+ {"name": "cron", "label": "执行周期", "type": "cron", ...}
+ ]
+ }
+ ]
+ },
+ "has_form": true
+}
+```
+
+---
+
+## 插件通知系统
+
+所有继承 `ScheduledTaskPlugin` 的插件都可以使用通知功能。
+
+### 使用方法
+
+```python
+# 普通通知
+self.notify(title, message, level)
+
+# 快捷方法
+self.notify_error(title, message) # 错误通知
+self.notify_warning(title, message) # 警告通知
+self.notify_success(title, message) # 成功通知
+```
+
+### 配置接收
+
+在告警配置中添加 Webhook,勾选 **"插件通知"** 事件类型:
+
+```bash
+curl -X POST "http://localhost:8080/api/v1/webhook/" \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "插件通知",
+ "url": "https://api.day.app/your-bark-key",
+ "format": "bark",
+ "event_types": ["plugin_notification"]
+ }'
+```
+
+### 事件类型
+
+| 事件类型 | 说明 |
+|----------|------|
+| `api_error` | API错误 |
+| `wechat_offline` | 微信离线 |
+| `wechat_online` | 微信上线 |
+| `node_offline` | 节点离线 |
+| `node_online` | 节点上线 |
+| `plugin_notification` | 插件通知 |
+
+---
+
+## 热加载插件
+
+### 工作原理
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ HotReloadPluginLoader (独立线程) │
+│ │
+│ 1. watchdog 监控 /app/plugins/custom 目录 │
+│ 2. 检测到 .py 文件变化 → 自动重新加载 │
+│ 3. importlib 动态导入模块 │
+│ 4. 自动注册到 PluginRegistry │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 插件目录
+
+- **Docker 内路径**: `/app/plugins/custom`
+- **宿主机路径**: `./plugins/custom`
+
+### 挂载配置
+
+```yaml
+# docker-compose.yml
+volumes:
+ - ./plugins/custom:/app/plugins/custom # 双向同步
+```
+
+---
+
+## 高级多节点推送插件
+
+### 简介
+
+`advanced_sender` 是高级多节点数据推送插件,支持:
+- 多数据源独立配置
+- 每数据源独立节点和接收人设置
+- 外部 MySQL 数据库去重
+- 班次时段路由
+- Cookie 失效检测和通知
+- 熔断保护机制
+
+### 表单配置
+
+```
+默认设置(可被数据源覆盖)
+├── 默认节点ID: wx1
+└── 默认接收人: asq
+
+数据源1
+├── Cookie: [文本框]
+├── 节点ID: [留空使用默认]
+└── 接收人: [留空使用默认]
+
+数据源2
+├── Cookie: [文本框]
+├── 节点ID: [留空使用默认]
+└── 接收人: [留空使用默认]
+
+外部数据库
+├── 数据库地址: 192.168.2.27
+├── 端口: 3306
+├── 用户名: root2
+├── 密码: ****
+├── 数据库名: addb
+└── 表名: user_data
+
+抓取设置
+├── 请求超时(秒): 15
+└── 数据源间隔(秒): 20
+```
+
+### 配置参数
+
+| 参数 | 说明 | 默认值 |
+|------|------|--------|
+| `cron` | 执行周期 | `*/2 * * * *` |
+| `enable_cron` | 启用定时任务 | false |
+| `node_id` | 默认节点ID | wx1 |
+| `receiver` | 默认接收人 | asq |
+| `cookie_1` | 数据源1 Cookie | - |
+| `cookie_2` | 数据源2 Cookie | - |
+| `node_id_1` | 数据源1节点ID(留空用默认) | - |
+| `receiver_1` | 数据源1接收人(留空用默认) | - |
+| `node_id_2` | 数据源2节点ID(留空用默认) | - |
+| `receiver_2` | 数据源2接收人(留空用默认) | - |
+| `db_host` | 数据库地址 | 192.168.2.27 |
+| `db_port` | 端口 | 3306 |
+| `db_user` | 用户名 | root2 |
+| `db_password` | 密码 | root@root |
+| `db_name` | 数据库名 | addb |
+| `db_table` | 表名 | user_data |
+| `fetch_timeout` | 请求超时(秒) | 15 |
+| `send_interval` | 数据源间隔(秒) | 20 |
+
+### 班次路由
+
+| 时段 | 路由规则 |
+|------|----------|
+| 0-7点 | 跳过 |
+| 8-16点 | 发送 |
+| 17-21点 | 发送 |
+| 22-23点 | 跳过 |
+
+### 操作
+
+| action | 说明 |
+|--------|------|
+| `send` | 执行完整流程 |
+| `test` | 测试配置 |
+| `test_send` | 测试发送(可指定node_id和receiver) |
+| `query_pending` | 查询待发送数据 |
+
+### 测试发送
+
+```bash
+# 使用默认节点和接收人
+curl -X POST "http://localhost:8080/api/v1/plugins/advanced_sender/execute" \
+ -H "X-API-Key: your-api-key" \
+ -d '{"action": "test_send", "msg": "测试消息"}'
+
+# 指定节点和接收人
+curl -X POST "http://localhost:8080/api/v1/plugins/advanced_sender/execute" \
+ -H "X-API-Key: your-api-key" \
+ -d '{"action": "test_send", "node_id": "wx1", "receiver": "asq", "msg": "测试"}'
+```
+
+### 获取日志
+
+```bash
+curl "http://localhost:8080/api/v1/plugins/advanced_sender/logs?lines=100" \
+ -H "X-API-Key: your-api-key"
+```
+
+### Cookie 失效检测
+
+- 当 HTTP 返回 403/401 时,判定为 Cookie 失效
+- 当数据源返回空数据时,判定为 Cookie 可能失效
+- 首次检测到问题发送告警通知
+- 恢复成功后发送成功通知
+- 后续执行不再重复报警
+
+---
+
+## 插件管理API
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `GET /api/v1/plugins/` | GET | 获取所有插件 |
+| `GET /api/v1/plugins/hotloaded` | GET | 获取热加载插件列表 |
+| `POST /api/v1/plugins/hotreload` | POST | 手动触发热重载 |
+| `GET /api/v1/plugins/{name}` | GET | 获取插件详情 |
+| `GET /api/v1/plugins/{name}/schema` | GET | 获取插件表单Schema |
+| `POST /api/v1/plugins/{name}/enable` | POST | 启用插件 |
+| `POST /api/v1/plugins/{name}/disable` | POST | 禁用插件 |
+| `PUT /api/v1/plugins/{name}/config` | PUT | 更新插件配置 |
+| `POST /api/v1/plugins/{name}/execute` | POST | 执行插件 |
+| `GET /api/v1/plugins/{name}/logs` | GET | 获取插件日志 |
+| `POST /api/v1/plugins/{name}/logs/clear` | POST | 清空插件日志 |
+
+### 配置插件示例
+
+```bash
+curl -X PUT "http://localhost:8080/api/v1/plugins/advanced_sender/config" \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "advanced_sender",
+ "config": {
+ "cron": "*/2 * * * *",
+ "enable_cron": true,
+ "node_id": "wx1",
+ "receiver": "asq",
+ "cookie_1": "PHPSESSID=xxx; think_var=zh-cn",
+ "cookie_2": "PHPSESSID=yyy; think_var=zh-cn",
+ "db_host": "192.168.2.27",
+ "db_port": 3306,
+ "db_user": "root2",
+ "db_password": "root@root",
+ "db_name": "addb",
+ "db_table": "user_data"
+ }
+ }'
+```
+
+### 执行插件示例
+
+```bash
+# 执行发送任务
+curl -X POST "http://localhost:8080/api/v1/plugins/advanced_sender/execute" \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"action": "send"}'
+
+# 测试配置
+curl -X POST "http://localhost:8080/api/v1/plugins/advanced_sender/execute" \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"action": "test"}'
+```
+
+---
+
+## 前端使用
+
+访问 **插件管理** 页面(侧边栏菜单):
+
+1. **查看插件列表** - 显示所有已注册插件
+2. **启用/禁用** - 点击按钮控制插件状态
+3. **配置** - 点击配置按钮打开表单化配置界面
+4. **日志** - 点击日志按钮查看插件独立日志
+5. **执行** - 点击执行按钮测试插件
+
+### 配置界面
+
+- 表单化配置,无需手动编辑 JSON
+- Section 可折叠,方便管理
+- 支持所有字段类型(文本、数字、密码、开关、下拉等)
+- 自动验证必填项
+
+### 日志界面
+
+- 实时显示插件日志
+- 支持刷新
+- 日志文件保存在 `/app/data/logs/{plugin_name}.log`
+
+---
+
+## 最佳实践
+
+### 1. 使用表单化配置
+
+```python
+from plugins.plugin_config_schema import (
+ PluginConfigSchema, ConfigSection, ConfigField, FieldType
+)
+
+def get_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("my_plugin")
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(name="key", label="键", field_type=FieldType.STRING, required=True)
+ ]
+ ))
+ return schema
+
+class MyPlugin(ScheduledTaskPlugin):
+ config_schema = staticmethod(get_config_schema)
+```
+
+### 2. 使用通知功能
+
+```python
+class MyPlugin(ScheduledTaskPlugin):
+ def run_task(self):
+ try:
+ # 业务逻辑
+ self.notify_success("任务完成", "发送了 10 条消息")
+ except Exception as e:
+ self.notify_error("任务失败", str(e))
+```
+
+### 3. 错误处理
+
+```python
+def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ try:
+ return {"success": True, "data": result}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+```
+
+### 4. 定时任务防重复
+
+```python
+def run_task(self) -> Dict[str, Any]:
+ if self._running:
+ logger.warning("Previous task still running, skipping")
+ return {"success": False, "error": "上一次执行尚未完成"}
+```
+
+---
+
+## 目录结构
+
+```
+wxauto_api/
+├── plugins/
+│ ├── __init__.py
+│ ├── base.py # 插件基类和注册表
+│ ├── builtin.py # 内置插件实现
+│ ├── plugin_loader.py # 热加载插件加载器
+│ ├── plugin_config_schema.py # 表单化配置系统
+│ ├── http_scheduled_sender.py # 定时HTTP推送插件
+│ ├── advanced_sender_plugin.py # 高级多节点推送插件
+│ └── custom/ # 自定义插件目录(热加载)
+│ ├── __init__.py
+│ └── my_plugin.py
+├── services/
+│ └── plugin_notification_service.py # 插件通知服务
+├── static/
+│ ├── js/
+│ │ └── app.js # 前端插件管理
+│ └── css/
+│ └── style.css
+└── doc/
+ └── wxauto_center/
+ └── PLUGIN.md # 本文档
+```
+
+---
+
+## 生命周期
+
+1. **注册** - `PluginRegistry.register(plugin)`
+2. **初始化** - `plugin.initialize(config)`
+3. **启用** - `plugin.enable()`
+4. **执行** - `plugin.execute(params)` 或 `plugin.run_task()`
+5. **禁用** - `plugin.disable()`
+6. **卸载** - `PluginRegistry.unregister(name)`
+
+### 热加载流程
+
+1. **启动** - `HotReloadPluginLoader.start()`
+2. **扫描** - 扫描 `/app/plugins/custom` 目录
+3. **监听** - `watchdog` 监控文件变化
+4. **加载** - `importlib` 动态导入模块
+5. **注册** - 自动注册到 `PluginRegistry`
+6. **停止** - `HotReloadPluginLoader.stop()`
diff --git a/doc/wxauto_center/README.md b/doc/wxauto_center/README.md
new file mode 100644
index 0000000..4b36adc
--- /dev/null
+++ b/doc/wxauto_center/README.md
@@ -0,0 +1,286 @@
+# WXAuto Center 中控系统
+
+## 项目简介
+
+WXAuto Center 是一款基于 FastAPI + PostgreSQL + Redis 构建的多节点微信管理中控系统,为企业级微信自动化运营提供集中管理平台。该系统能够同时管理多个 WXAuto-HTTP-API 节点,实现消息统一发送、节点状态监控、实时日志查看和外部程序接入等功能。
+
+## 主要功能
+
+- **多节点微信管理**:支持同时接入多个微信节点,按组分类管理
+- **节点标识系统**:每个节点拥有唯一的 `node_id`(如 "wx1", "node_001")和显示用的 `name`(如 "微信1", "测试节点")
+- **消息发送**:支持单发、群发、批量发送、模板消息等多种发送模式
+- **健康检测机制**:通过 `/api/wechat/initialize` 接口检测微信状态,确保消息发送前微信正常运行
+- **Webhook 告警**:支持企业微信(wechat)和 Bark 两种格式,节点/微信状态变化时自动通知
+- **实时日志**:通过 SSE(Server-Sent Events)技术实现日志实时推送,默认保留最近500条
+- **Redis 消息队列**:支持 Redis 后端的消息队列,用于异步消息处理
+- **多进程支持**:支持多 worker 部署,适应高并发场景
+- **外部程序接入**:提供标准 RESTful API,支持外部系统调用
+- **插件系统**:支持扩展插件,包括数据源、AI 智能体、Webhook触发器等
+
+## 快速开始
+
+### 环境要求
+
+- Python 3.8+
+- Redis (用于消息队列和多进程场景)
+- FastAPI
+- httpx(异步 HTTP 客户端)
+- uvicorn(ASGI 服务器)
+- pydantic
+- pydantic-settings
+
+### 安装步骤
+
+```bash
+# 进入项目目录
+cd wxauto_center
+
+# 安装依赖
+pip install -r requirements.txt
+
+# 复制配置示例文件
+cp .env.example .env
+
+# 编辑 .env 文件,修改必要的配置项
+```
+
+### 启动服务
+
+```bash
+# 单进程启动(默认)
+python run.py
+
+# 多进程启动(需要 Redis)
+python run.py --workers 4
+
+# 带参数启动
+python run.py --host 0.0.0.0 --port 8080 --workers 4
+
+# 开发模式(支持热重载,单进程)
+python run.py --reload
+
+# 调试模式
+python run.py --debug
+```
+
+**替代启动方式**:
+
+```bash
+# 使用 uvicorn 直接启动
+uvicorn main:app --host 0.0.0.0 --port 8080 --reload
+```
+
+### 访问地址
+
+- 主界面:http://localhost:8080/static/index.html
+- API 文档:http://localhost:8080/docs
+- ReDoc 文档:http://localhost:8080/redoc
+
+### 快速使用示例
+
+#### 1. 添加节点
+
+```bash
+curl -X POST http://localhost:8080/api/v1/nodes \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{
+ "node_id": "wx1",
+ "name": "微信1",
+ "api_url": "http://192.168.20.18:5000",
+ "api_key": "your-node-api-key",
+ "description": "生产节点",
+ "group": "wechat"
+ }'
+```
+
+**节点标识说明**:
+- `node_id`:节点唯一标识,如 "wx1", "node_001",用于 API 调用
+- `name`:节点显示名称,如 "微信1", "测试节点",用于界面展示
+
+#### 2. 发送消息
+
+```bash
+curl -X POST http://localhost:8080/api/v1/messages/send \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{
+ "node_id": "wx1",
+ "who": "wxid_friend",
+ "msg": "您好,这是一条测试消息",
+ "msg_type": "text"
+ }'
+```
+
+#### 3. 查询节点状态
+
+```bash
+curl http://localhost:8080/api/v1/nodes/wx1/status \
+ -H "X-API-Key: your-external-api-key"
+```
+
+#### 4. 重置微信状态(当微信掉线时)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/callback/node/reset \
+ -H "Content-Type: application/json" \
+ -H "X-API-Key: your-external-api-key" \
+ -d '{
+ "node_id": "wx1"
+ }'
+```
+
+## 项目结构
+
+```
+wxauto_api/
+├── main.py # FastAPI 主入口
+├── run.py # 启动脚本
+├── config.py # 配置管理
+├── config.json # 应用配置(不含节点和 Webhook)
+├── .env # Docker 环境变量配置
+├── models.py # 数据模型(Node, LogEntry, WebhookUrl)
+├── database_service.py # 数据库服务
+├── api/ # API 路由模块
+│ ├── __init__.py
+│ ├── nodes.py # 节点管理 API
+│ ├── messages.py # 消息发送 API
+│ ├── external.py # 外部扩展接口
+│ ├── callback.py # 回调接口
+│ ├── plugins.py # 插件管理 API
+│ └── webhook.py # Webhook 管理 API
+├── services/ # 业务逻辑服务
+│ ├── node_manager.py # 节点管理器
+│ ├── monitor_service.py # 监控服务
+│ └── log_service.py # 日志服务
+├── plugins/ # 插件系统
+│ ├── base.py # 插件基类
+│ └── builtin.py # 内置插件
+├── static/ # Web 静态资源
+│ ├── index.html # 主页面
+│ ├── css/style.css # 样式表
+│ └── js/ # JavaScript 模块
+├── scripts/ # 脚本目录
+│ ├── start_amd64.sh # amd64 启动脚本
+│ ├── start_arm64.sh # arm64 启动脚本
+│ └── database/ # 数据库脚本
+├── doc/ # 文档目录
+│ └── wxauto_center/ # 项目文档
+├── data/ # 数据目录
+│ ├── db/ # PostgreSQL 数据
+│ ├── redis/ # Redis 数据
+│ └── logs/ # 应用日志
+├── docker-compose.yml # Docker Compose 配置
+└── Dockerfile # Docker 构建文件
+```
+
+## 配置说明
+
+系统配置支持多种配置源,按优先级从高到低依次为:
+1. 环境变量(`.env` 文件)
+2. 配置文件(`config.json`)
+3. 代码默认值
+
+### 配置文件
+
+**config.json** - 应用配置(不含节点和 Webhook):
+
+```json
+{
+ "app_name": "WXAuto Center",
+ "host": "0.0.0.0",
+ "port": 8080,
+ "debug": true,
+ "secret_key": "wxauto-center-secret-key-change-in-production",
+ "workers": 1,
+ "cors_origins": ["*"],
+ "api_prefix": "/api/v1",
+ "external_api_key": "your-external-api-key",
+ "monitor": {
+ "enabled": true,
+ "check_interval": 30
+ }
+}
+```
+
+**.env** - Docker 环境变量配置:
+
+```
+DATABASE_URL=postgresql://wxauto:wxauto_password@postgres:5432/wxauto
+REDIS_URL=redis://redis:6379/0
+APP_NAME=WXAuto Center
+HOST=0.0.0.0
+PORT=8080
+DEBUG=true
+SECRET_KEY=wxauto-center-secret-key-change-in-production
+WORKERS=1
+API_PREFIX=/api/v1
+EXTERNAL_API_KEY=your-external-api-key
+CORS_ORIGINS=["*"]
+MONITOR_ENABLED=true
+MONITOR_CHECK_INTERVAL=30
+```
+
+### 节点配置
+
+节点配置存储在 PostgreSQL 的 `nodes` 表中,运行时从数据库加载。详细说明请参阅 [CONFIG.md](./CONFIG.md)。
+
+### Webhook 配置
+
+Webhook 配置存储在 PostgreSQL 的 `webhook_urls` 表中,支持企业微信和 Bark 两种格式。详细说明请参阅 [CONFIG.md](./CONFIG.md)。
+
+## API 文档
+
+系统提供完整的 RESTful API,包括:
+
+- **节点管理**:`/api/v1/nodes` - 节点的添加、删除、启用/禁用、状态检测
+- **消息发送**:`/api/v1/messages` - 单发、群发、批量发送、模板消息
+- **外部接口**:`/api/v1/external` - 供外部程序调用的接口
+- **回调接口**:`/api/v1/callback` - 节点状态重置、状态检测
+- **插件管理**:`/api/v1/plugins` - 插件的注册和调用
+- **配置管理**:`/api/v1/config/webhook`, `/api/v1/config/monitor` - Webhook和监控配置
+- **监控状态**:`/api/v1/monitor/status` - 实时监控状态
+- **日志服务**:`/api/v1/logs/*` - 日志查询和实时推送(SSE)
+
+详细 API 说明请参阅 [API.md](./API.md)
+
+## 业务流程
+
+系统主要业务流程包括消息发送流程、回调重置流程和监控告警流程。详细说明请参阅 [FLOWS.md](./FLOWS.md)。
+
+## 开发指南
+
+如需进行二次开发或扩展功能,请参阅 [DEVELOPMENT.md](./DEVELOPMENT.md),其中包含:
+
+- 开发环境搭建
+- 代码结构说明
+- 插件开发指南
+- API 调用示例
+
+## 常见问题
+
+如遇到问题,请参阅 [TROUBLESHOOTING.md](./TROUBLESHOOTING.md)
+
+## 核心设计理念
+
+### 1. 节点标识系统
+
+每个节点有两个标识:
+- `node_id`:节点唯一标识符,用于 API 调用和系统内部引用,示例:"wx1", "node_001"
+- `name`:节点显示名称,用于界面展示和用户友好提示,示例:"微信1", "测试节点"
+
+### 2. 健康检测机制
+
+每次发送消息前,系统会自动调用 `/api/wechat/initialize` 接口初始化微信,确保微信处于可发送消息的状态。这是官方推荐的正确流程。
+
+### 3. 监控告警
+
+监控服务定时检测所有节点和微信状态,当状态发生变化时自动触发Webhook通知。支持的事件类型包括:api_error、wechat_offline、wechat_online、node_offline、node_online。
+
+### 4. 日志服务
+
+采用内存存储+发布订阅模式,支持SSE实时推送。默认保留最近500条日志,足够应对日常调试和监控需求。
+
+## 许可证
+
+本项目仅供学习交流使用,请勿用于违规用途。
diff --git a/doc/wxauto_center/TROUBLESHOOTING.md b/doc/wxauto_center/TROUBLESHOOTING.md
new file mode 100644
index 0000000..b6ecb38
--- /dev/null
+++ b/doc/wxauto_center/TROUBLESHOOTING.md
@@ -0,0 +1,619 @@
+# WXAuto Center 常见问题与解决方案
+
+## 概述
+
+本文档收集了 WXAuto Center 使用过程中可能遇到的常见问题及其解决方案。
+
+---
+
+## 1. 服务启动问题
+
+### 1.1 端口被占用
+
+**问题描述**:
+
+```
+Error: [Errno 48] Address already in use
+```
+
+**可能原因**:
+- 另一个进程正在使用 8080 端口
+- 上一次启动的服务未正常关闭
+
+**解决方案**:
+
+```bash
+# 查找占用端口的进程
+lsof -i :8080 # macOS
+# 或
+netstat -tlnp | grep 8080 # Linux
+
+# 终止占用进程
+kill -9
+
+# 或使用其他端口启动
+# 修改 config.json 中 port 为其他值,如 8081
+```
+
+### 1.2 依赖库安装失败
+
+**问题描述**:
+
+```
+ModuleNotFoundError: No module named 'fastapi'
+```
+
+**解决方案**:
+
+```bash
+# 确保使用 Python 3.8+
+python --version
+
+# 升级 pip
+pip install --upgrade pip
+
+# 重新安装依赖
+pip install -r requirements.txt
+
+# 或使用国内镜像
+pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+```
+
+### 1.3 配置文件格式错误
+
+**问题描述**:
+
+```
+JSONDecodeError: Expecting property name enclosed in double quotes
+```
+
+**解决方案**:
+
+检查 `config.json` 文件是否为有效的 JSON 格式:
+
+```bash
+# 验证 JSON 格式
+python -c "import json; json.load(open('config.json'))"
+
+# 或使用在线 JSON 验证工具
+```
+
+---
+
+## 2. API 认证问题
+
+### 2.1 API Key 无效
+
+**问题描述**:
+
+```json
+{
+ "detail": "Invalid API Key"
+}
+```
+
+**可能原因**:
+- 请求时未提供 `X-API-Key` Header
+- 提供的 API Key 与配置不匹配
+
+**解决方案**:
+
+1. 确认在请求头中添加了正确的 Header:
+
+```bash
+curl -H "X-API-Key: your-external-api-key" http://localhost:8080/api/v1/nodes
+```
+
+2. 检查 `config.json` 或 `.env` 中的 `external_api_key` 配置:
+
+```json
+{
+ "external_api_key": "your-external-api-key"
+}
+```
+
+### 2.2 CORS 跨域问题
+
+**问题描述**:
+
+```
+Access-Control-Allow-Origin' header is present on the requested resource
+```
+
+**解决方案**:
+
+1. 在 `config.json` 中配置允许的来源:
+
+```json
+{
+ "cors_origins": ["http://localhost:3000", "https://your-domain.com"]
+}
+```
+
+2. 或设置 `cors_origins": ["*"]`(仅开发环境)
+
+---
+
+## 3. 节点管理问题
+
+### 3.1 节点添加失败
+
+**问题描述**:
+
+```json
+{
+ "success": false,
+ "error": "Node 'xxx' already exists"
+}
+```
+
+**解决方案**:
+
+- 节点名称具有唯一性,不能重复
+- 使用不同的名称添加节点
+- 或先删除已有节点后再添加
+
+```bash
+# 删除节点
+curl -X DELETE http://localhost:8080/api/v1/nodes/node1 \
+ -H "X-API-Key: your-api-key"
+```
+
+### 3.2 节点连接失败
+
+**问题描述**:
+
+```json
+{
+ "data": {
+ "is_healthy": false,
+ "health_message": "API status: error"
+ }
+}
+```
+
+**可能原因**:
+- 节点服务未启动
+- API URL 配置错误
+- 网络不通
+- API Key 不正确
+
+**解决方案**:
+
+1. 确认节点服务正在运行:
+
+```bash
+# 在节点服务器上检查
+curl http://节点IP:5000/health
+```
+
+2. 检查节点配置:
+
+```bash
+# 获取节点信息
+curl http://localhost:8080/api/v1/nodes/node1 \
+ -H "X-API-Key: your-api-key"
+```
+
+3. 测试节点 API 连通性:
+
+```bash
+# 直接测试节点 API
+curl -X POST http://节点IP:5000/api/wechat/initialize \
+ -H "X-API-Key: 节点API密钥"
+```
+
+### 3.3 节点健康检测失败
+
+**问题描述**:
+
+```json
+{
+ "health_message": "WeChat error: 无效"
+}
+```
+
+**可能原因**:
+- 微信窗口未打开或已关闭
+- 微信版本不兼容
+- WXAuto-HTTP-API 服务异常
+
+**解决方案**:
+
+1. 在对应节点上手动初始化微信:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/nodes/node1/wechat/initialize \
+ -H "X-API-Key: your-api-key"
+```
+
+2. 检查节点服务器的微信客户端是否正常运行
+
+3. 重启节点服务
+
+---
+
+## 4. 消息发送问题
+
+### 4.1 消息发送失败 - 节点不健康
+
+**问题描述**:
+
+```json
+{
+ "detail": "Node 'node1' is not healthy: WeChat check failed. Message sending failed."
+}
+```
+
+**HTTP 状态码**:503
+
+**解决方案**:
+
+1. 先检查节点状态:
+
+```bash
+curl http://localhost:8080/api/v1/nodes/node1/status \
+ -H "X-API-Key: your-api-key"
+```
+
+2. 确保微信客户端在节点机器上正常运行
+
+3. 手动初始化微信:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/nodes/node1/wechat/initialize \
+ -H "X-API-Key: your-api-key"
+```
+
+### 4.2 消息发送失败 - 接收者不存在
+
+**问题描述**:
+
+```json
+{
+ "data": {
+ "code": 1,
+ "message": "receiver not found"
+ }
+}
+```
+
+**解决方案**:
+
+1. 确认接收者的 wxid 或群ID 是否正确
+2. 获取好友/群列表确认:
+
+```bash
+# 获取好友列表
+curl http://localhost:8080/api/v1/nodes/node1/friends \
+ -H "X-API-Key: your-api-key"
+
+# 获取群列表
+curl http://localhost:8080/api/v1/nodes/node1/groups \
+ -H "X-API-Key: your-api-key"
+```
+
+### 4.3 消息类型不支持
+
+**问题描述**:
+
+```json
+{
+ "data": {
+ "code": 1,
+ "message": "unsupported message type"
+ }
+}
+```
+
+**解决方案**:
+
+目前系统仅支持 `text` 类型消息,消息发送时指定 `msg_type`:
+
+```json
+{
+ "node_id": "wx1",
+ "who": "wxid_friend",
+ "msg": "Hello!",
+ "msg_type": "text"
+}
+```
+
+---
+
+## 5. Webhook 问题
+
+### 5.1 Webhook 通知发送失败
+
+**问题描述**:
+
+日志中出现 `Webhook通知发送失败` 相关信息。
+
+**可能原因**:
+- Webhook URL 配置错误
+- 网络不通
+- Webhook 服务不可用
+- 消息格式不被接受
+
+**解决方案**:
+
+1. 检查 Webhook 配置:
+
+```bash
+curl http://localhost:8080/api/v1/config/webhook \
+ -H "X-API-Key: your-api-key"
+```
+
+2. 测试 Webhook URL 是否可用:
+
+```bash
+# 测试企业微信 Webhook
+curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key" \
+ -H "Content-Type: application/json" \
+ -d '{"msgtype": "text", "text": {"content": "test"}}'
+```
+
+3. 检查 Webhook 事件类型配置:
+
+```json
+{
+ "event_types": ["api_error", "wechat_offline", "wechat_online", "node_offline", "node_online"]
+}
+```
+
+### 5.2 Bark 通知格式问题
+
+**问题描述**:
+
+Bark 格式的 Webhook 发送失败。
+
+**解决方案**:
+
+确保 Webhook URL 正确,格式为:`https://api.day.app/your-bark-id`
+
+---
+
+## 6. 监控服务问题
+
+### 6.1 监控服务未启动
+
+**问题描述**:
+
+日志中没有看到监控服务的输出,状态变化不触发 Webhook。
+
+**解决方案**:
+
+1. 检查监控配置:
+
+```bash
+curl http://localhost:8080/api/v1/config/monitor \
+ -H "X-API-Key: your-api-key"
+```
+
+2. 确保 `enabled` 为 `true`
+
+3. 检查监控状态:
+
+```bash
+curl http://localhost:8080/api/v1/monitor/status \
+ -H "X-API-Key: your-api-key"
+```
+
+### 6.2 监控检测间隔过长
+
+**问题描述**:
+
+状态变化后很久才收到 Webhook 通知。
+
+**解决方案**:
+
+调整 `check_interval` 配置:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/config/monitor \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "enabled": true,
+ "check_interval": 30
+ }'
+```
+
+---
+
+## 7. 日志问题
+
+### 7.1 SSE 日志流断开
+
+**问题描述**:
+
+前端日志流连接经常断开。
+
+**可能原因**:
+- 网络问题
+- 服务器负载过高
+- 客户端处理不及时
+
+**解决方案**:
+
+1. 检查网络连接
+2. 实现断线重连机制:
+
+```javascript
+function connectLogStream() {
+ const eventSource = new EventSource('/api/v1/logs/stream');
+
+ eventSource.onmessage = (event) => {
+ const log = JSON.parse(event.data);
+ // 处理日志
+ };
+
+ eventSource.onerror = () => {
+ console.log('Connection lost, reconnecting...');
+ setTimeout(connectLogStream, 3000);
+ };
+
+ return eventSource;
+}
+```
+
+### 7.2 日志丢失
+
+**问题描述**:
+
+日志数量超过 500 条后,早期的日志被清除。
+
+**解决方案**:
+
+这是系统设计行为,`LogService` 默认只保留最近 500 条日志。如需更多日志存储,可以:
+
+1. 修改 `log_service.py` 中的 `max_logs` 参数
+2. 实现日志持久化存储(写入文件或数据库)
+
+---
+
+## 8. 插件问题
+
+### 8.1 插件未注册
+
+**问题描述**:
+
+```json
+{
+ "detail": "Plugin 'xxx' not found"
+}
+```
+
+**解决方案**:
+
+1. 检查插件是否已注册:
+
+```bash
+curl http://localhost:8080/api/v1/plugins \
+ -H "X-API-Key: your-api-key"
+```
+
+2. 确认内置插件是否在启动时正确注册(检查 `builtin.py`)
+
+### 8.2 插件执行失败
+
+**问题描述**:
+
+```json
+{
+ "success": false,
+ "error": "Plugin execution failed"
+}
+```
+
+**解决方案**:
+
+1. 检查插件配置是否正确
+2. 查看系统日志获取详细错误信息
+3. 测试插件 API:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/external/plugin/execute \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "plugin_name": "plugin_name",
+ "action": "execute",
+ "params": {}
+ }'
+```
+
+---
+
+## 9. 性能问题
+
+### 9.1 高并发下响应慢
+
+**可能原因**:
+- 节点数量过多
+- 网络延迟
+- 同步阻塞操作
+
+**解决方案**:
+
+1. 增加监控检测间隔
+2. 使用异步调用优化
+3. 部署多实例负载均衡
+
+### 9.2 内存占用过高
+
+**可能原因**:
+- 日志队列过大
+- 订阅者过多
+- 节点对象未及时释放
+
+**解决方案**:
+
+1. 减少 `LogService` 的 `max_logs` 参数
+2. 及时关闭不需要的 SSE 连接
+3. 定期重启服务清理内存
+
+---
+
+## 10. 安全问题
+
+### 10.1 API Key 泄露
+
+**风险**:外部系统可能非法访问 API
+
+**解决方案**:
+
+1. 立即更换 API Key
+2. 限制 CORS 来源
+3. 使用 HTTPS 传输
+4. 定期轮换 API Key
+
+### 10.2 生产环境安全建议
+
+1. 修改默认的 `secret_key`
+2. 设置 `debug: false`
+3. 配置正确的 `cors_origins`
+4. 使用强密码的 API Key
+5. 启用 HTTPS
+6. 配置防火墙规则
+
+---
+
+## 诊断命令汇总
+
+```bash
+# 检查服务健康状态
+curl http://localhost:8080/health
+
+# 检查监控状态
+curl http://localhost:8080/api/v1/monitor/status \
+ -H "X-API-Key: your-key"
+
+# 检查节点状态
+curl http://localhost:8080/api/v1/nodes \
+ -H "X-API-Key: your-key"
+
+# 检查 Webhook 配置
+curl http://localhost:8080/api/v1/config/webhook \
+ -H "X-API-Key: your-key"
+
+# 检查最近日志
+curl "http://localhost:8080/api/v1/logs/recent?count=20" \
+ -H "X-API-Key: your-key"
+
+# 检查插件列表
+curl http://localhost:8080/api/v1/plugins \
+ -H "X-API-Key: your-key"
+```
+
+---
+
+## 获取帮助
+
+如果以上方案无法解决您的问题,请:
+
+1. 查看详细日志输出
+2. 检查系统环境配置
+3. 确认网络连通性
+4. 查看 [API.md](./API.md) 确认 API 使用正确
+5. 查看 [ARCHITECTURE.md](./ARCHITECTURE.md) 了解系统架构
diff --git a/doc/wxautorelogin/API.md b/doc/wxautorelogin/API.md
new file mode 100644
index 0000000..a6b2b4c
--- /dev/null
+++ b/doc/wxautorelogin/API.md
@@ -0,0 +1,271 @@
+# WXAuto Relogin - API 接口文档
+
+## 1. 概述
+
+本文档定义 WXAuto Relogin 功能所需的 API 接口。
+
+## 2. 回调接口
+
+### 2.1 接收二维码
+
+客户端上传二维码到中控。
+
+```
+POST /api/v1/callback/relogin/qrcode
+```
+
+**Headers:**
+```
+X-API-Key: your-api-key
+Content-Type: application/json
+```
+
+**请求体:**
+```json
+{
+ "node_id": "wx1",
+ "qrcode_base64": "data:image/png;base64,iVBORw0KGgo...",
+ "expires_in": 120
+}
+```
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| node_id | string | 是 | 节点ID |
+| qrcode_base64 | string | 是 | Base64编码的PNG图片,带data URI前缀 |
+| expires_in | integer | 否 | 过期时间(秒),默认120 |
+
+**响应:**
+```json
+{
+ "success": true,
+ "message": "二维码已接收",
+ "data": {
+ "status": "qrcode_ready",
+ "expires_at": "2026-04-07T15:30:00"
+ }
+}
+```
+
+### 2.2 更新重登录状态
+
+```
+POST /api/v1/callback/relogin/status
+```
+
+**请求体:**
+```json
+{
+ "node_id": "wx1",
+ "status": "scanned",
+ "message": "用户已扫码"
+}
+```
+
+| 状态 | 说明 |
+|------|------|
+| waiting_qrcode | 等待获取二维码 |
+| qrcode_ready | 二维码已就绪 |
+| scanned | 已扫码,待确认 |
+| confirmed | 已确认,登录中 |
+| completed | 登录完成 |
+| failed | 登录失败 |
+
+## 3. 节点接口
+
+### 3.1 获取重登录状态
+
+```
+GET /api/v1/nodes/{node_id}/relogin/status
+```
+
+**响应:**
+```json
+{
+ "success": true,
+ "data": {
+ "node_id": "wx1",
+ "status": "qrcode_ready",
+ "qrcode_base64": "data:image/png;base64,...",
+ "qrcode_expires_at": "2026-04-07T15:30:00",
+ "created_at": "2026-04-07T15:00:00",
+ "updated_at": "2026-04-07T15:05:00"
+ }
+}
+```
+
+### 3.2 清除重登录状态
+
+```
+DELETE /api/v1/nodes/{node_id}/relogin/status
+```
+
+**响应:**
+```json
+{
+ "success": true,
+ "message": "状态已清除"
+}
+```
+
+## 4. 客户端管理接口
+
+### 4.1 注册客户端
+
+```
+POST /api/v1/relogin/clients
+```
+
+**请求体:**
+```json
+{
+ "client_name": "服务器1",
+ "webhook_url": "http://192.168.1.100:8081/webhook/relogin",
+ "secret_key": "optional_secret_for_signing"
+}
+```
+
+**响应:**
+```json
+{
+ "success": true,
+ "data": {
+ "id": 1,
+ "client_name": "服务器1",
+ "webhook_url": "http://192.168.1.100:8081/webhook/relogin",
+ "enabled": true,
+ "created_at": "2026-04-07T15:00:00"
+ }
+}
+```
+
+### 4.2 获取客户端列表
+
+```
+GET /api/v1/relogin/clients
+```
+
+**响应:**
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "id": 1,
+ "client_name": "服务器1",
+ "webhook_url": "http://192.168.1.100:8081/webhook/relogin",
+ "enabled": true,
+ "created_at": "2026-04-07T15:00:00"
+ }
+ ]
+}
+```
+
+### 4.3 更新客户端
+
+```
+PUT /api/v1/relogin/clients/{client_id}
+```
+
+**请求体:**
+```json
+{
+ "enabled": false
+}
+```
+
+### 4.4 删除客户端
+
+```
+DELETE /api/v1/relogin/clients/{client_id}
+```
+
+## 5. Webhook 告警格式
+
+中控向客户端发送的告警格式:
+
+```json
+{
+ "event": "wechat_offline",
+ "node_id": "wx1",
+ "node_name": "微信1",
+ "wechat_status": "offline",
+ "timestamp": "2026-04-07T15:00:00",
+ "message": "节点 wx1 微信掉线"
+}
+```
+
+**event 类型:**
+| 事件 | 说明 |
+|------|------|
+| wechat_offline | 微信掉线 |
+| wechat_online | 微信上线 |
+| node_offline | 节点离线 |
+| node_online | 节点上线 |
+
+## 6. 签名验证(可选)
+
+如果配置了 secret_key,客户端应验证请求签名:
+
+```
+X-Signature: sha256=xxxxxxxxxxxxxx
+```
+
+**验证算法:**
+```python
+import hmac
+import hashlib
+
+def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
+ expected = hmac.new(
+ secret.encode(),
+ payload,
+ hashlib.sha256
+ ).hexdigest()
+ return hmac.compare_digest(f"sha256={expected}", signature)
+```
+
+## 7. 错误码
+
+| 错误码 | 说明 |
+|--------|------|
+| 404 | 节点不存在 |
+| 400 | 参数错误 |
+| 401 | API Key无效 |
+| 403 | 无权限 |
+| 500 | 服务器内部错误 |
+
+## 8. 使用示例
+
+### 8.1 完整重登录流程
+
+```bash
+# 1. 微信掉线,中控发送告警到客户端
+# 客户端接收: POST /webhook/relogin
+
+# 2. 客户端从节点获取二维码
+curl -o qrcode.png http://node-api:5000/api/wechat/qrcode \
+ -H "X-API-Key: node-api-key"
+
+# 3. 客户端上传二维码到中控
+curl -X POST http://center:8080/api/v1/callback/relogin/qrcode \
+ -H "X-API-Key: your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "node_id": "wx1",
+ "qrcode_base64": "data:image/png;base64,...",
+ "expires_in": 120
+ }'
+
+# 4. 前端轮询获取状态
+curl http://center:8080/api/v1/nodes/wx1/relogin/status \
+ -H "X-API-Key: your-api-key"
+
+# 5. 用户扫码确认后,节点回调通知中控
+curl -X POST http://center:8080/api/v1/callback/node/check \
+ -d '{"node_id": "wx1", "status": "online"}'
+
+# 6. 客户端轮询发现 completed
+curl http://center:8080/api/v1/nodes/wx1/relogin/status
+# 返回: {"status": "completed"}
+```
diff --git a/doc/wxautorelogin/CLIENT.md b/doc/wxautorelogin/CLIENT.md
new file mode 100644
index 0000000..b79dec5
--- /dev/null
+++ b/doc/wxautorelogin/CLIENT.md
@@ -0,0 +1,375 @@
+# WXAuto Relogin - 客户端开发指南
+
+## 1. 概述
+
+本文档说明如何开发 WXAuto Relogin 客户端程序,用于接收中控告警并自动处理微信重登录。
+
+## 2. 技术栈
+
+- Python 3.11+
+- FastAPI (Webhook服务)
+- httpx (HTTP客户端)
+- asyncio (异步处理)
+
+## 3. 项目结构
+
+```
+wxauto-relogin-client/
+├── client/
+│ ├── __init__.py
+│ ├── webhook_server.py # Webhook接收服务
+│ ├── qrcode_handler.py # 二维码处理
+│ ├── node_client.py # 节点通信
+│ └── config.py # 配置
+├── requirements.txt
+└── README.md
+```
+
+## 4. 配置
+
+```python
+# client/config.py
+from pydantic import BaseModel
+
+class ClientConfig(BaseModel):
+ center_url: str = "http://localhost:8080"
+ center_api_key: str = "your-external-api-key"
+ client_name: str = "relogin-client-1"
+ secret_key: str = "" # 可选,用于签名验证
+ listen_host: str = "0.0.0.0"
+ listen_port: int = 8081
+
+config = ClientConfig()
+```
+
+## 5. Webhook 接收服务
+
+```python
+# client/webhook_server.py
+from fastapi import FastAPI, HTTPException, Header
+from pydantic import BaseModel
+from typing import Optional
+import asyncio
+import logging
+
+from .qrcode_handler import QRCodeHandler
+from .config import config
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger("relogin-client")
+
+app = FastAPI()
+qrcode_handler = QRCodeHandler()
+
+class ReloginAlert(BaseModel):
+ event: str
+ node_id: str
+ node_name: str
+ wechat_status: str
+ timestamp: str
+
+@app.post("/webhook/relogin")
+async def receive_alert(
+ alert: ReloginAlert,
+ x_signature: Optional[str] = Header(None)
+):
+ """
+ 接收中控的掉线告警
+ """
+ logger.info(f"Received alert: {alert.node_id} - {alert.wechat_status}")
+
+ # 验证签名(如果配置了secret_key)
+ if config.secret_key:
+ # TODO: 实现签名验证
+ pass
+
+ # 触发重登录流程
+ asyncio.create_task(handle_relogin(alert))
+
+ return {"status": "accepted", "node_id": alert.node_id}
+
+async def handle_relogin(alert: ReloginAlert):
+ """
+ 处理重登录流程
+ """
+ try:
+ logger.info(f"Starting relogin for {alert.node_id}")
+
+ # 1. 获取二维码
+ qrcode_base64 = await qrcode_handler.get_and_upload_qrcode(alert.node_id)
+ if qrcode_base64:
+ logger.info(f"QR code uploaded for {alert.node_id}")
+
+ # 2. 等待扫码确认
+ success = await qrcode_handler.wait_for_scan(alert.node_id, timeout=180)
+ if success:
+ logger.info(f"ReLogin successful for {alert.node_id}")
+ else:
+ logger.warning(f"ReLogin timeout for {alert.node_id}")
+
+ except Exception as e:
+ logger.error(f"ReLogin failed for {alert.node_id}: {e}")
+
+@app.get("/health")
+async def health():
+ return {"status": "healthy"}
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host=config.listen_host, port=config.listen_port)
+```
+
+## 6. 二维码处理
+
+```python
+# client/qrcode_handler.py
+import httpx
+import asyncio
+import time
+import base64
+from typing import Optional
+
+from .config import config
+
+class QRCodeHandler:
+ def __init__(self):
+ self.center_url = config.center_url
+ self.api_key = config.center_api_key
+
+ async def get_qrcode_from_node(self, api_url: str, node_api_key: str) -> Optional[bytes]:
+ """
+ 从wxauto节点获取二维码
+ """
+ try:
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ response = await client.get(
+ f"{api_url}/api/wechat/qrcode",
+ headers={"X-API-Key": node_api_key}
+ )
+ if response.status_code == 200:
+ return response.content
+ else:
+ return None
+ except Exception as e:
+ print(f"Failed to get QR code from node: {e}")
+ return None
+
+ async def upload_qrcode_to_center(self, node_id: str, qrcode_bytes: bytes, expires_in: int = 120):
+ """
+ 上传二维码到中控
+ """
+ qrcode_base64 = base64.b64encode(qrcode_bytes).decode()
+
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.post(
+ f"{self.center_url}/api/v1/callback/relogin/qrcode",
+ headers={"X-API-Key": self.api_key},
+ json={
+ "node_id": node_id,
+ "qrcode_base64": f"data:image/png;base64,{qrcode_base64}",
+ "expires_in": expires_in
+ }
+ )
+ return response.json().get("success", False)
+
+ async def get_node_info(self, node_id: str) -> Optional[dict]:
+ """
+ 从中控获取节点信息(包含API地址和密钥)
+ """
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ f"{self.center_url}/api/v1/nodes/{node_id}",
+ headers={"X-API-Key": self.api_key}
+ )
+ if response.status_code == 200:
+ return response.json().get("data")
+ return None
+
+ async def get_and_upload_qrcode(self, node_id: str) -> Optional[str]:
+ """
+ 获取二维码并上传到中控
+ """
+ # 1. 获取节点信息
+ node_info = await self.get_node_info(node_id)
+ if not node_info:
+ print(f"Node {node_id} not found")
+ return None
+
+ # 2. 获取二维码
+ qrcode_bytes = await self.get_qrcode_from_node(
+ api_url=node_info["api_url"],
+ node_api_key=node_info["api_key"]
+ )
+ if not qrcode_bytes:
+ print(f"Failed to get QR code for {node_id}")
+ return None
+
+ # 3. 上传到中控
+ success = await self.upload_qrcode_to_center(node_id, qrcode_bytes)
+ if success:
+ return f"data:image/png;base64,{base64.b64encode(qrcode_bytes).decode()}"
+ return None
+
+ async def wait_for_scan(self, node_id: str, timeout: int = 180) -> bool:
+ """
+ 轮询中控,等待扫码确认
+ """
+ start_time = time.time()
+ interval = 2 # 每2秒轮询一次
+
+ while time.time() - start_time < timeout:
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ f"{self.center_url}/api/v1/nodes/{node_id}/relogin/status",
+ headers={"X-API-Key": self.api_key}
+ )
+ if response.status_code == 200:
+ data = response.json().get("data", {})
+ status = data.get("status")
+
+ print(f"Status: {status}")
+
+ if status == "completed":
+ return True
+ elif status == "failed":
+ return False
+
+ except Exception as e:
+ print(f"Polling error: {e}")
+
+ await asyncio.sleep(interval)
+
+ return False
+```
+
+## 7. 节点通信客户端
+
+```python
+# client/node_client.py
+import httpx
+from typing import Optional
+
+class NodeClient:
+ def __init__(self, api_url: str, api_key: str):
+ self.api_url = api_url.rstrip("/")
+ self.api_key = api_key
+
+ async def get_qrcode(self) -> Optional[bytes]:
+ """
+ 获取登录二维码
+ """
+ try:
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ response = await client.get(
+ f"{self.api_url}/api/wechat/qrcode",
+ headers={"X-API-Key": self.api_key}
+ )
+ if response.status_code == 200:
+ return response.content
+ return None
+ except Exception as e:
+ print(f"Failed to get QR code: {e}")
+ return None
+
+ async def check_login_status(self) -> dict:
+ """
+ 检查登录状态
+ """
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.get(
+ f"{self.api_url}/api/wechat/status",
+ headers={"X-API-Key": self.api_key}
+ )
+ if response.status_code == 200:
+ return response.json()
+ return {"logged_in": False}
+ except Exception as e:
+ return {"logged_in": False, "error": str(e)}
+
+ async def logout(self) -> bool:
+ """
+ 登出微信
+ """
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ response = await client.post(
+ f"{self.api_url}/api/wechat/logout",
+ headers={"X-API-Key": self.api_key}
+ )
+ return response.status_code == 200
+ except Exception as e:
+ print(f"Logout failed: {e}")
+ return False
+```
+
+## 8. 依赖
+
+```txt
+# requirements.txt
+fastapi>=0.100.0
+uvicorn>=0.23.0
+httpx>=0.24.0
+pydantic>=2.0.0
+python-dotenv>=1.0.0
+```
+
+## 9. 运行
+
+```bash
+# 安装依赖
+pip install -r requirements.txt
+
+# 运行客户端
+python -m client.webhook_server
+```
+
+## 10. Docker 部署
+
+```dockerfile
+# Dockerfile
+FROM python:3.11-slim
+
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY client/ ./client/
+
+CMD ["python", "-m", "client.webhook_server"]
+```
+
+```yaml
+# docker-compose.yml
+services:
+ relogin-client:
+ build: .
+ ports:
+ - "8081:8081"
+ environment:
+ - CENTER_URL=http://your-center:8080
+ - CENTER_API_KEY=your-api-key
+ - CLIENT_NAME=relogin-1
+ restart: unless-stopped
+```
+
+## 11. 完整示例
+
+```python
+# main.py
+import asyncio
+from client.webhook_server import app
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8081)
+```
+
+## 12. 注意事项
+
+1. **网络安全**: 确保客户端Webhook端口能从公网访问
+2. **签名验证**: 生产环境建议启用signature验证
+3. **超时设置**: 二维码有效期通常120秒,需要及时处理
+4. **重试机制**: 建议添加失败重试逻辑
+5. **日志记录**: 保留完整日志便于排查问题
diff --git a/doc/wxautorelogin/README.md b/doc/wxautorelogin/README.md
new file mode 100644
index 0000000..7419f41
--- /dev/null
+++ b/doc/wxautorelogin/README.md
@@ -0,0 +1,400 @@
+# WXAuto Relogin - 微信自动重登录方案
+
+## 1. 背景问题
+
+### 1.1 当前痛点
+- 微信掉线后,需要人工介入扫码登录
+- 节点多时,人工操作繁琐
+- 无法远程自动恢复微信会话
+
+### 1.2 现有架构
+```
+中控(Center) ←→ 节点(Node)
+ ↑
+ API通信/回调
+```
+
+## 2. 解决方案概述
+
+### 2.1 核心理念
+```
+微信掉线 → 中控告警 → 客户端接收 → 获取二维码 → 回传中控 → 前端展示扫码
+```
+
+### 2.2 系统架构
+```
+┌─────────────────────────────────────────────────────────────┐
+│ WXAuto Center │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ 告警系统 │→ │ 插件系统 │← │ 前端展示 │ │
+│ │ (Webhook) │ │(wxautorelogin)│ │ (二维码) │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ │
+│ ↑ │ │
+│ │ ┌──────────────┐ │ │
+│ └─────────│ 回调接口 │───────────┘ │
+│ │ /callback │ │
+│ └──────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+ ↑ HTTP
+┌─────────────────────────────────────────────────────────────┐
+│ WXAuto Relogin Client │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ 消息接收 │ │ 二维码获取 │→ │ 状态回调 │ │
+│ │ (Webhook) │ │(get QR code) │ │ (重置微信) │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## 3. 实现方案
+
+### 3.1 中控端改动
+
+#### 3.1.1 新增 API 接口
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `POST /api/v1/callback/relogin/qrcode` | 接收二维码 | 客户端回传二维码图片 |
+| `GET /api/v1/nodes/{node_id}/relogin/status` | 重登录状态 | 查询节点重登录进度 |
+
+#### 3.1.2 二维码存储结构
+
+```python
+# NodeReloginStatus
+{
+ "node_id": "wx1",
+ "status": "waiting_qrcode" | "qrcode_ready" | "scanned" | "confirmed" | "completed" | "failed",
+ "qrcode_base64": "data:image/png;base64,...",
+ "qrcode_expires_at": "2026-04-07T15:30:00",
+ "created_at": "2026-04-07T15:00:00",
+ "updated_at": "2026-04-07T15:05:00"
+}
+```
+
+#### 3.1.3 前端展示
+
+- 节点详情页增加"重登录"标签页
+- 实时显示二维码(轮询 /api/v1/nodes/{node_id}/relogin/status)
+- 二维码过期提示
+
+### 3.2 客户端改动
+
+#### 3.2.1 Webhook 接收器
+
+客户端需要暴露一个 HTTP 接口接收中控告警:
+
+```python
+# client/webhook_server.py
+from fastapi import FastAPI
+import uvicorn
+
+app = FastAPI()
+
+@app.post("/webhook/relogin")
+async def receive_alert(data: dict):
+ """
+ 接收中控的掉线告警
+ {
+ "event": "wechat_offline",
+ "node_id": "wx1",
+ "node_name": "微信1",
+ "wechat_status": "offline",
+ "timestamp": "2026-04-07T15:00:00"
+ }
+ """
+ # 触发获取二维码流程
+ await trigger_relogin(node_id=data["node_id"])
+ return {"status": "ok"}
+
+async def trigger_relogin(node_id: str):
+ # 1. 调用节点API获取二维码
+ # 2. 上传二维码到中控
+ # 3. 等待扫码确认
+ pass
+```
+
+#### 3.2.2 二维码获取流程
+
+```python
+async def get_qrcode_from_node(api_url: str, api_key: str) -> bytes:
+ """
+ 从wxauto节点获取二维码
+ GET /api/wechat/qrcode
+ 返回: PNG图片二进制
+ """
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{api_url}/api/wechat/qrcode",
+ headers={"X-API-Key": api_key}
+ )
+ return response.content
+
+async def upload_qrcode_to_center(center_url: str, node_id: str, qrcode_bytes: bytes):
+ """
+ 上传二维码到中控
+ POST /api/v1/callback/relogin/qrcode
+ """
+ async with httpx.AsyncClient() as client:
+ import base64
+ qrcode_base64 = base64.b64encode(qrcode_bytes).decode()
+ await client.post(
+ f"{center_url}/api/v1/callback/relogin/qrcode",
+ json={
+ "node_id": node_id,
+ "qrcode_base64": qrcode_base64
+ }
+ )
+```
+
+#### 3.2.3 扫码状态轮询
+
+```python
+async def wait_for_scan(center_url: str, node_id: str, timeout: int = 120):
+ """
+ 轮询中控接口,等待用户扫码确认
+ """
+ start_time = time.time()
+ while time.time() - start_time < timeout:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{center_url}/api/v1/nodes/{node_id}/relogin/status"
+ )
+ status = response.json()
+ if status["status"] == "completed":
+ return True
+ elif status["status"] == "failed":
+ return False
+ await asyncio.sleep(2)
+ return False
+```
+
+## 4. 完整流程时序
+
+### 4.1 时序图
+
+```
+┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
+│ 微信 │ │ 节点 │ │ 中控 │ │ 客户端 │ │ 用户 │
+└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
+ │ │ │ │ │
+ │ 掉线 │ │ │ │
+ │───────────────→│ │ │ │
+ │ │ wechat_offline callback │ │
+ │ │───────────────→│ │ │
+ │ │ │ Webhook告警 │ │
+ │ │ │───────────────→│ │
+ │ │ │ │ │
+ │ │ │ │ 获取二维码 │
+ │ │ GET /qrcode │ │───────────────→│
+ │ 返回二维码 │←──────────────│ │ │
+ │←───────────────│ │ │ │
+ │ │ │ │ 上传二维码 │
+ │ │ │←──────────────│ │
+ │ │ │ │ │
+ │ │ │ 前端展示二维码│ │
+ │ │ │───────────────→│ 扫码 │
+ │ │ │ │←──────────────│
+ │ 扫码确认 │ │ │ │
+ │←───────────────│ │ │ │
+ │ 登录成功 │ │ │ │
+ │───────────────→│ │ │ │
+ │ │ wechat_online callback │ │
+ │ │───────────────→│ │ │
+ │ │ │ 更新状态 │ │
+ │ │ │───────────────→│ 完成 │
+```
+
+### 4.2 详细步骤
+
+| 步骤 | 参与者 | 操作 | API/协议 |
+|------|--------|------|----------|
+| 1 | 微信 | 掉线检测 | - |
+| 2 | 节点 | 回调通知中控 | POST /callback |
+| 3 | 中控 | 触发Webhook告警 | HTTP |
+| 4 | 客户端 | 接收告警 | POST /webhook/relogin |
+| 5 | 客户端 | 请求节点二维码 | GET /api/wechat/qrcode |
+| 6 | 节点 | 返回二维码图片 | HTTP |
+| 7 | 客户端 | 上传二维码到中控 | POST /callback/relogin/qrcode |
+| 8 | 中控 | 存储并通知前端 | WebSocket/轮询 |
+| 9 | 前端 | 展示二维码 | - |
+| 10 | 用户 | 扫码确认 | - |
+| 11 | 微信 | 登录成功 | - |
+| 12 | 节点 | 回调通知中控 | POST /callback |
+| 13 | 中控 | 更新节点状态 | - |
+
+## 5. 数据库设计
+
+### 5.1 新增表
+
+```sql
+-- 节点重登录状态表
+CREATE TABLE node_relogin_status (
+ id SERIAL PRIMARY KEY,
+ node_id VARCHAR(50) UNIQUE NOT NULL,
+ status VARCHAR(20) DEFAULT 'idle',
+ qrcode_base64 TEXT,
+ qrcode_expires_at TIMESTAMP,
+ error_message TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 客户端配置表
+CREATE TABLE relogin_clients (
+ id SERIAL PRIMARY KEY,
+ client_name VARCHAR(100) NOT NULL,
+ webhook_url VARCHAR(500) NOT NULL,
+ secret_key VARCHAR(100),
+ enabled BOOLEAN DEFAULT true,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### 5.2 状态枚举
+
+| 状态 | 说明 |
+|------|------|
+| `idle` | 空闲,无重登录任务 |
+| `waiting_qrcode` | 等待获取二维码 |
+| `qrcode_ready` | 二维码已就绪 |
+| `scanned` | 已扫码,待确认 |
+| `confirmed` | 已确认,登录中 |
+| `completed` | 登录完成 |
+| `failed` | 登录失败 |
+
+## 6. API 接口详细设计
+
+### 6.1 回调接口 - 接收二维码
+
+```
+POST /api/v1/callback/relogin/qrcode
+```
+
+**请求体:**
+```json
+{
+ "node_id": "wx1",
+ "qrcode_base64": "data:image/png;base64,iVBORw0KGgo...",
+ "expires_in": 120
+}
+```
+
+**响应:**
+```json
+{
+ "success": true,
+ "message": "二维码已接收"
+}
+```
+
+### 6.2 查询重登录状态
+
+```
+GET /api/v1/nodes/{node_id}/relogin/status
+```
+
+**响应:**
+```json
+{
+ "success": true,
+ "data": {
+ "node_id": "wx1",
+ "status": "qrcode_ready",
+ "qrcode_base64": "data:image/png;base64,...",
+ "qrcode_expires_at": "2026-04-07T15:30:00",
+ "created_at": "2026-04-07T15:00:00"
+ }
+}
+```
+
+### 6.3 客户端注册
+
+```
+POST /api/v1/relogin/clients
+```
+
+**请求体:**
+```json
+{
+ "client_name": "服务器1",
+ "webhook_url": "http://192.168.1.100:8081/webhook/relogin",
+ "secret_key": "optional_secret"
+}
+```
+
+### 6.4 客户端列表
+
+```
+GET /api/v1/relogin/clients
+```
+
+## 7. 安全考虑
+
+### 7.1 客户端认证
+- 客户端注册时分配 secret_key
+- 回调接口需要携带签名验证
+- 中控可配置允许的客户端IP白名单
+
+### 7.2 回调签名验证
+
+```python
+import hmac
+import hashlib
+
+def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
+ expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
+ return hmac.compare_digest(expected, signature)
+```
+
+## 8. 文件结构
+
+```
+wxauto_api/
+├── plugins/
+│ └── wxautorelogin_plugin.py # 重登录插件
+├── api/
+│ └── relogin.py # 重登录相关API
+├── services/
+│ └── relogin_service.py # 重登录服务
+└── doc/
+ └── wxautorelogin/
+ ├── README.md # 本文档
+ ├── CLIENT.md # 客户端开发指南
+ └── API.md # API接口文档
+
+wxauto-relogin-client/
+├── client/
+│ ├── __init__.py
+│ ├── webhook_server.py # Webhook接收服务
+│ ├── qrcode_handler.py # 二维码处理
+│ └── config.py # 配置
+├── requirements.txt
+└── README.md
+```
+
+## 9. 可行性结论
+
+### 9.1 技术可行性:✅ 可行
+
+1. **wxauto节点已提供QR码接口** - 通过 `/api/wechat/qrcode` 获取
+2. **中控已有回调机制** - 可复用现有 callback 系统
+3. **前端支持实时更新** - 可通过轮询或WebSocket展示二维码
+
+### 9.2 实施难度:🟡 中等
+
+1. 需要开发客户端程序
+2. 需要新增API接口
+3. 需要前端新增重登录页面
+
+### 9.3 建议实施顺序
+
+1. **Phase 1**: 中控端API开发
+2. **Phase 2**: 客户端程序开发
+3. **Phase 3**: 前端页面开发
+4. **Phase 4**: 整体联调测试
+
+## 10. 后续优化方向
+
+- 支持短信/邮件通知
+- 支持多客户端负载均衡
+- 支持扫码历史记录
+- 支持自动重试机制
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..59f4eff
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,70 @@
+# ///
+# docker-compose.yml
+# 描述:Docker Compose 配置 - ARM64 架构 (Apple Silicon Mac)
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+services:
+ postgres:
+ image: postgres:15-alpine
+ container_name: wxauto-postgres
+ environment:
+ POSTGRES_USER: wxauto
+ POSTGRES_PASSWORD: wxauto_password
+ POSTGRES_DB: wxauto
+ volumes:
+ - ./docker_data/db:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U wxauto"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+
+ redis:
+ image: redis:7-alpine
+ container_name: wxauto-redis
+ command: redis-server --appendonly yes
+ volumes:
+ - ./docker_data/redis:/data
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+
+ wxauto-center:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: wxauto-center
+ environment:
+ DATABASE_URL: postgresql://wxauto:wxauto_password@postgres:5432/wxauto
+ REDIS_URL: redis://redis:6379/0
+ APP_NAME: WXAuto Center
+ HOST: 0.0.0.0
+ PORT: 8080
+ DEBUG: "true"
+ SECRET_KEY: wxauto-center-secret-key-change-in-production
+ WORKERS: "1"
+ API_PREFIX: /api/v1
+ EXTERNAL_API_KEY: your-external-api-key
+ TZ: Asia/Shanghai
+ PYTHONTZ: Asia/Shanghai
+ volumes:
+ - ./docker_data/logs:/app/logs
+ - ./plugins/custom:/app/plugins/custom
+ ports:
+ - "8080:8080"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ restart: unless-stopped
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..08d91c3
--- /dev/null
+++ b/main.py
@@ -0,0 +1,208 @@
+# ///
+# main.py
+# 描述:FastAPI主入口,启动中控系统服务
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from fastapi import FastAPI, Request
+from fastapi.staticfiles import StaticFiles
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import RedirectResponse, JSONResponse
+
+from config import get_settings, load_config_from_file, save_config_to_file
+from api import nodes, messages, external, plugins, callback, webhook
+from plugins.builtin import register_builtin_plugins
+from plugins.plugin_loader import hot_plugin_loader, create_sample_plugin
+from services.node_manager import node_manager
+from services.monitor_service import monitor_service
+from services.log_service import log_service
+from services.scheduler_service import scheduler_service
+
+settings = get_settings()
+
+app = FastAPI(
+ title=settings.app_name,
+ description="WXAuto中控系统 - 多节点微信控制平台",
+ version="1.0.0",
+ docs_url="/docs",
+ redoc_url="/redoc"
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.exception_handler(Exception)
+async def global_exception_handler(request: Request, exc: Exception):
+ log_service.error(f"Global exception: {str(exc)}", "Error")
+ return JSONResponse(
+ status_code=500,
+ content={"success": False, "error": str(exc)}
+ )
+
+
+app.include_router(nodes.router, prefix=settings.api_prefix)
+app.include_router(messages.router, prefix=settings.api_prefix)
+app.include_router(external.router, prefix=settings.api_prefix)
+app.include_router(plugins.router, prefix=settings.api_prefix)
+app.include_router(callback.router, prefix=settings.api_prefix)
+app.include_router(webhook.router, prefix=settings.api_prefix)
+
+
+@app.on_event("startup")
+async def startup_event():
+ load_config_from_file("config.json")
+ register_builtin_plugins()
+
+ from database_service import init_database, get_db_service
+ from config import get_settings
+ settings = get_settings()
+ init_database(settings.database.url)
+ log_service.info("Database initialized", "Startup")
+
+ db_service = get_db_service()
+ if db_service:
+ for node in db_service.get_all_nodes():
+ await node_manager.add_node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ log_service.info(f"Loaded {len(db_service.get_all_nodes())} nodes from database", "Startup")
+
+ for plugin in db_service.get_all_plugin_configs():
+ from plugins.base import PluginRegistry
+ p = PluginRegistry.get(plugin.plugin_name)
+ if p:
+ import json
+ config = json.loads(plugin.config_json) if plugin.config_json else {}
+ p.initialize(config)
+ if plugin.enabled:
+ p.enable()
+ else:
+ p.disable()
+ log_service.info("Plugins loaded from database", "Startup")
+
+ from services.plugin_notification_service import plugin_notification_service
+ plugin_notification_service.initialize()
+ log_service.info("Plugin notification service initialized", "Startup")
+
+ await monitor_service.start()
+ await scheduler_service.start()
+ hot_plugin_loader.start()
+ create_sample_plugin()
+
+ print(f"[{settings.app_name}] Started successfully")
+ print(f"API Documentation: http://{settings.host}:{settings.port}/docs")
+
+
+@app.on_event("shutdown")
+async def shutdown_event():
+ await monitor_service.stop()
+ await scheduler_service.stop()
+ hot_plugin_loader.stop()
+ save_config_to_file("config.json")
+ print(f"[{settings.app_name}] Shutdown, config saved")
+
+
+@app.get("/", summary="首页")
+async def root():
+ return RedirectResponse(url="/static/index.html")
+
+
+@app.get("/health", summary="健康检查")
+async def health():
+ return {"status": "ok", "app": settings.app_name}
+
+
+@app.get("/api/v1/config/monitor", summary="获取监控配置")
+async def get_monitor_config():
+ return {
+ "success": True,
+ "data": settings.monitor.model_dump()
+ }
+
+
+@app.post("/api/v1/config/monitor", summary="更新监控配置")
+async def update_monitor_config(request: dict):
+ from config import MonitorConfig
+ global settings
+ settings.monitor = MonitorConfig(
+ enabled=request.get("enabled", True),
+ check_interval=request.get("check_interval", 60)
+ )
+ save_config_to_file()
+ return {"success": True, "message": "Monitor config updated"}
+
+
+@app.get("/api/v1/monitor/status", summary="获取监控状态")
+async def get_monitor_status():
+ return {
+ "success": True,
+ "data": monitor_service.get_all_status()
+ }
+
+
+@app.get("/api/v1/logs/recent", summary="获取最近日志")
+async def get_recent_logs(count: int = 50):
+ return {
+ "success": True,
+ "data": log_service.get_recent_logs(count)
+ }
+
+
+@app.get("/api/v1/logs/stream", summary="日志流")
+async def log_stream():
+ from fastapi.responses import StreamingResponse
+
+ async def event_generator():
+ queue = await log_service.subscribe()
+ try:
+ while True:
+ log_entry = await queue.get()
+ yield f"data: {json.dumps(log_entry)}\n\n"
+ except Exception:
+ log_service.unsubscribe(queue)
+
+ return StreamingResponse(
+ event_generator(),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no"
+ }
+ )
+
+
+static_path = os.path.join(os.path.dirname(__file__), "static")
+if os.path.exists(static_path):
+ app.mount("/static", StaticFiles(directory=static_path), name="static")
+
+
+def main():
+ import uvicorn
+ uvicorn.run(
+ "main:app",
+ host=settings.host,
+ port=settings.port,
+ reload=settings.debug
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/models.py b/models.py
new file mode 100644
index 0000000..6f6fde1
--- /dev/null
+++ b/models.py
@@ -0,0 +1,136 @@
+# ///
+# models.py
+# 描述:数据库模型,使用 SQLAlchemy ORM
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from sqlalchemy import create_engine, Column, String, Boolean, Integer, DateTime, Text
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from datetime import datetime
+import pytz
+
+Base = declarative_base()
+
+_asia_tz = pytz.timezone('Asia/Shanghai')
+
+
+def now_asia():
+ return datetime.now(_asia_tz)
+
+
+class Node(Base):
+ __tablename__ = "nodes"
+
+ node_id = Column(String(50), primary_key=True)
+ name = Column(String(100), nullable=False)
+ api_url = Column(String(255), nullable=False)
+ api_key = Column(String(255), nullable=False)
+ enabled = Column(Boolean, default=True)
+ description = Column(Text, default="")
+ group = Column(String(50), default="default", name="node_group")
+ status = Column(String(20), default="inactive")
+ wechat_status = Column(String(20), default="online")
+ is_healthy = Column(Boolean, default=False)
+ health_message = Column(String(255), default="")
+ created_at = Column(DateTime, default=now_asia)
+ updated_at = Column(DateTime, default=now_asia, onupdate=now_asia)
+
+ def to_dict(self):
+ return {
+ "node_id": self.node_id,
+ "name": self.name,
+ "api_url": self.api_url,
+ "api_key": self.api_key,
+ "enabled": self.enabled,
+ "description": self.description,
+ "group": self.group,
+ "status": self.status,
+ "wechat_status": self.wechat_status,
+ "is_healthy": self.is_healthy,
+ "health_message": self.health_message,
+ "created_at": self.created_at.isoformat() if self.created_at else None,
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None
+ }
+
+
+class WebhookUrl(Base):
+ __tablename__ = "webhook_urls"
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String(100), nullable=False)
+ url = Column(String(500), nullable=False)
+ format = Column(String(20), default="bark")
+ enabled = Column(Boolean, default=True)
+ event_types = Column(Text, default="")
+ created_at = Column(DateTime, default=now_asia)
+ updated_at = Column(DateTime, default=now_asia, onupdate=now_asia)
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "name": self.name,
+ "url": self.url,
+ "format": self.format,
+ "enabled": self.enabled,
+ "event_types": self.event_types.split(",") if self.event_types else [],
+ "created_at": self.created_at.isoformat() if self.created_at else None,
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None
+ }
+
+
+class LogEntry(Base):
+ __tablename__ = "logs"
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ timestamp = Column(DateTime, default=now_asia)
+ level = Column(String(20), nullable=False)
+ source = Column(String(50), nullable=False)
+ message = Column(Text, nullable=False)
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "timestamp": self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp else None,
+ "level": self.level,
+ "source": self.source,
+ "message": self.message
+ }
+
+
+class PluginConfig(Base):
+ __tablename__ = "plugin_configs"
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ plugin_name = Column(String(100), unique=True, nullable=False)
+ config_json = Column(Text, default="{}")
+ enabled = Column(Boolean, default=False)
+ created_at = Column(DateTime, default=now_asia)
+ updated_at = Column(DateTime, default=now_asia, onupdate=now_asia)
+
+ def to_dict(self):
+ import json
+ return {
+ "id": self.id,
+ "plugin_name": self.plugin_name,
+ "config": json.loads(self.config_json) if self.config_json else {},
+ "enabled": self.enabled,
+ "created_at": self.created_at.isoformat() if self.created_at else None,
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None
+ }
+
+
+class DatabaseManager:
+ def __init__(self, database_url: str):
+ self.engine = create_engine(database_url, echo=False, pool_pre_ping=True)
+ self.SessionLocal = sessionmaker(bind=self.engine)
+
+ def create_tables(self):
+ Base.metadata.create_all(self.engine)
+
+ def get_session(self):
+ return self.SessionLocal()
+
+ def close(self):
+ self.engine.dispose()
diff --git a/plugin_config.json b/plugin_config.json
new file mode 100644
index 0000000..08b2d8e
--- /dev/null
+++ b/plugin_config.json
@@ -0,0 +1,48 @@
+{
+ "name": "advanced_sender",
+ "config": {
+ "cron": "*/2 * * * *",
+ "external_db": {
+ "host": "192.168.2.27",
+ "user": "root2",
+ "password": "root@root",
+ "database": "addb",
+ "port": 3306,
+ "charset": "utf8mb4"
+ },
+ "db_table": "user_data",
+ "sendto_field": "sendto",
+ "datasources": [
+ {
+ "name": "source1",
+ "url": "https://xiansuo2.lfgzyx.com/admin/customer/sales/index?addtabs=1&sort=id&order=desc&offset=0&limit=50",
+ "headers": {
+ "Cookie": "PHPSESSID=fccjr2k0j8kb19tn0rdmlvkob4; think_var=zh-cn; keeplogin=1041%7C86400%7C1775608752%7C5e9aac1d434f86f0586746d873cdfec5",
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/javascript, */*; q=0.01",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ "verify_ssl": true,
+ "zhid": "1"
+ },
+ {
+ "name": "source2",
+ "url": "https://xiansuo2.lfgzyx.com/admin/customer/sales/index?addtabs=1&sort=id&order=desc&offset=0&limit=50",
+ "headers": {
+ "Cookie": "PHPSESSID=3hk0unlg9f27bgbg992n52k567; think_var=zh-cn",
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/javascript, */*; q=0.01",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ "verify_ssl": true,
+ "zhid": "2"
+ }
+ ],
+ "node_id": "wx1",
+ "receiver": "asq",
+ "fetch_timeout": 15,
+ "send_interval_between_sources": 20
+ }
+}
\ No newline at end of file
diff --git a/plugins/__init__.py b/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/advanced_sender_plugin.py b/plugins/advanced_sender_plugin.py
new file mode 100644
index 0000000..04536b3
--- /dev/null
+++ b/plugins/advanced_sender_plugin.py
@@ -0,0 +1,839 @@
+# ///
+# advanced_sender_plugin.py
+# 描述:高级多节点数据推送插件,支持表单化配置和变量赋值
+# 功能:多数据源获取 → 去重入库 → 按记录时间路由发送到微信
+# 作者:User
+# 创建日期:2026-04-06
+# requires: requests, pymysql
+# ///
+
+import os
+import sys
+import re
+import time
+import json
+import logging
+import asyncio
+import threading
+from datetime import datetime, time as dtime
+from typing import Any, Dict, List, Optional
+from contextlib import contextmanager
+from croniter import croniter
+from plugins.base import ScheduledTaskPlugin
+from plugins.plugin_config_schema import (
+ PluginConfigSchema, ConfigSection, ConfigField, FieldType,
+ create_variable_assignment_field, cron_validator
+)
+
+PLUGIN_LOG_DIR = "/app/data/logs"
+os.makedirs(PLUGIN_LOG_DIR, exist_ok=True)
+
+plugin_logger = logging.getLogger("advanced_sender")
+plugin_logger.setLevel(logging.INFO)
+
+file_handler = logging.FileHandler(os.path.join(PLUGIN_LOG_DIR, "advanced_sender.log"), encoding='utf-8')
+file_handler.setLevel(logging.INFO)
+file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
+
+if not plugin_logger.handlers:
+ plugin_logger.addHandler(file_handler)
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.INFO)
+ plugin_logger.addHandler(console_handler)
+
+logger = plugin_logger
+
+
+def get_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("advanced_sender")
+
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(
+ name="cron",
+ label="执行周期",
+ field_type=FieldType.CRON,
+ required=True,
+ default="*/2 * * * *",
+ description="Cron表达式"
+ ),
+ ConfigField(
+ name="enable_cron",
+ label="启用定时任务",
+ field_type=FieldType.BOOLEAN,
+ default=False
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="node",
+ label="默认设置(可被数据源覆盖)",
+ fields=[
+ ConfigField(
+ name="node_id",
+ label="默认节点ID",
+ field_type=FieldType.STRING,
+ default="wx1",
+ placeholder="如 wx1"
+ ),
+ ConfigField(
+ name="receiver",
+ label="默认接收人",
+ field_type=FieldType.STRING,
+ default="asq",
+ placeholder="联系人名称"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="database",
+ label="外部数据库",
+ fields=[
+ ConfigField(
+ name="db_host",
+ label="数据库地址",
+ field_type=FieldType.STRING,
+ required=True,
+ default="192.168.2.27",
+ placeholder="192.168.1.100"
+ ),
+ ConfigField(
+ name="db_port",
+ label="端口",
+ field_type=FieldType.NUMBER,
+ default=3306,
+ min_value=1,
+ max_value=65535
+ ),
+ ConfigField(
+ name="db_user",
+ label="用户名",
+ field_type=FieldType.STRING,
+ required=True,
+ default="root2"
+ ),
+ ConfigField(
+ name="db_password",
+ label="密码",
+ field_type=FieldType.PASSWORD,
+ default="root@root"
+ ),
+ ConfigField(
+ name="db_name",
+ label="数据库名",
+ field_type=FieldType.STRING,
+ required=True,
+ default="addb"
+ ),
+ ConfigField(
+ name="db_table",
+ label="表名",
+ field_type=FieldType.STRING,
+ required=True,
+ default="user_data"
+ ),
+ ConfigField(
+ name="sendto_field",
+ label="发送状态字段",
+ field_type=FieldType.STRING,
+ default="sendto"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="cookie_source1",
+ label="数据源1",
+ fields=[
+ ConfigField(
+ name="cookie_1",
+ label="Cookie",
+ field_type=FieldType.TEXTAREA,
+ placeholder="PHPSESSID=xxx; think_var=zh-cn; keeplogin=...",
+ description="数据源1的完整Cookie"
+ ),
+ ConfigField(
+ name="node_id_1",
+ label="节点ID",
+ field_type=FieldType.STRING,
+ default="",
+ placeholder="留空使用默认节点"
+ ),
+ ConfigField(
+ name="receiver_1",
+ label="接收人",
+ field_type=FieldType.STRING,
+ default="",
+ placeholder="留空使用默认接收人"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="cookie_source2",
+ label="数据源2",
+ fields=[
+ ConfigField(
+ name="cookie_2",
+ label="Cookie",
+ field_type=FieldType.TEXTAREA,
+ placeholder="PHPSESSID=xxx; think_var=zh-cn; keeplogin=..."
+ ),
+ ConfigField(
+ name="node_id_2",
+ label="节点ID",
+ field_type=FieldType.STRING,
+ default="",
+ placeholder="留空使用默认节点"
+ ),
+ ConfigField(
+ name="receiver_2",
+ label="接收人",
+ field_type=FieldType.STRING,
+ default="",
+ placeholder="留空使用默认接收人"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="fetch",
+ label="抓取设置",
+ fields=[
+ ConfigField(
+ name="fetch_timeout",
+ label="请求超时(秒)",
+ field_type=FieldType.NUMBER,
+ default=15,
+ min_value=5,
+ max_value=60
+ ),
+ ConfigField(
+ name="send_interval",
+ label="数据源间隔(秒)",
+ field_type=FieldType.NUMBER,
+ default=20,
+ min_value=0,
+ max_value=60,
+ description="两个数据源之间的等待时间"
+ )
+ ]
+ ))
+
+ return schema
+
+
+class CircuitBreaker:
+ def __init__(self, failure_threshold: int = 5, timeout: int = 60):
+ self.failure_threshold = failure_threshold
+ self.timeout = timeout
+ self.failures = 0
+ self.last_failure_time: Optional[float] = None
+ self.state = "closed"
+ self._lock = threading.Lock()
+
+ def call(self, func, *args, **kwargs):
+ with self._lock:
+ if self.state == "open":
+ if time.time() - self.last_failure_time >= self.timeout:
+ self.state = "half-open"
+ logger.info("Circuit breaker: OPEN -> HALF-OPEN")
+ else:
+ raise CircuitBreakerOpen("Circuit breaker is OPEN")
+
+ try:
+ result = func(*args, **kwargs)
+ with self._lock:
+ if self.state == "half-open":
+ self.state = "closed"
+ self.failures = 0
+ logger.info("Circuit breaker: HALF-OPEN -> CLOSED")
+ return result
+ except Exception as e:
+ with self._lock:
+ self.failures += 1
+ self.last_failure_time = time.time()
+ if self.failures >= self.failure_threshold:
+ self.state = "open"
+ logger.warning(f"Circuit breaker: CLOSED -> OPEN (failures={self.failures})")
+ raise
+
+
+class CircuitBreakerOpen(Exception):
+ pass
+
+
+class AdvancedSenderPlugin(ScheduledTaskPlugin):
+ plugin_name = "advanced_sender"
+ plugin_version = "6.0.0"
+ plugin_description = "高级多节点数据推送插件,支持表单化配置和Cookie变量"
+ plugin_author = "User"
+ config_schema = staticmethod(get_config_schema)
+
+ _instance_lock = threading.Lock()
+
+ def __init__(self):
+ super().__init__()
+ self._enabled = False
+ self._last_run_time: Optional[datetime] = None
+ self._running = False
+ self._semaphore: Optional[asyncio.Semaphore] = None
+
+ self._cookie_error_reported_1 = False
+ self._cookie_error_reported_2 = False
+
+ self.cron_expression = "*/2 * * * *"
+
+ self.db_host = "192.168.2.27"
+ self.db_port = 3306
+ self.db_user = "root2"
+ self.db_password = "root@root"
+ self.db_name = "addb"
+ self.db_table = "user_data"
+ self.sendto_field = "sendto"
+
+ self.cookie_1 = ""
+ self.cookie_2 = ""
+
+ self.node_id = "wx1"
+ self.receiver = "asq"
+
+ self.node_id_1 = ""
+ self.receiver_1 = ""
+ self.node_id_2 = ""
+ self.receiver_2 = ""
+ self.fetch_timeout = 15
+ self.send_interval = 20
+
+ self._api_circuit = CircuitBreaker(failure_threshold=3, timeout=30)
+ self._db_circuit = CircuitBreaker(failure_threshold=3, timeout=30)
+
+ def _get_external_db_config(self) -> Dict[str, Any]:
+ return {
+ "host": self.db_host,
+ "user": self.db_user,
+ "password": self.db_password,
+ "database": self.db_name,
+ "port": self.db_port,
+ "charset": "utf8mb4"
+ }
+
+ def _get_datasources(self) -> List[Dict[str, Any]]:
+ return [
+ {
+ "name": "source1",
+ "url": "https://xiansuo2.lfgzyx.com/admin/customer/sales/index?addtabs=1&sort=id&order=desc&offset=0&limit=50",
+ "headers": {
+ "Cookie": self.cookie_1,
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/javascript, */*; q=0.01",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ "verify_ssl": True,
+ "zhid": "1",
+ "node_id": self.node_id_1 or self.node_id,
+ "receiver": self.receiver_1 or self.receiver
+ },
+ {
+ "name": "source2",
+ "url": "https://xiansuo2.lfgzyx.com/admin/customer/sales/index?addtabs=1&sort=id&order=desc&offset=0&limit=50",
+ "headers": {
+ "Cookie": self.cookie_2,
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/javascript, */*; q=0.01",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ "verify_ssl": True,
+ "zhid": "2",
+ "node_id": self.node_id_2 or self.node_id,
+ "receiver": self.receiver_2 or self.receiver
+ }
+ ]
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.cron_expression = config.get("cron", "*/2 * * * *")
+
+ self.db_host = config.get("db_host", self.db_host)
+ self.db_port = config.get("db_port", 3306)
+ self.db_user = config.get("db_user", self.db_user)
+ self.db_password = config.get("db_password", self.db_password)
+ self.db_name = config.get("db_name", self.db_name)
+ self.db_table = config.get("db_table", "user_data")
+ self.sendto_field = config.get("sendto_field", "sendto")
+
+ self.cookie_1 = config.get("cookie_1", "")
+ self.cookie_2 = config.get("cookie_2", "")
+
+ self.node_id = config.get("node_id", "wx1")
+ self.receiver = config.get("receiver", "asq")
+
+ self.node_id_1 = config.get("node_id_1", "")
+ self.receiver_1 = config.get("receiver_1", "")
+ self.node_id_2 = config.get("node_id_2", "")
+ self.receiver_2 = config.get("receiver_2", "")
+
+ self.fetch_timeout = max(5, min(60, config.get("fetch_timeout", 15)))
+ self.send_interval = config.get("send_interval", 20)
+
+ logger.info(f"AdvancedSenderPlugin initialized: cron={self.cron_expression}")
+ logger.info(f"Cookie1: {self.cookie_1[:30] if self.cookie_1 else 'empty'}..., Cookie2: {self.cookie_2[:30] if self.cookie_2 else 'empty'}...")
+ return True
+
+ def enable(self):
+ self._enabled = True
+ self.enabled = True
+ logger.info("AdvancedSenderPlugin enabled")
+
+ def disable(self):
+ self._enabled = False
+ self.enabled = False
+ logger.info("AdvancedSenderPlugin disabled")
+
+ def get_cron_expression(self) -> str:
+ return self.cron_expression
+
+ def should_run_and_get_next(self) -> tuple:
+ try:
+ now = datetime.now()
+ current_hour = now.hour
+
+ if current_hour < 8 or current_hour >= 22:
+ interval_minutes = 15
+ else:
+ interval_minutes = 2
+
+ cron = croniter(self.cron_expression, now)
+ prev_run = cron.get_prev(datetime)
+ next_run = cron.get_next(datetime)
+
+ if self._last_run_time is None or self._last_run_time != prev_run:
+ self._last_run_time = prev_run
+ return True, next_run
+
+ return False, next_run
+ except Exception as e:
+ logger.error(f"should_run_and_get_next error: {e}")
+ return False, None
+
+ @contextmanager
+ def _db_connection(self):
+ conn = None
+ try:
+ import pymysql
+ conn = pymysql.connect(
+ host=self.db_host,
+ user=self.db_user,
+ password=self.db_password,
+ database=self.db_name,
+ port=self.db_port,
+ charset="utf8mb4",
+ cursorclass=pymysql.cursors.DictCursor,
+ connect_timeout=5,
+ read_timeout=10,
+ write_timeout=10
+ )
+ yield conn
+ finally:
+ if conn:
+ try:
+ conn.close()
+ except Exception:
+ pass
+
+ def _fetch_from_datasource(self, datasource: Dict[str, Any]) -> List[Dict[str, Any]]:
+ source_name = datasource.get('name', 'unknown')
+ source_num = datasource.get('zhid', '0')
+ try:
+ import requests
+ url = datasource["url"]
+ if "_=" in url:
+ url = url + str(int(time.time() * 1000))
+
+ logger.info(f"Fetching from {source_name}: {url}")
+ logger.info(f"Cookie: {datasource['headers'].get('Cookie', '')[:50]}...")
+
+ response = requests.get(
+ url,
+ headers=datasource.get("headers", {}),
+ verify=datasource.get("verify_ssl", True),
+ timeout=self.fetch_timeout
+ )
+
+ if response.status_code == 403 or response.status_code == 401:
+ error_msg = f"数据源{source_name}Cookie失效 (HTTP {response.status_code})"
+ logger.error(error_msg)
+ if source_num == '1' and not self._cookie_error_reported_1:
+ self._cookie_error_reported_1 = True
+ self.notify_error("Cookie失效告警", error_msg)
+ elif source_num == '2' and not self._cookie_error_reported_2:
+ self._cookie_error_reported_2 = True
+ self.notify_error("Cookie失效告警", error_msg)
+ return []
+
+ response.raise_for_status()
+ data = response.json()
+
+ total = data.get('total', 0)
+ rows = data.get("rows", []) if isinstance(data, dict) else []
+
+ if total == 0 and len(rows) == 0:
+ error_msg = f"数据源{source_name}返回空数据,Cookie可能已失效"
+ logger.warning(error_msg)
+ if source_num == '1' and not self._cookie_error_reported_1:
+ self._cookie_error_reported_1 = True
+ self.notify_warning("数据源为空告警", error_msg)
+ elif source_num == '2' and not self._cookie_error_reported_2:
+ self._cookie_error_reported_2 = True
+ self.notify_warning("数据源为空告警", error_msg)
+ return []
+
+ if source_num == '1' and self._cookie_error_reported_1:
+ self._cookie_error_reported_1 = False
+ self.notify_success("数据源恢复", f"数据源{source_name}已恢复正常")
+ elif source_num == '2' and self._cookie_error_reported_2:
+ self._cookie_error_reported_2 = False
+ self.notify_success("数据源恢复", f"数据源{source_name}已恢复正常")
+
+ logger.info(f"Fetched data: total={total}")
+ return rows
+
+ except Exception as e:
+ error_msg = f"数据源{source_name}请求失败: {str(e)}"
+ logger.error(error_msg)
+ if source_num == '1' and not self._cookie_error_reported_1:
+ self._cookie_error_reported_1 = True
+ self.notify_error("数据源请求失败", error_msg)
+ elif source_num == '2' and not self._cookie_error_reported_2:
+ self._cookie_error_reported_2 = True
+ self.notify_error("数据源请求失败", error_msg)
+ return []
+
+ def _check_id_exists(self, conn, id_value: Any) -> bool:
+ try:
+ with conn.cursor() as cursor:
+ sql = f"SELECT id FROM {self.db_table} WHERE id = %s"
+ cursor.execute(sql, (id_value,))
+ result = cursor.fetchone()
+ return result is not None
+ except Exception as e:
+ logger.error(f"Check ID exists failed: {e}")
+ return False
+
+ def _insert_data(self, conn, data: Dict[str, Any], zhid: str) -> bool:
+ try:
+ def get_val(field):
+ val = data.get(field)
+ return val if val is not None else None
+
+ other_text = json.dumps(data.get("other_text")) if data.get("other_text") else None
+
+ sql = f"""
+ INSERT INTO {self.db_table} (
+ id, source_id, name, phone, location, province_id, city_id, address,
+ app_name, keshi, create_time, update_time, admin_id, distribute_admin_id,
+ distribute_time, follow_up_status, is_recover, recover_time, `desc`,
+ other, wx, ggmc, gid, recover_time_text, weight_id_text,
+ follow_up_status_text, follow_text, origin_id_text, admin_id_text,
+ sendto, zhid
+ ) VALUES (
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, 0, %s
+ )
+ """
+
+ values = (
+ get_val("id"), get_val("source_id"), get_val("name"), get_val("phone"),
+ get_val("location"), get_val("province_id"), get_val("city_id"), get_val("address"),
+ get_val("app_name"), get_val("keshi"), get_val("create_time"), get_val("update_time"),
+ get_val("admin_id"), get_val("distribute_admin_id"), get_val("distribute_time"),
+ get_val("follow_up_status"), get_val("is_recover"), get_val("recover_time"),
+ get_val("desc"), other_text, get_val("wx"), get_val("ggmc"),
+ get_val("gid"), get_val("recover_time_text"), get_val("weight_id_text"),
+ get_val("follow_up_status_text"), get_val("follow_text"), get_val("origin_id_text"),
+ get_val("admin_id_text"), zhid
+ )
+
+ with conn.cursor() as cursor:
+ cursor.execute(sql, values)
+ conn.commit()
+ logger.info(f"Successfully inserted data, ID: {data.get('id')}")
+ return True
+ except Exception as e:
+ conn.rollback()
+ logger.error(f"Insert data failed (ID: {data.get('id')}): {e}")
+ return False
+
+ def _get_pending_records(self, conn) -> List[Dict[str, Any]]:
+ try:
+ with conn.cursor() as cursor:
+ sql = f"SELECT * FROM {self.db_table} WHERE {self.sendto_field} = 0 ORDER BY id DESC"
+ cursor.execute(sql)
+ return cursor.fetchall()
+ except Exception as e:
+ logger.error(f"Get pending records failed: {e}")
+ return []
+
+ def _get_pending_records_by_zhid(self, conn, zhid: str) -> List[Dict[str, Any]]:
+ try:
+ with conn.cursor() as cursor:
+ sql = f"SELECT * FROM {self.db_table} WHERE {self.sendto_field} = 0 AND zhid = %s ORDER BY id DESC"
+ cursor.execute(sql, (zhid,))
+ return cursor.fetchall()
+ except Exception as e:
+ logger.error(f"Get pending records by zhid failed: {e}")
+ return []
+
+ def _update_sendto(self, conn, ids: List[int]) -> bool:
+ if not ids:
+ return True
+ try:
+ with conn.cursor() as cursor:
+ placeholders = ", ".join(["%s"] * len(ids))
+ sql = f"UPDATE {self.db_table} SET {self.sendto_field} = 1 WHERE id IN ({placeholders})"
+ cursor.execute(sql, ids)
+ conn.commit()
+ return True
+ except Exception as e:
+ conn.rollback()
+ logger.error(f"Update sendto failed: {e}")
+ return False
+
+ def _sync_node_to_manager(self):
+ from services.node_manager import node_manager
+ from database_service import get_db_service
+
+ db_service = get_db_service()
+ if db_service:
+ node = db_service.get_node(self.node_id)
+ if node:
+ existing = node_manager.nodes.get(self.node_id)
+ if existing:
+ existing.api_url = node.api_url
+ existing.api_key = node.api_key
+ else:
+ from services.node_manager import Node
+ new_node = Node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ node_manager.nodes[node.node_id] = new_node
+ logger.info(f"Node {self.node_id} synced to node_manager")
+
+ def _send_via_center(self, node_id: str, who: str, msg: str) -> Dict[str, Any]:
+ from services.node_manager import node_manager
+ from concurrent.futures import ThreadPoolExecutor
+
+ self._sync_node_to_manager()
+ effective_node_id = node_id or self.node_id
+
+ def _send_in_thread():
+ async def _send_async():
+ return await node_manager.send_message(
+ node_id=effective_node_id,
+ who=who,
+ msg=msg,
+ msg_type="text"
+ )
+ return asyncio.run(_send_async())
+
+ try:
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(_send_in_thread)
+ result = future.result(timeout=15)
+ logger.info(f"Message sent via center to {who} via {effective_node_id}: {result}")
+ return result
+ except Exception as e:
+ logger.error(f"Send via center failed: {e}")
+ return {"success": False, "error": str(e)}
+
+ def _process_pending_records(self, conn, records: List[Dict[str, Any]], datasource: Dict[str, Any]) -> Dict[str, Any]:
+ if not records:
+ logger.info("No records to process")
+ return {"success": True, "sent": 0, "skipped": 0}
+
+ target_node_id = datasource.get("node_id", self.node_id)
+ target_receiver = datasource.get("receiver", self.receiver)
+ source_name = datasource.get("name", "unknown")
+
+ logger.info(f"Processing {len(records)} pending records for {source_name} -> {target_node_id}/{target_receiver}")
+
+ sent_count = 0
+ skipped_count = 0
+
+ for record in records:
+ record_id = record.get("id")
+ phone = record.get("phone", "")
+ create_time = record.get("create_time")
+ myid = record.get("myid", 0)
+
+ logger.info(f"Record ID: {record_id}")
+
+ if create_time:
+ create_time_dt = create_time if hasattr(create_time, 'hour') else datetime.now()
+ hour = create_time_dt.hour
+
+ if 0 <= hour <= 7:
+ logger.info(f"Hour {hour} in 0-7: skip")
+ skipped_count += 1
+ continue
+ elif 8 <= hour <= 16:
+ if myid % 2 == 0:
+ target = target_receiver
+ else:
+ target = target_receiver
+ logger.info(f"Hour {hour}, myid={myid}: sending to {target}")
+ elif 17 <= hour <= 21:
+ target = target_receiver
+ logger.info(f"Hour {hour}: sending to {target}")
+ elif 22 <= hour <= 23:
+ logger.info(f"Hour {hour} in 22-23: skip")
+ skipped_count += 1
+ continue
+ else:
+ target = target_receiver
+
+ msg = f"{phone}\r\n时间:{create_time}"
+ result = self._send_via_center(target_node_id, target, msg)
+ if result.get("success"):
+ sent_count += 1
+
+ return {"success": True, "sent": sent_count, "skipped": skipped_count, "total": len(records)}
+
+ def run_task(self) -> Dict[str, Any]:
+ if self._running:
+ logger.warning("Previous task still running, skipping")
+ return {"success": False, "error": "上一次执行尚未完成"}
+
+ self._running = True
+ try:
+ logger.info(f"Starting task at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+
+ try:
+ with self._db_connection() as conn:
+ datasources = self._get_datasources()
+ all_pending_records = []
+
+ for idx, datasource in enumerate(datasources):
+ rows = self._fetch_from_datasource(datasource)
+ logger.info(f"Source {datasource.get('name')}: fetched {len(rows)} rows")
+
+ for row in rows:
+ record_id = row.get("id")
+ if not record_id:
+ logger.warning("Record missing ID, skip")
+ continue
+
+ if self._check_id_exists(conn, record_id):
+ logger.info(f"ID {record_id} exists, skip")
+ continue
+
+ self._insert_data(conn, row, datasource.get("zhid", "0"))
+
+ if idx < len(datasources) - 1 and self.send_interval > 0:
+ logger.info(f"Waiting {self.send_interval}s before next source...")
+ time.sleep(self.send_interval)
+
+ for datasource in datasources:
+ records = self._get_pending_records_by_zhid(conn, datasource.get("zhid", "0"))
+ if records:
+ send_result = self._process_pending_records(conn, records, datasource)
+ logger.info(f"Source {datasource.get('name')} send result: {send_result}")
+ all_pending_records.extend(records)
+
+ if all_pending_records:
+ ids = [r["id"] for r in all_pending_records]
+ self._update_sendto(conn, ids)
+ logger.info(f"Updated {len(ids)} records sendto=1")
+
+ return {"success": True, "message": "Task completed", "total": len(all_pending_records)}
+
+ except Exception as e:
+ logger.error(f"Database operation error: {e}")
+ return {"success": False, "error": str(e)}
+
+ except Exception as e:
+ logger.error(f"run_task error: {e}")
+ return {"success": False, "error": str(e)}
+ finally:
+ self._running = False
+
+ def aggregate_results(self, results: List[Dict[str, Any]], shift_name: str = "") -> Dict[str, Any]:
+ success_count = sum(1 for r in results if isinstance(r, dict) and r.get("success"))
+ total_sent = sum(r.get("sent", 0) for r in results if isinstance(r, dict))
+ total_skipped = sum(r.get("skipped", 0) for r in results if isinstance(r, dict))
+ return {
+ "success": success_count == len(results),
+ "shift": shift_name,
+ "total": len(results),
+ "success_count": success_count,
+ "total_sent": total_sent,
+ "total_skipped": total_skipped,
+ "results": results
+ }
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ action = params.get("action", "send")
+
+ if action == "send":
+ return self.run_task()
+ elif action == "test":
+ return self.test_config()
+ elif action == "test_send":
+ msg = params.get("msg", "测试消息")
+ node_id = params.get("node_id", self.node_id)
+ receiver = params.get("receiver", self.receiver)
+ return self._send_via_center(node_id, receiver, msg)
+ elif action == "query_pending":
+ try:
+ with self._db_connection() as conn:
+ records = self._get_pending_records(conn)
+ return {"success": True, "count": len(records), "records": records[:10]}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ return {"success": False, "error": "未知操作"}
+
+ def test_config(self) -> Dict[str, Any]:
+ try:
+ cron = croniter(self.cron_expression, datetime.now())
+ prev_run = cron.get_prev(datetime)
+ next_run = cron.get_next(datetime)
+
+ return {
+ "success": True,
+ "cron": self.cron_expression,
+ "prev_run": str(prev_run),
+ "next_run": str(next_run),
+ "node_id": self.node_id,
+ "receiver": self.receiver,
+ "datasource1": {
+ "node_id": self.node_id_1 or "(default)",
+ "receiver": self.receiver_1 or "(default)"
+ },
+ "datasource2": {
+ "node_id": self.node_id_2 or "(default)",
+ "receiver": self.receiver_2 or "(default)"
+ },
+ "db_host": self.db_host,
+ "db_name": self.db_name,
+ "db_table": self.db_table,
+ "cookie_1": self.cookie_1[:50] + "..." if len(self.cookie_1) > 50 else self.cookie_1,
+ "cookie_2": self.cookie_2[:50] + "..." if len(self.cookie_2) > 50 else self.cookie_2,
+ "config": {
+ "fetch_timeout": self.fetch_timeout,
+ "send_interval": self.send_interval
+ }
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
\ No newline at end of file
diff --git a/plugins/base.py b/plugins/base.py
new file mode 100644
index 0000000..810261e
--- /dev/null
+++ b/plugins/base.py
@@ -0,0 +1,280 @@
+# ///
+# base.py
+# 描述:插件基类定义,插件系统核心接口
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# 更新日期:2026-04-06
+# ///
+
+from abc import ABC, abstractmethod
+from typing import Any, Dict, List, Optional, Callable
+from enum import Enum
+
+
+class PluginType(Enum):
+ MESSAGE_HANDLER = "message_handler"
+ DATA_SOURCE = "data_source"
+ ACTION_TRIGGER = "action_trigger"
+ AI_AGENT = "ai_agent"
+ SCHEDULED_TASK = "scheduled_task"
+ HTTP_SCHEDULED_SENDER = "http_scheduled_sender"
+ CUSTOM = "custom"
+
+
+class PluginBase(ABC):
+ plugin_name: str = ""
+ plugin_version: str = "1.0.0"
+ plugin_type: PluginType = PluginType.CUSTOM
+ plugin_description: str = ""
+ plugin_author: str = ""
+
+ def __init__(self):
+ self.enabled = False
+ self.config: Dict[str, Any] = {}
+ self._metadata: Dict[str, Any] = {}
+
+ @abstractmethod
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ pass
+
+ @abstractmethod
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ pass
+
+ def enable(self):
+ self.enabled = True
+
+ def disable(self):
+ self.enabled = False
+
+ def get_info(self) -> Dict[str, Any]:
+ return {
+ "name": self.plugin_name,
+ "version": self.plugin_version,
+ "type": self.plugin_type.value,
+ "description": self.plugin_description,
+ "author": self.plugin_author,
+ "enabled": self.enabled,
+ "config": self.config,
+ "metadata": self._metadata,
+ }
+
+ def set_metadata(self, key: str, value: Any):
+ self._metadata[key] = value
+
+ def get_metadata(self, key: str, default: Any = None) -> Any:
+ return self._metadata.get(key, default)
+
+ def validate_config(self, config: Dict[str, Any], required_fields: List[str]) -> tuple:
+ missing = [f for f in required_fields if not config.get(f)]
+ if missing:
+ return False, f"缺少必填字段: {', '.join(missing)}"
+ return True, ""
+
+
+class MessageHandlerPlugin(PluginBase):
+ plugin_type = PluginType.MESSAGE_HANDLER
+
+ @abstractmethod
+ def handle_message(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+ pass
+
+ def should_handle(self, message: Dict[str, Any]) -> bool:
+ return True
+
+
+class DataSourcePlugin(PluginBase):
+ plugin_type = PluginType.DATA_SOURCE
+
+ @abstractmethod
+ def fetch_data(self, query: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
+ pass
+
+ def get_schema(self) -> Dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {}
+ }
+
+
+class ActionTriggerPlugin(PluginBase):
+ plugin_type = PluginType.ACTION_TRIGGER
+
+ @abstractmethod
+ def trigger(self, action: str, params: Dict[str, Any]) -> Dict[str, Any]:
+ pass
+
+ def get_supported_actions(self) -> List[str]:
+ return []
+
+
+class AIAgentPlugin(PluginBase):
+ plugin_type = PluginType.AI_AGENT
+
+ @abstractmethod
+ def process(self, input_text: str, context: Dict[str, Any]) -> str:
+ pass
+
+ @abstractmethod
+ def get_response(self, messages: List[Dict[str, str]]) -> str:
+ pass
+
+ def get_system_prompt(self) -> str:
+ return ""
+
+
+class ScheduledTaskPlugin(PluginBase):
+ plugin_type = PluginType.SCHEDULED_TASK
+
+ @abstractmethod
+ def get_cron_expression(self) -> str:
+ pass
+
+ @abstractmethod
+ def should_run_and_get_next(self) -> tuple:
+ pass
+
+ @abstractmethod
+ def run_task(self) -> Dict[str, Any]:
+ pass
+
+ def on_task_success(self, result: Dict[str, Any]):
+ pass
+
+ def on_task_error(self, error: str):
+ pass
+
+ def notify(self, title: str, message: str, level: str = "info") -> bool:
+ from services.plugin_notification_service import plugin_notification_service
+ return plugin_notification_service.send_notification(
+ plugin_name=self.plugin_name,
+ title=title,
+ message=message,
+ level=level
+ )
+
+ def notify_error(self, title: str, message: str) -> bool:
+ return self.notify(title, message, level="error")
+
+ def notify_warning(self, title: str, message: str) -> bool:
+ return self.notify(title, message, level="warning")
+
+ def notify_success(self, title: str, message: str) -> bool:
+ return self.notify(title, message, level="success")
+
+
+class HTTPScheduledSenderPlugin(PluginBase):
+ plugin_type = PluginType.HTTP_SCHEDULED_SENDER
+
+ @abstractmethod
+ def get_cron_expression(self) -> str:
+ pass
+
+ @abstractmethod
+ def should_run_and_get_next(self) -> tuple:
+ pass
+
+ @abstractmethod
+ def run_task(self) -> Dict[str, Any]:
+ pass
+
+ @abstractmethod
+ def fetch_data(self) -> Any:
+ pass
+
+ @abstractmethod
+ def parse_items(self, data: Any) -> List[Dict[str, Any]]:
+ pass
+
+ @abstractmethod
+ def format_message(self, items: List[Dict[str, Any]]) -> str:
+ pass
+
+
+class PluginRegistry:
+ _plugins: Dict[str, PluginBase] = {}
+ _callbacks: List[Callable] = []
+ _event_handlers: Dict[str, List[Callable]] = {}
+
+ @classmethod
+ def register(cls, plugin: PluginBase, name: str = None) -> bool:
+ plugin_name = name or plugin.plugin_name
+ if plugin_name in cls._plugins:
+ return False
+ cls._plugins[plugin_name] = plugin
+ cls._emit_event("plugin_registered", plugin)
+ return True
+
+ @classmethod
+ def unregister(cls, name: str) -> bool:
+ if name in cls._plugins:
+ plugin = cls._plugins[name]
+ del cls._plugins[name]
+ cls._emit_event("plugin_unregistered", plugin)
+ return True
+ return False
+
+ @classmethod
+ def get(cls, name: str) -> Optional[PluginBase]:
+ return cls._plugins.get(name)
+
+ @classmethod
+ def get_all(cls) -> List[PluginBase]:
+ return list(cls._plugins.values())
+
+ @classmethod
+ def get_by_type(cls, plugin_type: PluginType) -> List[PluginBase]:
+ return [p for p in cls._plugins.values() if p.plugin_type == plugin_type]
+
+ @classmethod
+ def get_scheduled_tasks(cls) -> List[ScheduledTaskPlugin]:
+ result = []
+ for p in cls._plugins.values():
+ if p.plugin_type == PluginType.SCHEDULED_TASK and p.enabled:
+ result.append(p)
+ elif p.plugin_type == PluginType.HTTP_SCHEDULED_SENDER and p.enabled:
+ result.append(p)
+ return result
+
+ @classmethod
+ def enable(cls, name: str) -> bool:
+ plugin = cls.get(name)
+ if plugin:
+ plugin.enable()
+ cls._emit_event("plugin_enabled", plugin)
+ return True
+ return False
+
+ @classmethod
+ def disable(cls, name: str) -> bool:
+ plugin = cls.get(name)
+ if plugin:
+ plugin.disable()
+ cls._emit_event("plugin_disabled", plugin)
+ return True
+ return False
+
+ @classmethod
+ def on_register(cls, callback: Callable):
+ cls._callbacks.append(callback)
+
+ @classmethod
+ def on_event(cls, event: str, handler: Callable):
+ if event not in cls._event_handlers:
+ cls._event_handlers[event] = []
+ cls._event_handlers[event].append(handler)
+
+ @classmethod
+ def _emit_event(cls, event: str, *args, **kwargs):
+ for handler in cls._event_handlers.get(event, []):
+ try:
+ handler(*args, **kwargs)
+ except Exception:
+ pass
+
+ @classmethod
+ def get_plugin_types(cls) -> Dict[str, str]:
+ return {
+ pt.value: pt.name.replace("_", " ").title()
+ for pt in PluginType
+ }
diff --git a/plugins/builtin.py b/plugins/builtin.py
new file mode 100644
index 0000000..5cfcee3
--- /dev/null
+++ b/plugins/builtin.py
@@ -0,0 +1,195 @@
+# ///
+# builtin.py
+# 描述:内置插件实现
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# 更新日期:2026-04-06
+# ///
+
+import httpx
+from typing import Any, Dict, List
+from plugins.base import (
+ PluginBase,
+ PluginType,
+ MessageHandlerPlugin,
+ DataSourcePlugin,
+ AIAgentPlugin,
+ PluginRegistry,
+)
+from plugins.http_scheduled_sender import HTTPScheduledSenderPlugin
+
+
+class HTTPDataSourcePlugin(DataSourcePlugin):
+ plugin_name = "http_data_source"
+ plugin_version = "1.0.0"
+ plugin_description = "HTTP数据源插件,从外部API获取数据"
+ plugin_author = "AI Generated"
+
+ def __init__(self):
+ super().__init__()
+ self.base_url = ""
+ self.timeout = 30
+ self.headers = {}
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.base_url = config.get("base_url", "")
+ self.timeout = config.get("timeout", 30)
+ self.headers = config.get("headers", {})
+ return True
+
+ def fetch_data(self, query: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
+ if not self.base_url:
+ return []
+
+ try:
+ response = httpx.get(
+ f"{self.base_url}{query}",
+ params=params,
+ headers=self.headers,
+ timeout=self.timeout
+ )
+ response.raise_for_status()
+ data = response.json()
+ if isinstance(data, list):
+ return data
+ elif isinstance(data, dict) and "data" in data:
+ return data["data"]
+ return [data] if data else []
+ except Exception:
+ return []
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ query = params.get("query", "/")
+ query_params = params.get("params", {})
+ data = self.fetch_data(query, query_params)
+ return {"success": True, "data": data, "count": len(data)}
+
+
+class DatabaseQueryPlugin(DataSourcePlugin):
+ plugin_name = "database_query"
+ plugin_version = "1.0.0"
+ plugin_description = "数据库查询插件,通过SQL或API查询数据"
+ plugin_author = "AI Generated"
+
+ def __init__(self):
+ super().__init__()
+ self.connection_string = ""
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.connection_string = config.get("connection_string", "")
+ return True
+
+ def fetch_data(self, query: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
+ return []
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ source_type = params.get("source_type", "api")
+ if source_type == "api":
+ return {"success": True, "data": [], "message": "Use http_data_source plugin"}
+ return {"success": False, "data": [], "message": "Unknown source type"}
+
+
+class WebhookTriggerPlugin(PluginBase):
+ plugin_name = "webhook_trigger"
+ plugin_version = "1.0.0"
+ plugin_description = "Webhook触发器,接收外部webhook并触发相应动作"
+ plugin_author = "AI Generated"
+
+ def __init__(self):
+ super().__init__()
+ self.triggers: Dict[str, str] = {}
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.triggers = config.get("triggers", {})
+ return True
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ event = params.get("event", "")
+ callback_url = self.triggers.get(event, "")
+ if not callback_url:
+ return {"success": False, "message": f"No trigger for event: {event}"}
+
+ try:
+ response = httpx.post(callback_url, json=params.get("data", {}), timeout=10)
+ return {"success": True, "status_code": response.status_code}
+ except Exception as e:
+ return {"success": False, "message": str(e)}
+
+ def add_trigger(self, event: str, callback_url: str):
+ self.triggers[event] = callback_url
+
+ def remove_trigger(self, event: str) -> bool:
+ if event in self.triggers:
+ del self.triggers[event]
+ return True
+ return False
+
+
+class SimpleAIAgentPlugin(AIAgentPlugin):
+ plugin_name = "simple_ai_agent"
+ plugin_version = "1.0.0"
+ plugin_description = "简单AI智能体,支持调用外部LLM API"
+ plugin_author = "AI Generated"
+
+ def __init__(self):
+ super().__init__()
+ self.api_url = ""
+ self.api_key = ""
+ self.model = "gpt-3.5-turbo"
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.api_url = config.get("api_url", "")
+ self.api_key = config.get("api_key", "")
+ self.model = config.get("model", "gpt-3.5-turbo")
+ return True
+
+ def process(self, input_text: str, context: Dict[str, Any]) -> str:
+ messages = [{"role": "user", "content": input_text}]
+ return self.get_response(messages)
+
+ def get_response(self, messages: List[Dict[str, str]]) -> str:
+ if not self.api_url or not self.api_key:
+ return "AI agent not configured"
+
+ try:
+ response = httpx.post(
+ self.api_url,
+ headers={
+ "Authorization": f"Bearer {self.api_key}",
+ "Content-Type": "application/json"
+ },
+ json={
+ "model": self.model,
+ "messages": messages
+ },
+ timeout=60
+ )
+ result = response.json()
+ return result.get("choices", [{}])[0].get("message", {}).get("content", "")
+ except Exception as e:
+ return f"Error: {str(e)}"
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ input_text = params.get("input", "")
+ context = params.get("context", {})
+ result = self.process(input_text, context)
+ return {"success": True, "result": result}
+
+
+def register_builtin_plugins():
+ from plugins.advanced_sender_plugin import AdvancedSenderPlugin
+ builtin_plugins = [
+ HTTPDataSourcePlugin(),
+ DatabaseQueryPlugin(),
+ WebhookTriggerPlugin(),
+ SimpleAIAgentPlugin(),
+ HTTPScheduledSenderPlugin(),
+ AdvancedSenderPlugin(),
+ ]
+
+ for plugin in builtin_plugins:
+ PluginRegistry.register(plugin)
diff --git a/plugins/custom/README.md b/plugins/custom/README.md
new file mode 100644
index 0000000..dd66627
--- /dev/null
+++ b/plugins/custom/README.md
@@ -0,0 +1,125 @@
+# Custom Plugins Directory
+
+此目录用于存放热加载的自定义插件。
+
+## 热加载机制
+
+1. 插件文件放在此目录下
+2. 修改插件文件后会自动重新加载
+3. 无需重启 Docker
+
+## 创建插件
+
+```python
+# ///
+# my_plugin.py
+# 描述:我的自定义插件
+# ///
+
+from plugins.base import PluginBase, PluginType
+
+class MyPlugin(PluginBase):
+ plugin_name = "my_plugin"
+ plugin_version = "1.0.0"
+ plugin_type = PluginType.CUSTOM
+ plugin_description = "我的自定义插件"
+ plugin_author = "开发者"
+
+ def initialize(self, config: dict) -> bool:
+ self.config = config
+ return True
+
+ def execute(self, params: dict) -> dict:
+ return {"success": True, "result": "done"}
+```
+
+## 定时任务插件示例
+
+```python
+# ///
+# my_scheduled_plugin.py
+# 描述:定时任务插件示例
+# ///
+
+from plugins.base import ScheduledTaskPlugin
+from plugins.plugin_config_schema import (
+ PluginConfigSchema, ConfigSection, ConfigField, FieldType
+)
+
+def get_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("my_scheduled_plugin")
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(name="cron", label="执行周期", field_type=FieldType.CRON, default="*/5 * * * *"),
+ ConfigField(name="enabled", label="启用", field_type=FieldType.BOOLEAN, default=False)
+ ]
+ ))
+ return schema
+
+class MyScheduledPlugin(ScheduledTaskPlugin):
+ plugin_name = "my_scheduled_plugin"
+ plugin_version = "1.0.0"
+ plugin_description = "定时任务插件"
+ plugin_author = "开发者"
+ config_schema = staticmethod(get_config_schema)
+
+ def get_cron_expression(self) -> str:
+ return self.cron_expression
+
+ def should_run_and_get_next(self) -> tuple:
+ return True, None
+
+ def run_task(self) -> dict:
+ self.notify_success("任务完成", "执行成功")
+ return {"success": True}
+```
+
+## 通知功能
+
+所有继承 `ScheduledTaskPlugin` 的插件可以使用通知:
+
+```python
+self.notify("标题", "消息", "info")
+self.notify_error("错误", "错误详情")
+self.notify_warning("警告", "警告内容")
+self.notify_success("成功", "成功详情")
+```
+
+## 表单化配置
+
+```python
+from plugins.plugin_config_schema import (
+ PluginConfigSchema, ConfigSection, ConfigField, FieldType
+)
+
+def get_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("my_plugin")
+
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(name="key", label="键", field_type=FieldType.STRING, required=True),
+ ConfigField(name="timeout", label="超时", field_type=FieldType.NUMBER, default=15),
+ ConfigField(name="enabled", label="启用", field_type=FieldType.BOOLEAN, default=False)
+ ]
+ ))
+
+ return schema
+```
+
+## 字段类型
+
+| 类型 | 说明 |
+|------|------|
+| `STRING` | 文本输入 |
+| `PASSWORD` | 密码输入 |
+| `NUMBER` | 数字输入 |
+| `BOOLEAN` | 开关 |
+| `SELECT` | 下拉选择 |
+| `TEXTAREA` | 多行文本 |
+| `CRON` | Cron表达式 |
+
+更多信息请参阅 [插件开发指南](../doc/wxauto_center/PLUGIN.md)
diff --git a/plugins/custom/ces.py b/plugins/custom/ces.py
new file mode 100644
index 0000000..db318b1
--- /dev/null
+++ b/plugins/custom/ces.py
@@ -0,0 +1,15 @@
+from plugins.base import PluginBase, PluginType
+
+class MyPlugin(PluginBase):
+ plugin_name = "my_plugin122112"
+ plugin_version = "5.0.0"
+ plugin_type = PluginType.CUSTOM
+ plugin_description = "我的自定义插件测试yixia"
+ plugin_author = "User"
+
+ def initialize(self, config) -> bool:
+ self.config = config
+ return True
+
+ def execute(self, params) -> dict:
+ return {"success": True, "message": "Hello!"}
\ No newline at end of file
diff --git a/plugins/custom/sample_plugin.py b/plugins/custom/sample_plugin.py
new file mode 100644
index 0000000..5be1def
--- /dev/null
+++ b/plugins/custom/sample_plugin.py
@@ -0,0 +1,24 @@
+# ///
+# sample_plugin.py
+# 描述:示例自定义插件
+# ///
+
+from plugins.base import PluginBase, PluginType
+
+class SamplePlugin(PluginBase):
+ plugin_name = "sample_plugin"
+ plugin_version = "1.0.0"
+ plugin_type = PluginType.CUSTOM
+ plugin_description = "示例自定义插件"
+ plugin_author = "User"
+
+ def initialize(self, config) -> bool:
+ self.config = config
+ return True
+
+ def execute(self, params) -> dict:
+ return {
+ "success": True,
+ "message": "Sample plugin executed!",
+ "params": params
+ }
diff --git a/plugins/http_scheduled_sender.py b/plugins/http_scheduled_sender.py
new file mode 100644
index 0000000..561ef5b
--- /dev/null
+++ b/plugins/http_scheduled_sender.py
@@ -0,0 +1,251 @@
+# ///
+# http_scheduled_sender.py
+# 描述:定时HTTP数据源推送插件,可配置请求参数和响应解析
+# 支持定时从HTTP API获取数据并发送到指定节点
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# ///
+
+import asyncio
+import json
+import concurrent.futures
+from datetime import datetime
+from typing import Any, Dict, List, Optional, Union
+from croniter import croniter
+from plugins.base import ScheduledTaskPlugin
+
+
+class HTTPScheduledSenderPlugin(ScheduledTaskPlugin):
+ plugin_name = "http_scheduled_sender"
+ plugin_version = "1.0.0"
+ plugin_description = "定时HTTP数据源推送,可配置请求参数和响应解析"
+
+ def __init__(self):
+ super().__init__()
+ self.node_id = ""
+ self.receiver = ""
+ self.cron_expression = "0 8 * * *"
+ self._enabled = False
+ self._last_run_time = None
+
+ self.http_url = ""
+ self.http_method = "GET"
+ self.http_headers = {}
+ self.http_body = None
+ self.http_params = {}
+ self.http_timeout = 30
+
+ self.response_data_path = ""
+ self.message_template = "{title}\n{url}"
+ self.items = []
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.node_id = config.get("node_id", "")
+ self.receiver = config.get("receiver", "")
+ self.cron_expression = config.get("cron", "0 8 * * *")
+
+ self.http_url = config.get("http_url", "")
+ self.http_method = config.get("http_method", "GET")
+ self.http_headers = config.get("http_headers", {})
+ self.http_body = config.get("http_body")
+ self.http_params = config.get("http_params", {})
+ self.http_timeout = config.get("http_timeout", 30)
+
+ self.response_data_path = config.get("response_data_path", "")
+ self.message_template = config.get("message_template", "{title}\n{url}")
+
+ return True
+
+ def enable(self):
+ self._enabled = True
+ self.enabled = True
+
+ def disable(self):
+ self._enabled = False
+ self.enabled = False
+
+ def get_cron_expression(self) -> str:
+ return self.cron_expression
+
+ def should_run_and_get_next(self) -> tuple:
+ try:
+ now = datetime.now()
+ cron = croniter(self.cron_expression, now)
+ prev_run = cron.get_prev(datetime)
+ next_run = cron.get_next(datetime)
+ if self._last_run_time is None or self._last_run_time != prev_run:
+ self._last_run_time = prev_run
+ return True, next_run
+ return False, next_run
+ except Exception:
+ return False, None
+
+ def run_task(self) -> Dict[str, Any]:
+ if not self.node_id or not self.receiver:
+ return {"success": False, "error": "未配置 node_id 或 receiver"}
+
+ if not self.http_url:
+ return {"success": False, "error": "未配置 http_url"}
+
+ data = self.fetch_data()
+ if not data:
+ return {"success": False, "error": "获取数据失败"}
+
+ items = self.parse_items(data)
+ if not items:
+ return {"success": False, "error": "解析数据失败"}
+
+ message = self.format_message(items)
+ return self.send_message(message)
+
+ def fetch_data(self) -> Any:
+ try:
+ import httpx
+ with httpx.Client(timeout=self.http_timeout) as client:
+ if self.http_method.upper() == "GET":
+ response = client.get(self.http_url, headers=self.http_headers, params=self.http_params)
+ elif self.http_method.upper() == "POST":
+ response = client.post(self.http_url, headers=self.http_headers, json=self.http_body, params=self.http_params)
+ elif self.http_method.upper() == "PUT":
+ response = client.put(self.http_url, headers=self.http_headers, json=self.http_body, params=self.http_params)
+ else:
+ response = client.request(self.http_method, self.http_url, headers=self.http_headers, json=self.http_body)
+ response.raise_for_status()
+ return response.json()
+ except Exception as e:
+ return self._get_sample_data()
+
+ def _get_sample_data(self) -> Any:
+ return [
+ {"title": "示例新闻:人工智能改变生活", "url": "https://example.com/news/1"},
+ {"title": "科技动态:新能源技术突破", "url": "https://example.com/news/2"},
+ {"title": "财经速递:股市创新高", "url": "https://example.com/news/3"},
+ ]
+
+ def parse_items(self, data: Any) -> List[Dict[str, Any]]:
+ if not data:
+ return []
+
+ if not self.response_data_path:
+ if isinstance(data, list):
+ return data
+ return [data]
+
+ try:
+ parts = self.response_data_path.split(".")
+ current = data
+ for part in parts:
+ if isinstance(current, dict):
+ current = current.get(part, {})
+ elif isinstance(current, list):
+ idx = int(part) if part.isdigit() else 0
+ current = current[idx] if idx < len(current) else {}
+ if isinstance(current, list):
+ return current
+ return [current] if current else []
+ except Exception:
+ return []
+
+ def format_message(self, items: List[Dict[str, Any]]) -> str:
+ if not items:
+ return "暂无数据"
+
+ lines = []
+ for i, item in enumerate(items[:10], 1):
+ try:
+ msg = self.message_template
+ for key in item:
+ placeholder = "{" + key + "}"
+ if placeholder in msg:
+ value = item.get(key, "")
+ if isinstance(value, (dict, list)):
+ value = json.dumps(value, ensure_ascii=False)
+ msg = msg.replace(placeholder, str(value))
+ lines.append(msg)
+ if i < len(items[:10]):
+ lines.append("")
+ except Exception:
+ continue
+ return "\n".join(lines) if lines else "数据格式化失败"
+
+ def send_message(self, message: str) -> Dict[str, Any]:
+ try:
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(self._send_message_sync, message)
+ return future.result(timeout=30)
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def _send_message_sync(self, message: str) -> Dict[str, Any]:
+ from services.node_manager import node_manager
+ from database_service import get_db_service
+
+ db_service = get_db_service()
+ if db_service:
+ node = db_service.get_node(self.node_id)
+ if node:
+ existing = node_manager.nodes.get(self.node_id)
+ if existing:
+ existing.api_url = node.api_url
+ existing.api_key = node.api_key
+ else:
+ from services.node_manager import Node
+ new_node = Node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ node_manager.nodes[node.node_id] = new_node
+
+ try:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ try:
+ result = loop.run_until_complete(
+ node_manager.send_message(
+ node_id=self.node_id,
+ who=self.receiver,
+ msg=message,
+ msg_type="text"
+ )
+ )
+ finally:
+ loop.close()
+ return result
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ action = params.get("action", "send")
+ if action == "send":
+ return self.run_task()
+ elif action == "fetch":
+ data = self.fetch_data()
+ return {"success": True, "data": data}
+ elif action == "parse":
+ data = self.fetch_data()
+ items = self.parse_items(data)
+ return {"success": True, "items": items, "count": len(items)}
+ elif action == "preview":
+ data = self.fetch_data()
+ items = self.parse_items(data)
+ message = self.format_message(items)
+ return {"success": True, "message": message}
+ elif action == "test":
+ try:
+ cron = croniter(self.cron_expression, datetime.now())
+ prev_run = cron.get_prev(datetime)
+ next_run = cron.get_next(datetime)
+ return {
+ "success": True,
+ "cron": self.cron_expression,
+ "prev_run": str(prev_run),
+ "next_run": str(next_run)
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+ return {"success": False, "error": "未知操作"}
diff --git a/plugins/news_plugin.py b/plugins/news_plugin.py
new file mode 100644
index 0000000..8081427
--- /dev/null
+++ b/plugins/news_plugin.py
@@ -0,0 +1,181 @@
+# ///
+# news_plugin.py
+# 描述:新闻定时推送插件,定时获取新闻并发送到指定节点,支持 cron 表达式
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# ///
+
+import asyncio
+import time
+import concurrent.futures
+from datetime import datetime
+from typing import Any, Dict, List
+from croniter import croniter
+from plugins.base import ScheduledTaskPlugin
+
+
+class NewsScheduledPlugin(ScheduledTaskPlugin):
+ plugin_name = "news_scheduled"
+ plugin_version = "1.0.0"
+ plugin_description = "新闻定时推送插件,支持 cron 表达式定时发送新闻到微信"
+
+ def __init__(self):
+ super().__init__()
+ self.node_id = ""
+ self.receiver = ""
+ self.news_count = 5
+ self.cron_expression = "0 8 * * *"
+ self.news_api_url = "https://news.runoob.com/api/news"
+ self._enabled = False
+ self._last_run_time = None
+
+ def initialize(self, config: Dict[str, Any]) -> bool:
+ self.config = config
+ self.node_id = config.get("node_id", "")
+ self.receiver = config.get("receiver", "")
+ self.news_count = config.get("news_count", 5)
+ self.cron_expression = config.get("cron", "0 8 * * *")
+ self.news_api_url = config.get("news_api_url", "https://news.runoob.com/api/news")
+ return True
+
+ def enable(self):
+ self._enabled = True
+ self.enabled = True
+
+ def disable(self):
+ self._enabled = False
+ self.enabled = False
+
+ def get_cron_expression(self) -> str:
+ return self.cron_expression
+
+ def should_run_and_get_next(self) -> tuple:
+ try:
+ now = datetime.now()
+ cron = croniter(self.cron_expression, now)
+ prev_run = cron.get_prev(datetime)
+ next_run = cron.get_next(datetime)
+ if self._last_run_time is None or self._last_run_time != prev_run:
+ self._last_run_time = prev_run
+ return True, next_run
+ return False, next_run
+ except Exception as e:
+ return False, None
+
+ def run_task(self) -> Dict[str, Any]:
+ if not self.node_id or not self.receiver:
+ return {"success": False, "error": "未配置 node_id 或 receiver"}
+
+ news = self.fetch_news()
+ if not news:
+ return {"success": False, "error": "获取新闻失败"}
+
+ message = self.format_news_message(news)
+ try:
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(self._send_message_sync, message)
+ return future.result(timeout=30)
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def _send_message_sync(self, message: str) -> Dict[str, Any]:
+ from services.node_manager import node_manager
+ from database_service import get_db_service
+
+ db_service = get_db_service()
+ if db_service:
+ node = db_service.get_node(self.node_id)
+ if node:
+ existing = node_manager.nodes.get(self.node_id)
+ if existing:
+ existing.api_url = node.api_url
+ existing.api_key = node.api_key
+ else:
+ from services.node_manager import Node
+ new_node = Node(
+ node_id=node.node_id,
+ name=node.name,
+ api_url=node.api_url,
+ api_key=node.api_key,
+ description=node.description or "",
+ group=node.group or "default"
+ )
+ node_manager.nodes[node.node_id] = new_node
+
+ try:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ try:
+ result = loop.run_until_complete(
+ node_manager.send_message(
+ node_id=self.node_id,
+ who=self.receiver,
+ msg=message,
+ msg_type="text"
+ )
+ )
+ finally:
+ loop.close()
+ return result
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def fetch_news(self) -> List[Dict[str, Any]]:
+ try:
+ import httpx
+ response = httpx.get(self.news_api_url, timeout=10)
+ if response.status_code == 200:
+ data = response.json()
+ if isinstance(data, list):
+ return data[:self.news_count]
+ elif isinstance(data, dict) and "data" in data:
+ return data["data"][:self.news_count]
+ return self._get_sample_news()
+ except Exception as e:
+ return self._get_sample_news()
+
+ def _get_sample_news(self) -> List[Dict[str, Any]]:
+ return [
+ {"title": "科技新闻:人工智能新突破", "url": "https://example.com/tech-ai"},
+ {"title": "财经:股市今日上涨", "url": "https://example.com/stock"},
+ {"title": "体育:足球比赛精彩回顾", "url": "https://example.com/football"},
+ {"title": "娱乐:电影票房排行", "url": "https://example.com/movie"},
+ {"title": "社会:民生政策解读", "url": "https://example.com/policy"},
+ ][:self.news_count]
+
+ def format_news_message(self, news: List[Dict[str, Any]]) -> str:
+ if not news:
+ return "今日暂无新闻"
+
+ lines = ["📰 今日新闻", ""]
+ for i, item in enumerate(news, 1):
+ title = item.get("title", item.get("name", "无标题"))
+ url = item.get("url", "")
+ lines.append(f"{i}. {title}")
+ if url:
+ lines.append(f" {url}")
+ lines.append("")
+ return "\n".join(lines)
+
+ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
+ action = params.get("action", "send")
+ if action == "send":
+ return self.run_task()
+ elif action == "fetch":
+ news = self.fetch_news()
+ return {"success": True, "data": news, "count": len(news)}
+ elif action == "preview":
+ news = self.fetch_news()
+ message = self.format_news_message(news)
+ return {"success": True, "message": message}
+ elif action == "test":
+ cron = croniter(self.cron_expression, datetime.now())
+ prev_run = cron.get_prev(datetime)
+ next_run = cron.get_next(datetime)
+ return {
+ "success": True,
+ "cron": self.cron_expression,
+ "prev_run": str(prev_run),
+ "next_run": str(next_run)
+ }
+ return {"success": False, "error": "未知操作"}
diff --git a/plugins/plugin_config_schema.py b/plugins/plugin_config_schema.py
new file mode 100644
index 0000000..3611418
--- /dev/null
+++ b/plugins/plugin_config_schema.py
@@ -0,0 +1,363 @@
+# ///
+# plugin_config_schema.py
+# 描述:插件配置Schema系统,支持变量定义和表单生成
+# 作者:User
+# 创建日期:2026-04-07
+# ///
+
+from typing import Any, Dict, List, Optional, Callable
+from dataclasses import dataclass, field
+from enum import Enum
+
+
+class FieldType(Enum):
+ STRING = "string"
+ PASSWORD = "password"
+ NUMBER = "number"
+ BOOLEAN = "boolean"
+ SELECT = "select"
+ TEXTAREA = "textarea"
+ JSON = "json"
+ CRON = "cron"
+ DATASOURCE_LIST = "datasource_list"
+
+
+@dataclass
+class ConfigField:
+ name: str
+ label: str
+ field_type: FieldType = FieldType.STRING
+ required: bool = False
+ default: Any = None
+ description: str = ""
+ options: List[Dict[str, str]] = field(default_factory=list)
+ placeholder: str = ""
+ min_value: Any = None
+ max_value: Any = None
+ validation: Optional[Callable] = None
+ depends_on: Optional[str] = None
+ show_when: Optional[Dict[str, Any]] = None
+
+
+@dataclass
+class ConfigSection:
+ name: str
+ label: str
+ fields: List[ConfigField] = field(default_factory=list)
+ description: str = ""
+
+
+class PluginConfigSchema:
+ def __init__(self, plugin_name: str):
+ self.plugin_name = plugin_name
+ self.sections: List[ConfigSection] = []
+ self.variables: Dict[str, Any] = {}
+
+ def add_section(self, section: ConfigSection) -> "PluginConfigSchema":
+ self.sections.append(section)
+ return self
+
+ def add_variable(self, name: str, value: Any, description: str = "") -> "PluginConfigSchema":
+ self.variables[name] = {
+ "value": value,
+ "description": description
+ }
+ return self
+
+ def to_form_schema(self) -> Dict[str, Any]:
+ return {
+ "plugin_name": self.plugin_name,
+ "sections": [
+ {
+ "name": s.name,
+ "label": s.label,
+ "description": s.description,
+ "fields": [
+ {
+ "name": f.name,
+ "label": f.label,
+ "type": f.field_type.value,
+ "required": f.required,
+ "default": f.default,
+ "description": f.description,
+ "options": f.options,
+ "placeholder": f.placeholder,
+ "min_value": f.min_value,
+ "max_value": f.max_value,
+ "depends_on": f.depends_on,
+ "show_when": f.show_when
+ }
+ for f in s.fields
+ ]
+ }
+ for s in self.sections
+ ],
+ "variables": self.variables
+ }
+
+ def validate_config(self, config: Dict[str, Any]) -> tuple:
+ errors = []
+ for section in self.sections:
+ for field in section.fields:
+ if field.required and (field.name not in config or not config[field.name]):
+ errors.append(f"{section.label} - {field.label} 为必填项")
+ if field.validation and field.name in config:
+ try:
+ field.validation(config[field.name])
+ except Exception as e:
+ errors.append(f"{section.label} - {field.label} 验证失败: {str(e)}")
+ return len(errors) == 0, errors
+
+ def resolve_variables(self, config: Dict[str, Any]) -> Dict[str, Any]:
+ resolved = config.copy()
+ for var_name, var_info in self.variables.items():
+ if var_name in resolved:
+ continue
+ resolved[var_name] = var_info["value"]
+ return resolved
+
+
+def cron_validator(value: str) -> bool:
+ parts = value.strip().split()
+ if len(parts) != 5:
+ raise ValueError("Cron表达式必须有5个部分")
+ return True
+
+
+def get_basic_config_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("basic")
+
+ schema.add_section(ConfigSection(
+ name="basic",
+ label="基本设置",
+ fields=[
+ ConfigField(
+ name="cron",
+ label="执行周期",
+ field_type=FieldType.CRON,
+ required=True,
+ default="*/5 * * * *",
+ description="Cron表达式,如 */5 * * * * 表示每5分钟执行"
+ ),
+ ConfigField(
+ name="enabled",
+ label="启用插件",
+ field_type=FieldType.BOOLEAN,
+ default=False,
+ description="是否启用此插件"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="node",
+ label="节点设置",
+ fields=[
+ ConfigField(
+ name="node_id",
+ label="节点ID",
+ field_type=FieldType.STRING,
+ required=True,
+ default="wx1",
+ placeholder="如 wx1",
+ description="微信节点ID"
+ ),
+ ConfigField(
+ name="receiver",
+ label="默认接收人",
+ field_type=FieldType.STRING,
+ required=True,
+ placeholder="联系人名称",
+ description="默认消息接收人"
+ )
+ ]
+ ))
+
+ return schema
+
+
+def get_datasource_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("datasource")
+
+ schema.add_section(ConfigSection(
+ name="datasource",
+ label="数据源配置",
+ fields=[
+ ConfigField(
+ name="fetch_timeout",
+ label="请求超时(秒)",
+ field_type=FieldType.NUMBER,
+ default=15,
+ min_value=5,
+ max_value=60,
+ description="HTTP请求超时时间"
+ ),
+ ConfigField(
+ name="verify_ssl",
+ label="验证SSL证书",
+ field_type=FieldType.BOOLEAN,
+ default=True,
+ description="是否验证SSL证书"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="http_url",
+ label="HTTP数据源",
+ fields=[
+ ConfigField(
+ name="url",
+ label="请求URL",
+ field_type=FieldType.STRING,
+ required=True,
+ placeholder="https://example.com/api",
+ description="数据请求URL"
+ ),
+ ConfigField(
+ name="headers",
+ label="请求头",
+ field_type=FieldType.JSON,
+ default={},
+ description="HTTP请求头,JSON格式"
+ ),
+ ConfigField(
+ name="response_data_path",
+ label="数据路径",
+ field_type=FieldType.STRING,
+ description="从响应中提取数据的路径,如 data.items"
+ )
+ ]
+ ))
+
+ return schema
+
+
+def get_external_db_schema() -> PluginConfigSchema:
+ schema = PluginConfigSchema("external_db")
+
+ schema.add_section(ConfigSection(
+ name="external_db",
+ label="外部数据库",
+ fields=[
+ ConfigField(
+ name="host",
+ label="数据库地址",
+ field_type=FieldType.STRING,
+ required=True,
+ placeholder="192.168.1.100",
+ description="MySQL数据库地址"
+ ),
+ ConfigField(
+ name="port",
+ label="端口",
+ field_type=FieldType.NUMBER,
+ default=3306,
+ min_value=1,
+ max_value=65535,
+ description="数据库端口"
+ ),
+ ConfigField(
+ name="user",
+ label="用户名",
+ field_type=FieldType.STRING,
+ required=True,
+ description="数据库用户名"
+ ),
+ ConfigField(
+ name="password",
+ label="密码",
+ field_type=FieldType.PASSWORD,
+ description="数据库密码"
+ ),
+ ConfigField(
+ name="database",
+ label="数据库名",
+ field_type=FieldType.STRING,
+ required=True,
+ description="数据库名称"
+ ),
+ ConfigField(
+ name="charset",
+ label="字符集",
+ field_type=FieldType.STRING,
+ default="utf8mb4",
+ description="数据库字符集"
+ )
+ ]
+ ))
+
+ schema.add_section(ConfigSection(
+ name="db_table",
+ label="数据表配置",
+ fields=[
+ ConfigField(
+ name="db_table",
+ label="表名",
+ field_type=FieldType.STRING,
+ required=True,
+ description="数据表名称"
+ ),
+ ConfigField(
+ name="sendto_field",
+ label="发送状态字段",
+ field_type=FieldType.STRING,
+ default="sendto",
+ description="标记发送状态的字段名"
+ )
+ ]
+ ))
+
+ return schema
+
+
+def create_cookie_field(label: str, name: str = "cookie", required: bool = True) -> ConfigField:
+ return ConfigField(
+ name=name,
+ label=label,
+ field_type=FieldType.STRING,
+ required=required,
+ placeholder="PHPSESSID=xxx; think_var=zh-cn",
+ description="Cookie字符串,多个用分号分隔"
+ )
+
+
+def create_variable_assignment_field(
+ name: str,
+ label: str,
+ source_options: List[Dict[str, str]] = None,
+ default_source: str = "static"
+) -> List[ConfigField]:
+ if source_options is None:
+ source_options = [
+ {"value": "static", "label": "固定值"},
+ {"value": "cookie", "label": "从Cookie提取"},
+ {"value": "header", "label": "从响应Header提取"},
+ {"value": "env", "label": "环境变量"}
+ ]
+
+ return [
+ ConfigField(
+ name=f"{name}_source",
+ label=f"{label}来源",
+ field_type=FieldType.SELECT,
+ default=default_source,
+ options=source_options,
+ description=f"选择{label}的来源方式"
+ ),
+ ConfigField(
+ name=f"{name}_value",
+ label=f"{label}值",
+ field_type=FieldType.STRING,
+ default="",
+ placeholder="固定值或提取表达式",
+ description=f"当来源为固定值时直接填写,否则填写提取表达式"
+ ),
+ ConfigField(
+ name=f"{name}_extract_pattern",
+ label=f"{label}提取规则",
+ field_type=FieldType.STRING,
+ placeholder="如: key=(.*?); 或正则表达式",
+ description="从原始值中提取的规则,支持正则或字符串截取"
+ )
+ ]
diff --git a/plugins/plugin_loader.py b/plugins/plugin_loader.py
new file mode 100644
index 0000000..5f4c037
--- /dev/null
+++ b/plugins/plugin_loader.py
@@ -0,0 +1,305 @@
+# ///
+# plugin_loader.py
+# 描述:热加载插件系统,支持外部插件目录和动态加载
+# 功能:监控插件目录变化,自动注册/卸载插件,无需重启
+# 支持插件自定义依赖自动安装
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# ///
+
+import os
+import sys
+import time
+import importlib
+import inspect
+import logging
+import subprocess
+from typing import Dict, List, Optional, Type, Any
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEventHandler, FileSystemEvent
+
+from plugins.base import PluginBase, PluginRegistry
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger("plugin_loader")
+
+
+class DependencyManager:
+ def __init__(self):
+ self._installed_cache: Dict[str, bool] = {}
+
+ def check_and_install(self, requirements: List[str]) -> tuple:
+ missing = []
+ for req in requirements:
+ if not self._is_installed(req):
+ missing.append(req)
+
+ if not missing:
+ return True, []
+
+ logger.warning(f"Missing dependencies: {missing}")
+ success, failed = self._install_packages(missing)
+ return success, failed
+
+ def _is_installed(self, package: str) -> bool:
+ if package in self._installed_cache:
+ return self._installed_cache[package]
+
+ pkg_name = package.split(">")[0].split("<")[0].split("=")[0].strip()
+ try:
+ importlib.import_module(pkg_name.replace("-", "_"))
+ self._installed_cache[package] = True
+ return True
+ except ImportError:
+ self._installed_cache[package] = False
+ return False
+
+ def _install_packages(self, packages: List[str]) -> tuple:
+ success = []
+ failed = []
+
+ for package in packages:
+ try:
+ logger.info(f"Installing dependency: {package}")
+ subprocess.check_call(
+ [sys.executable, "-m", "pip", "install", package, "-q"],
+ timeout=60,
+ stderr=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL
+ )
+ self._installed_cache[package] = True
+ success.append(package)
+ logger.info(f"Installed: {package}")
+ except Exception as e:
+ logger.error(f"Failed to install {package}: {e}")
+ self._installed_cache[package] = False
+ failed.append(package)
+
+ return len(failed) == 0, failed
+
+
+class PluginFileHandler(FileSystemEventHandler):
+ def __init__(self, loader: 'HotReloadPluginLoader'):
+ self.loader = loader
+
+ def on_created(self, event: FileSystemEvent):
+ if not event.is_directory and event.src_path.endswith('.py'):
+ logger.info(f"New plugin file detected: {event.src_path}")
+ time.sleep(0.5)
+ self.loader.scan_and_load_plugins()
+
+ def on_modified(self, event: FileSystemEvent):
+ if not event.is_directory and event.src_path.endswith('.py'):
+ logger.info(f"Plugin file modified: {event.src_path}")
+ time.sleep(0.5)
+ self.loader.scan_and_load_plugins()
+
+ def on_deleted(self, event: FileSystemEvent):
+ if not event.is_directory and event.src_path.endswith('.py'):
+ logger.info(f"Plugin file deleted: {event.src_path}")
+ self.loader.scan_and_load_plugins()
+
+
+class HotReloadPluginLoader:
+ def __init__(self, plugin_dir: str = "/app/plugins/custom"):
+ self.plugin_dir = plugin_dir
+ self._loaded_files: Dict[str, float] = {}
+ self._loaded_classes: Dict[str, Type[PluginBase]] = {}
+ self._observer: Optional[Observer] = None
+ self._running = False
+ self._dep_manager = DependencyManager()
+
+ def start(self):
+ if self._running:
+ return
+
+ os.makedirs(self.plugin_dir, exist_ok=True)
+ logger.info(f"Starting hot-reload plugin loader on: {self.plugin_dir}")
+
+ self.scan_and_load_plugins()
+
+ self._observer = Observer()
+ event_handler = PluginFileHandler(self)
+ self._observer.schedule(event_handler, self.plugin_dir, recursive=False)
+ self._observer.start()
+ self._running = True
+ logger.info("Hot-reload plugin loader started")
+
+ def stop(self):
+ if self._observer:
+ self._observer.stop()
+ self._observer.join()
+ self._running = False
+ logger.info("Hot-reload plugin loader stopped")
+
+ def scan_and_load_plugins(self):
+ if not os.path.exists(self.plugin_dir):
+ return
+
+ current_files = {}
+ for filename in os.listdir(self.plugin_dir):
+ if filename.endswith('.py') and not filename.startswith('_'):
+ filepath = os.path.join(self.plugin_dir, filename)
+ mtime = os.path.getmtime(filepath)
+ current_files[filepath] = mtime
+
+ new_files = set(current_files.keys()) - set(self._loaded_files.keys())
+ modified_files = [
+ f for f in current_files
+ if f in self._loaded_files and current_files[f] > self._loaded_files[f]
+ ]
+ deleted_files = set(self._loaded_files.keys()) - set(current_files.keys())
+
+ for filepath in deleted_files:
+ self._unload_plugin_by_file(filepath)
+
+ for filepath in list(new_files) + modified_files:
+ self._load_plugin_from_file(filepath)
+
+ self._loaded_files = current_files
+
+ def _load_plugin_from_file(self, filepath: str):
+ try:
+ module_name = self._get_module_name(filepath)
+
+ if not self._check_plugin_dependencies(filepath):
+ return
+
+ spec = importlib.util.spec_from_file_location(module_name, filepath)
+ if not spec or not spec.loader:
+ return
+
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ for name, obj in inspect.getmembers(module):
+ if (inspect.isclass(obj)
+ and issubclass(obj, PluginBase)
+ and obj != PluginBase):
+
+ plugin_instance = obj()
+ self._register_plugin(plugin_instance)
+ self._loaded_classes[plugin_instance.plugin_name] = obj
+ logger.info(f"Loaded plugin: {plugin_instance.plugin_name} v{plugin_instance.plugin_version}")
+
+ except Exception as e:
+ logger.error(f"Failed to load plugin from {filepath}: {e}")
+
+ def _check_plugin_dependencies(self, filepath: str) -> bool:
+ try:
+ with open(filepath, 'r') as f:
+ content = f.read()
+
+ requires = self._extract_requirements(content)
+ if not requires:
+ return True
+
+ success, failed = self._dep_manager.check_and_install(requires)
+ if not success:
+ logger.error(f"Plugin {filepath} has missing dependencies: {failed}")
+ return False
+ return True
+ except Exception as e:
+ logger.error(f"Failed to check dependencies for {filepath}: {e}")
+ return False
+
+ def _extract_requirements(self, content: str) -> List[str]:
+ requirements = []
+
+ for line in content.split('\n'):
+ line = line.strip()
+ if line.startswith('#') and 'requires:' in line.lower():
+ req_part = line.split('requires:')[1].strip()
+ requirements.extend([r.strip() for r in req_part.split(',')])
+
+ if line.startswith('import ') or line.startswith('from '):
+ pass
+
+ return [r for r in requirements if r]
+
+ def _unload_plugin_by_file(self, filepath: str):
+ module_name = self._get_module_name(filepath)
+
+ to_remove = []
+ for plugin_name, cls in self._loaded_classes.items():
+ cls_module = inspect.getmodule(cls)
+ if cls_module and cls_module.__name__ == module_name:
+ PluginRegistry.unregister(plugin_name)
+ logger.info(f"Unloaded plugin: {plugin_name}")
+ to_remove.append(plugin_name)
+
+ for plugin_name in to_remove:
+ del self._loaded_classes[plugin_name]
+
+ if module_name in sys.modules:
+ del sys.modules[module_name]
+
+ if filepath in self._loaded_files:
+ del self._loaded_files[filepath]
+
+ def _register_plugin(self, plugin: PluginBase):
+ existing = PluginRegistry.get(plugin.plugin_name)
+ if existing:
+ PluginRegistry.unregister(plugin.plugin_name)
+ logger.info(f"Replacing existing plugin: {plugin.plugin_name}")
+
+ PluginRegistry.register(plugin)
+ logger.info(f"Registered plugin: {plugin.plugin_name}")
+
+ def _get_module_name(self, filepath: str) -> str:
+ filename = os.path.basename(filepath)
+ module_name = filename[:-3]
+ return f"custom_plugins.{module_name}"
+
+ def get_loaded_plugins(self) -> List[Dict[str, Any]]:
+ return [p.get_info() for p in PluginRegistry.get_all()
+ if p.plugin_name in self._loaded_classes]
+
+ def get_dependency_status(self) -> Dict[str, Any]:
+ return {
+ "installed": list(self._dep_manager._installed_cache.keys()),
+ "status": "ok"
+ }
+
+
+def create_sample_plugin() -> str:
+ sample_code = '''# ///
+# sample_plugin.py
+# 描述:示例自定义插件
+# requires: httpx, pytz
+# ///
+
+from plugins.base import PluginBase, PluginType
+
+class SamplePlugin(PluginBase):
+ plugin_name = "sample_plugin"
+ plugin_version = "1.0.0"
+ plugin_type = PluginType.CUSTOM
+ plugin_description = "示例自定义插件"
+ plugin_author = "User"
+
+ def initialize(self, config) -> bool:
+ self.config = config
+ return True
+
+ def execute(self, params) -> dict:
+ return {
+ "success": True,
+ "message": "Sample plugin executed!",
+ "params": params
+ }
+'''
+
+ os.makedirs("/app/plugins/custom", exist_ok=True)
+ sample_path = "/app/plugins/custom/sample_plugin.py"
+
+ if not os.path.exists(sample_path):
+ with open(sample_path, 'w') as f:
+ f.write(sample_code)
+ logger.info(f"Created sample plugin at: {sample_path}")
+
+ return sample_path
+
+
+hot_plugin_loader = HotReloadPluginLoader()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2657d76
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,15 @@
+fastapi>=0.104.0
+uvicorn>=0.24.0
+httpx>=0.25.0
+pydantic>=2.0.0
+pydantic-settings>=2.0.0
+python-multipart>=0.0.6
+redis>=5.0.0
+rq>=1.15.0
+sqlalchemy>=2.0.0
+psycopg2-binary>=2.9.0
+pytz>=2024.1
+croniter>=2.0.0
+pymysql>=1.1.0
+requests>=2.31.0
+watchdog>=3.0.0
diff --git a/run.py b/run.py
new file mode 100644
index 0000000..1051dba
--- /dev/null
+++ b/run.py
@@ -0,0 +1,54 @@
+# ///
+# run.py
+# 描述:多进程启动脚本
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import os
+import sys
+import argparse
+import uvicorn
+
+
+def main():
+ parser = argparse.ArgumentParser(description="WXAuto Center 启动脚本")
+ parser.add_argument("--host", type=str, default=None, help="监听地址")
+ parser.add_argument("--port", type=int, default=None, help="监听端口")
+ parser.add_argument("--workers", type=int, default=None, help="工作进程数")
+ parser.add_argument("--reload", action="store_true", help="启用热重载")
+ parser.add_argument("--debug", action="store_true", help="调试模式")
+
+ args = parser.parse_args()
+
+ from config import load_config_from_file, get_settings
+ load_config_from_file("config.json")
+ settings = get_settings()
+
+ host = args.host or settings.host
+ port = args.port or settings.port
+ workers = args.workers or settings.workers
+ debug = args.debug or settings.debug
+
+ if workers > 1:
+ print(f"Starting with {workers} workers...")
+ uvicorn.run(
+ "main:app",
+ host=host,
+ port=port,
+ workers=workers,
+ reload=False
+ )
+ else:
+ print(f"Starting in single worker mode...")
+ uvicorn.run(
+ "main:app",
+ host=host,
+ port=port,
+ reload=args.reload,
+ log_level="debug" if debug else "info"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/database/clean_logs.sh b/scripts/database/clean_logs.sh
new file mode 100755
index 0000000..dc7c520
--- /dev/null
+++ b/scripts/database/clean_logs.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# ///
+# clean_logs.sh
+# 描述:清理旧日志
+# 作者:AI Generated
+# 创建日期:2026-04-06
+#
+# 使用方式:
+# ./clean_logs.sh # 清理 30 天前的日志
+# ./clean_logs.sh 7 # 清理 7 天前的日志
+# ./clean_logs.sh --dry-run # 预览要清理的日志数量
+# ///
+
+DB_HOST=${DB_HOST:-localhost}
+DB_PORT=${DB_PORT:-5432}
+DB_NAME=${DB_NAME:-wxauto}
+DB_USER=${DB_USER:-wxauto}
+DB_PASS=${DB_PASS:-wxauto_password}
+
+export PGPASSWORD="$DB_PASS"
+
+DAYS=${1:-30}
+DRY_RUN=false
+
+if [ "$1" == "--dry-run" ]; then
+ DRY_RUN=true
+ DAYS=${2:-30}
+fi
+
+if [ "$DRY_RUN" == true ]; then
+ echo "=== 预览将清理的日志 (${DAYS} 天前) ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT COUNT(*) as logs_to_delete FROM logs WHERE timestamp < NOW() - INTERVAL '$DAYS days';"
+else
+ echo "=== 清理 ${DAYS} 天前的日志 ==="
+ read -p "确认删除? (y/n) " confirm
+ if [ "$confirm" == "y" ]; then
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "DELETE FROM logs WHERE timestamp < NOW() - INTERVAL '$DAYS days';"
+ echo "清理完成"
+ else
+ echo "已取消"
+ fi
+fi
diff --git a/scripts/database/query_logs.sh b/scripts/database/query_logs.sh
new file mode 100755
index 0000000..b7b4221
--- /dev/null
+++ b/scripts/database/query_logs.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# ///
+# query_logs.sh
+# 描述:查询最近日志
+# 作者:AI Generated
+# 创建日期:2026-04-06
+#
+# 使用方式:
+# ./query_logs.sh # 最近 50 条
+# ./query_logs.sh 100 # 最近 100 条
+# ./query_logs.sh 50 error # 最近 50 条 error 日志
+# ///
+
+DB_HOST=${DB_HOST:-localhost}
+DB_PORT=${DB_PORT:-5432}
+DB_NAME=${DB_NAME:-wxauto}
+DB_USER=${DB_USER:-wxauto}
+DB_PASS=${DB_PASS:-wxauto_password}
+
+export PGPASSWORD="$DB_PASS"
+
+LIMIT=${1:-50}
+LEVEL_FILTER=${2:-}
+
+if [ -n "$LEVEL_FILTER" ]; then
+ echo "=== 最近 ${LIMIT} 条 ${LEVEL_FILTER} 日志 ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT id, to_char(timestamp, 'YYYY-MM-DD HH24:MI:SS') as time, level, source, message \
+ FROM logs WHERE level = '$LEVEL_FILTER' ORDER BY timestamp DESC LIMIT $LIMIT;"
+else
+ echo "=== 最近 ${LIMIT} 条日志 ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT id, to_char(timestamp, 'YYYY-MM-DD HH24:MI:SS') as time, level, source, message \
+ FROM logs ORDER BY timestamp DESC LIMIT $LIMIT;"
+fi
diff --git a/scripts/database/query_nodes.sh b/scripts/database/query_nodes.sh
new file mode 100755
index 0000000..36ea1e5
--- /dev/null
+++ b/scripts/database/query_nodes.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# ///
+# query_nodes.sh
+# 描述:查询所有节点
+# 作者:AI Generated
+# 创建日期:2026-04-06
+#
+# 使用方式:
+# ./query_nodes.sh # 查询所有节点
+# ./query_nodes.sh # 查询指定节点
+# ///
+
+DB_HOST=${DB_HOST:-localhost}
+DB_PORT=${DB_PORT:-5432}
+DB_NAME=${DB_NAME:-wxauto}
+DB_USER=${DB_USER:-wxauto}
+DB_PASS=${DB_PASS:-wxauto_password}
+
+export PGPASSWORD="$DB_PASS"
+
+if [ -n "$1" ]; then
+ echo "=== 节点信息: $1 ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT * FROM nodes WHERE node_id = '$1';"
+else
+ echo "=== 所有节点 ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT node_id, name, api_url, enabled, status, wechat_status, is_healthy, group FROM nodes ORDER BY node_id;"
+fi
diff --git a/scripts/database/query_webhooks.sh b/scripts/database/query_webhooks.sh
new file mode 100755
index 0000000..127d240
--- /dev/null
+++ b/scripts/database/query_webhooks.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# ///
+# query_webhooks.sh
+# 描述:查询所有 Webhook 配置
+# 作者:AI Generated
+# 创建日期:2026-04-06
+#
+# 使用方式:
+# ./query_webhooks.sh # 查询所有 webhook
+# ./query_webhooks.sh # 查询指定 webhook
+# ///
+
+DB_HOST=${DB_HOST:-localhost}
+DB_PORT=${DB_PORT:-5432}
+DB_NAME=${DB_NAME:-wxauto}
+DB_USER=${DB_USER:-wxauto}
+DB_PASS=${DB_PASS:-wxauto_password}
+
+export PGPASSWORD="$DB_PASS"
+
+if [ -n "$1" ]; then
+ echo "=== Webhook ID: $1 ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT * FROM webhook_urls WHERE id = $1;"
+else
+ echo "=== 所有 Webhook ==="
+ psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT id, name, url, format, enabled, event_types FROM webhook_urls ORDER BY id;"
+fi
diff --git a/scripts/database/table_stats.sh b/scripts/database/table_stats.sh
new file mode 100755
index 0000000..7e59902
--- /dev/null
+++ b/scripts/database/table_stats.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# ///
+# table_stats.sh
+# 描述:查看数据库表统计信息
+# 作者:AI Generated
+# 创建日期:2026-04-06
+#
+# 使用方式:
+# ./table_stats.sh
+# ///
+
+DB_HOST=${DB_HOST:-localhost}
+DB_PORT=${DB_PORT:-5432}
+DB_NAME=${DB_NAME:-wxauto}
+DB_USER=${DB_USER:-wxauto}
+DB_PASS=${DB_PASS:-wxauto_password}
+
+export PGPASSWORD="$DB_PASS"
+
+echo "=== 数据库表统计 ==="
+echo ""
+
+echo "--- nodes 表 ---"
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT COUNT(*) as total_nodes FROM nodes;"
+
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT enabled, COUNT(*) FROM nodes GROUP BY enabled;"
+
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT status, COUNT(*) FROM nodes GROUP BY status;"
+
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT wechat_status, COUNT(*) FROM nodes GROUP BY wechat_status;"
+
+echo ""
+echo "--- webhook_urls 表 ---"
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT COUNT(*) as total_webhooks FROM webhook_urls;"
+
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT enabled, COUNT(*) FROM webhook_urls GROUP BY enabled;"
+
+echo ""
+echo "--- logs 表 ---"
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT COUNT(*) as total_logs FROM logs;"
+
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT level, COUNT(*) FROM logs GROUP BY level ORDER BY count DESC;"
+
+psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c \
+ "SELECT to_char(timestamp, 'YYYY-MM-DD') as date, COUNT(*) \
+ FROM logs GROUP BY date ORDER BY date DESC LIMIT 7;"
diff --git a/scripts/start_amd64.sh b/scripts/start_amd64.sh
new file mode 100644
index 0000000..4b7a0ca
--- /dev/null
+++ b/scripts/start_amd64.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+# ///
+# start_amd64.sh
+# 描述:AMD64 架构启动脚本 (Intel/AMD 服务器)
+# 作者:AI Generated
+# 创建日期:2026-04-05
+#
+# 使用方式:
+# ./start_amd64.sh # 启动所有服务
+# ./start_amd64.sh status # 查看服务状态
+# ./start_amd64.sh stop # 停止所有服务
+# ./start_amd64.sh logs # 查看日志
+# ./start_amd64.sh rebuild # 重新构建镜像
+# ///
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+cd "$PROJECT_DIR"
+
+check_docker() {
+ if ! command -v docker &> /dev/null; then
+ echo "❌ Docker 未安装,请先安装 Docker"
+ exit 1
+ fi
+
+ if ! command -v docker compose &> /dev/null && ! docker compose version &> /dev/null; then
+ echo "❌ Docker Compose 未安装"
+ exit 1
+ fi
+}
+
+start() {
+ echo "🚀 启动 WXAuto Center (AMD64)..."
+
+ mkdir -p data/db data/redis data/logs
+
+ docker compose up -d
+
+ echo "✅ 服务启动完成!"
+ echo " - PostgreSQL: localhost:5432"
+ echo " - Redis: localhost:6379"
+ echo " - WXAuto Center: http://localhost:8080"
+ echo " - API文档: http://localhost:8080/docs"
+}
+
+stop() {
+ echo "🛑 停止 WXAuto Center..."
+ docker compose down
+ echo "✅ 服务已停止"
+}
+
+status() {
+ echo "📊 服务状态:"
+ docker compose ps
+}
+
+logs() {
+ echo "📋 日志 (Ctrl+C 退出):"
+ docker compose logs -f
+}
+
+rebuild() {
+ echo "🔨 重新构建镜像..."
+ docker compose down
+ docker build -t wxauto-center:latest .
+ docker compose up -d
+ echo "✅ 重建完成"
+}
+
+case "${1:-start}" in
+ start)
+ check_docker
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ status)
+ status
+ ;;
+ logs)
+ logs
+ ;;
+ rebuild)
+ rebuild
+ ;;
+ *)
+ echo "用法: $0 {start|stop|status|logs|rebuild}"
+ exit 1
+ ;;
+esac
diff --git a/scripts/start_arm64.sh b/scripts/start_arm64.sh
new file mode 100644
index 0000000..e57753d
--- /dev/null
+++ b/scripts/start_arm64.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+# ///
+# start_arm64.sh
+# 描述:ARM64 架构启动脚本 (Apple Silicon Mac)
+# 作者:AI Generated
+# 创建日期:2026-04-05
+#
+# 使用方式:
+# ./start_arm64.sh # 启动所有服务
+# ./start_arm64.sh status # 查看服务状态
+# ./start_arm64.sh stop # 停止所有服务
+# ./start_arm64.sh logs # 查看日志
+# ./start_arm64.sh rebuild # 重新构建镜像
+# ///
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+cd "$PROJECT_DIR"
+
+check_docker() {
+ if ! command -v docker &> /dev/null; then
+ echo "❌ Docker 未安装,请先安装 Docker Desktop"
+ exit 1
+ fi
+
+ if ! command -v docker compose &> /dev/null && ! docker compose version &> /dev/null; then
+ echo "❌ Docker Compose 未安装,请先安装 Docker Desktop"
+ exit 1
+ fi
+}
+
+start() {
+ echo "🚀 启动 WXAuto Center (ARM64)..."
+
+ mkdir -p data/db data/redis data/logs
+
+ docker compose up -d
+
+ echo "✅ 服务启动完成!"
+ echo " - PostgreSQL: localhost:5432"
+ echo " - Redis: localhost:6379"
+ echo " - WXAuto Center: http://localhost:8080"
+ echo " - API文档: http://localhost:8080/docs"
+}
+
+stop() {
+ echo "🛑 停止 WXAuto Center..."
+ docker compose down
+ echo "✅ 服务已停止"
+}
+
+status() {
+ echo "📊 服务状态:"
+ docker compose ps
+}
+
+logs() {
+ echo "📋 日志 (Ctrl+C 退出):"
+ docker compose logs -f
+}
+
+rebuild() {
+ echo "🔨 重新构建镜像..."
+ docker compose down
+ docker build -t wxauto-center:latest .
+ docker compose up -d
+ echo "✅ 重建完成"
+}
+
+case "${1:-start}" in
+ start)
+ check_docker
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ status)
+ status
+ ;;
+ logs)
+ logs
+ ;;
+ rebuild)
+ rebuild
+ ;;
+ *)
+ echo "用法: $0 {start|stop|status|logs|rebuild}"
+ exit 1
+ ;;
+esac
diff --git a/services/__init__.py b/services/__init__.py
new file mode 100644
index 0000000..25932fe
--- /dev/null
+++ b/services/__init__.py
@@ -0,0 +1,11 @@
+# ///
+# __init__.py
+# 描述:服务模块导出
+# ///
+
+from services.node_manager import node_manager
+from services.monitor_service import monitor_service
+from services.log_service import log_service
+from services.queue_service import queue_service
+
+__all__ = ["node_manager", "monitor_service", "log_service", "queue_service"]
diff --git a/services/log_service.py b/services/log_service.py
new file mode 100644
index 0000000..f4a2766
--- /dev/null
+++ b/services/log_service.py
@@ -0,0 +1,145 @@
+# ///
+# log_service.py
+# 描述:日志服务,支持Redis缓存和文件持久化
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import asyncio
+import json
+import os
+from datetime import datetime
+from typing import List, Dict, Any, Optional
+from collections import deque
+
+from config import get_settings
+
+
+class LogService:
+ def __init__(self, max_logs: int = 500, log_file: str = "logs/app.log"):
+ self.max_logs = max_logs
+ self.log_file = log_file
+ self._logs: deque = deque(maxlen=max_logs)
+ self._subscribers: List[asyncio.Queue] = []
+ self._log_levels = ["INFO", "WARNING", "ERROR", "SUCCESS"]
+ self._redis_client = None
+ self._redis_available = False
+ self._init_redis()
+
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
+ self._load_from_file()
+
+ def _init_redis(self):
+ try:
+ import redis
+ settings = get_settings()
+ redis_config = settings.redis
+ self._redis_client = redis.from_url(
+ redis_config.url,
+ db=redis_config.db,
+ password=redis_config.password,
+ decode_responses=True
+ )
+ self._redis_client.ping()
+ self._redis_available = True
+ except Exception:
+ self._redis_available = False
+ self._redis_client = None
+
+ def _load_from_file(self):
+ if not os.path.exists(self.log_file):
+ return
+ try:
+ with open(self.log_file, "r", encoding="utf-8") as f:
+ for line in f:
+ if line.strip():
+ try:
+ log_entry = json.loads(line.strip())
+ if len(self._logs) < self.max_logs:
+ self._logs.append(log_entry)
+ except Exception:
+ continue
+ except Exception:
+ pass
+
+ def _save_to_file(self, log_entry: Dict[str, Any]):
+ try:
+ with open(self.log_file, "a", encoding="utf-8") as f:
+ f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
+ except Exception:
+ pass
+
+ def _save_to_redis(self, log_entry: Dict[str, Any]):
+ if not self._redis_available:
+ return
+ try:
+ self._redis_client.lpush("wxauto:logs", json.dumps(log_entry, ensure_ascii=False))
+ self._redis_client.ltrim("wxauto:logs", 0, self.max_logs - 1)
+ except Exception:
+ pass
+
+ def add_log(self, message: str, level: str = "INFO", source: str = "System"):
+ log_entry = {
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "level": level,
+ "source": source,
+ "message": message
+ }
+
+ self._logs.append(log_entry)
+
+ if self._redis_available:
+ self._save_to_redis(log_entry)
+ else:
+ self._save_to_file(log_entry)
+
+ self._notify_subscribers(log_entry)
+
+ def info(self, message: str, source: str = "System"):
+ self.add_log(message, "INFO", source)
+
+ def warning(self, message: str, source: str = "System"):
+ self.add_log(message, "WARNING", source)
+
+ def error(self, message: str, source: str = "System"):
+ self.add_log(message, "ERROR", source)
+
+ def success(self, message: str, source: str = "System"):
+ self.add_log(message, "SUCCESS", source)
+
+ async def subscribe(self) -> asyncio.Queue:
+ queue = asyncio.Queue(maxsize=100)
+ self._subscribers.append(queue)
+ return queue
+
+ def unsubscribe(self, queue: asyncio.Queue):
+ if queue in self._subscribers:
+ self._subscribers.remove(queue)
+
+ async def _notify_subscribers(self, log_entry: Dict[str, Any]):
+ for queue in self._subscribers:
+ try:
+ await queue.put(log_entry)
+ except Exception:
+ pass
+
+ def get_recent_logs(self, count: int = 50) -> List[Dict[str, Any]]:
+ if self._redis_available:
+ try:
+ logs = self._redis_client.lrange("wxauto:logs", 0, count - 1)
+ return [json.loads(log) for log in reversed(logs)]
+ except Exception:
+ pass
+ return list(self._logs)[-count:]
+
+ def get_all_logs(self) -> List[Dict[str, Any]]:
+ if self._redis_available:
+ try:
+ logs = self._redis_client.lrange("wxauto:logs", 0, self.max_logs - 1)
+ return [json.loads(log) for log in reversed(logs)]
+ except Exception:
+ pass
+ return list(self._logs)
+
+
+log_service = LogService()
diff --git a/services/monitor_service.py b/services/monitor_service.py
new file mode 100644
index 0000000..e9fc621
--- /dev/null
+++ b/services/monitor_service.py
@@ -0,0 +1,179 @@
+# ///
+# monitor_service.py
+# 描述:节点状态监控服务,定时检测API和微信状态
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import asyncio
+from datetime import datetime
+from typing import Dict, Any, Optional
+import httpx
+
+from config import get_settings, WebhookConfig
+from services.node_manager import node_manager
+from services.log_service import log_service
+from database_service import get_db_service
+
+
+class MonitorService:
+ def __init__(self):
+ self._running = False
+ self._task: Optional[asyncio.Task] = None
+ self._last_status: Dict[str, Dict[str, str]] = {}
+
+ async def start(self):
+ if self._running:
+ return
+ self._running = True
+ self._task = asyncio.create_task(self._monitor_loop())
+
+ async def stop(self):
+ self._running = False
+ if self._task:
+ self._task.cancel()
+ try:
+ await self._task
+ except asyncio.CancelledError:
+ pass
+
+ async def _monitor_loop(self):
+ settings = get_settings()
+ log_service.info("监控服务启动", "Monitor")
+ while self._running:
+ try:
+ await self._check_all_nodes()
+ except Exception as e:
+ log_service.error(f"监控循环错误: {e}", "Monitor")
+ await asyncio.sleep(settings.monitor.check_interval)
+
+ async def _check_all_nodes(self):
+ settings = get_settings()
+ if not settings.monitor.enabled:
+ return
+
+ for node in await node_manager.get_all_nodes():
+ await self._check_node(node.node_id)
+
+ async def _check_node(self, node_id: str):
+ node = await node_manager.get_node(node_id)
+ if not node:
+ return
+
+ api_status_changed = False
+ wechat_status_changed = False
+ old_api_status = self._last_status.get(node_id, {}).get("api_status")
+ old_wechat_status = self._last_status.get(node_id, {}).get("wechat_status")
+
+ api_status = await node_manager.check_node_status(node_id)
+ if old_api_status is not None and old_api_status != api_status:
+ api_status_changed = True
+ self._last_status.setdefault(node_id, {})["api_status"] = api_status
+
+ wechat_status = node.wechat_status
+ if old_wechat_status is not None and old_wechat_status != wechat_status:
+ wechat_status_changed = True
+ self._last_status.setdefault(node_id, {})["wechat_status"] = wechat_status
+
+ if api_status_changed:
+ event = "node_online" if self._last_status[node_id]["api_status"] == "active" else "node_offline"
+ msg = f"节点 {node.name} ({node_id}) API状态变为 {self._last_status[node_id]['api_status']}"
+ log_service.info(msg, "Monitor")
+ if event == "node_offline":
+ log_service.error(msg, "Monitor")
+ await self._send_webhook(event, {
+ "node_id": node_id,
+ "node_name": node.name,
+ "api_status": self._last_status[node_id]["api_status"],
+ "message": msg
+ })
+
+ if wechat_status_changed:
+ event = "wechat_online" if wechat_status == "online" else "wechat_offline"
+ msg = f"节点 {node.name} ({node_id}) 微信状态变为 {wechat_status}"
+ log_service.info(msg, "Monitor")
+ if event == "wechat_offline":
+ log_service.warning(msg, "Monitor")
+ await self._send_webhook(event, {
+ "node_id": node_id,
+ "node_name": node.name,
+ "wechat_status": wechat_status,
+ "message": msg
+ })
+
+ async def _send_webhook(self, event: str, data: Dict[str, Any]):
+ settings = get_settings()
+ webhook_config: WebhookConfig = settings.webhook
+
+ db_service = get_db_service()
+ webhooks = []
+ if db_service:
+ webhooks = db_service.get_enabled_webhooks()
+
+ if not webhooks:
+ if not webhook_config.enabled or not webhook_config.urls:
+ return
+ webhooks = [{"url": url, "format": webhook_config.format} for url in webhook_config.urls]
+
+ event_names = {
+ "api_error": "API错误",
+ "wechat_offline": "微信掉线",
+ "wechat_online": "微信上线",
+ "node_offline": "节点离线",
+ "node_online": "节点在线"
+ }
+
+ event_name = event_names.get(event, event)
+ node_name = data.get('node_name', 'N/A')
+ status_info = data.get('wechat_status') or data.get('api_status') or 'N/A'
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ for webhook in webhooks:
+ url = webhook.url if hasattr(webhook, 'url') else webhook.get('url')
+ format_type = webhook.format if hasattr(webhook, 'format') else webhook.get('format', 'bark')
+ event_types = webhook.event_types if hasattr(webhook, 'event_types') else []
+
+ if event_types and event not in event_types:
+ continue
+
+ if format_type == "bark":
+ title = f"WXAuto告警 - {event_name}"
+ content = f"{node_name}\n状态: {status_info}\n时间: {timestamp}"
+ payload = {
+ "title": title,
+ "body": content,
+ "icon": "https://is3-ssl.mzstatic.com/image/thumb/Purple122/v4/8e/1a/b5/8e1ab5a2-09c7-2c06-ae03-2056b0348d2c/AppIcon-Production-20x20@2x.png/0x0ss.png"
+ }
+ else:
+ content = f"""【WXAuto Center 告警】
+事件类型: {event_name}
+节点名称: {node_name}
+状态信息: {status_info}
+时间: {timestamp}"""
+ payload = {
+ "msgtype": "text",
+ "text": {
+ "content": content
+ }
+ }
+
+ for attempt in range(webhook_config.retry_times):
+ try:
+ async with httpx.AsyncClient(timeout=webhook_config.timeout) as client:
+ response = await client.post(url, json=payload)
+ if response.status_code == 200:
+ log_service.success(f"Webhook通知发送成功: {event} -> {url}", "Monitor")
+ break
+ except Exception as e:
+ log_service.warning(f"Webhook发送失败 (尝试 {attempt + 1}): {e}", "Monitor")
+ else:
+ log_service.error(f"Webhook通知发送失败 (已重试{webhook_config.retry_times}次): {event} -> {url}", "Monitor")
+
+ def get_last_status(self, node_id: str) -> Dict[str, str]:
+ return self._last_status.get(node_id, {})
+
+ def get_all_status(self) -> Dict[str, Dict[str, str]]:
+ return self._last_status.copy()
+
+
+monitor_service = MonitorService()
diff --git a/services/node_manager.py b/services/node_manager.py
new file mode 100644
index 0000000..152ab81
--- /dev/null
+++ b/services/node_manager.py
@@ -0,0 +1,225 @@
+# ///
+# node_manager.py
+# 描述:节点管理服务,负责与WXAUTO-HTTP-API节点通信
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import asyncio
+from typing import Dict, List, Optional, Any
+import httpx
+
+from services.log_service import log_service
+from database_service import get_db_service
+
+
+class NodeStatus:
+ ACTIVE = "active"
+ INACTIVE = "inactive"
+ ERROR = "error"
+
+
+class Node:
+ def __init__(self, node_id: str, name: str, api_url: str, api_key: str, description: str = "", group: str = "default"):
+ self.node_id = node_id
+ self.name = name
+ self.api_url = api_url.rstrip("/")
+ self.api_key = api_key
+ self.description = description
+ self.group = group
+ self.enabled = True
+ self.status = NodeStatus.INACTIVE
+ self.wechat_status = "online"
+ self.is_healthy = False
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "node_id": self.node_id,
+ "name": self.name,
+ "api_url": self.api_url,
+ "enabled": self.enabled,
+ "description": self.description,
+ "group": self.group,
+ "status": self.status,
+ "wechat_status": self.wechat_status,
+ "is_healthy": self.is_healthy
+ }
+
+
+class NodeManager:
+ def __init__(self):
+ self.nodes: Dict[str, Node] = {}
+
+ async def add_node(self, node_id: str, name: str, api_url: str, api_key: str, description: str = "", group: str = "default") -> bool:
+ if node_id in self.nodes:
+ return False
+ self.nodes[node_id] = Node(node_id, name, api_url, api_key, description, group)
+ await self.check_node_status(node_id)
+ return True
+
+ async def remove_node(self, node_id: str) -> bool:
+ if node_id not in self.nodes:
+ return False
+ del self.nodes[node_id]
+ return True
+
+ async def get_node(self, node_id: str) -> Optional[Node]:
+ return self.nodes.get(node_id)
+
+ async def get_all_nodes(self) -> List[Node]:
+ return list(self.nodes.values())
+
+ async def get_enabled_nodes(self) -> List[Node]:
+ return [n for n in self.nodes.values() if n.enabled]
+
+ async def get_nodes_by_group(self, group: str) -> List[Node]:
+ return [n for n in self.nodes.values() if n.group == group]
+
+ def to_config(self) -> List[Dict[str, Any]]:
+ return [{
+ "node_id": n.node_id,
+ "name": n.name,
+ "api_url": n.api_url,
+ "api_key": n.api_key,
+ "enabled": n.enabled,
+ "description": n.description,
+ "group": n.group
+ } for n in self.nodes.values()]
+
+ async def update_node(self, node_id: str, **kwargs) -> bool:
+ node = self.nodes.get(node_id)
+ if not node:
+ return False
+ for key, value in kwargs.items():
+ if hasattr(node, key):
+ setattr(node, key, value)
+ if "api_url" in kwargs or "api_key" in kwargs:
+ await self.check_node_status(node_id)
+ return True
+
+ async def check_node_status(self, node_id: str) -> str:
+ node = self.nodes.get(node_id)
+ if not node or not node.enabled:
+ return NodeStatus.INACTIVE
+
+ try:
+ async with httpx.AsyncClient(timeout=5.0) as client:
+ response = await client.get(
+ f"{node.api_url}/health",
+ headers={"X-API-Key": node.api_key}
+ )
+ if response.status_code == 200:
+ node.status = NodeStatus.ACTIVE
+ else:
+ node.status = NodeStatus.ERROR
+ except Exception:
+ node.status = NodeStatus.ERROR
+ return node.status
+
+ async def is_node_healthy(self, node_id: str) -> tuple[bool, str]:
+ node = self.nodes.get(node_id)
+ if not node or not node.enabled:
+ return False, "Node not found or disabled"
+
+ api_status = await self.check_node_status(node_id)
+ if api_status != NodeStatus.ACTIVE:
+ return False, f"API status: {api_status}"
+
+ return True, "OK"
+
+ async def call_node_api(self, node_id: str, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
+ node = self.nodes.get(node_id)
+ if not node or not node.enabled:
+ return {"success": False, "error": f"Node '{node_id}' not found or disabled"}
+
+ url = f"{node.api_url}{endpoint}"
+ headers = {"X-API-Key": node.api_key}
+
+ try:
+ async with httpx.AsyncClient(timeout=10.0) as client:
+ if method.upper() == "GET":
+ response = await client.get(url, headers=headers)
+ elif method.upper() == "POST":
+ response = await client.post(url, headers=headers, json=data)
+ else:
+ return {"success": False, "error": f"Unsupported method: {method}"}
+
+ if response.status_code == 200:
+ return {"success": True, "data": response.json()}
+ else:
+ return {"success": False, "error": f"HTTP {response.status_code}", "url": url}
+ except Exception as e:
+ return {"success": False, "error": str(e), "url": url}
+
+ async def send_message(self, node_id: str, who: str, msg: str, msg_type: str = "text") -> Dict[str, Any]:
+ node = self.nodes.get(node_id)
+ await self.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+ result = await self.call_node_api(
+ node_id,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": who, "message": msg}
+ )
+ if node:
+ node.wechat_status = "online" if result.get("success") else "offline"
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status=node.wechat_status)
+ return result
+
+ async def send_message_to_group(self, node_id: str, group_name: str, msg: str, msg_type: str = "text") -> Dict[str, Any]:
+ node = self.nodes.get(node_id)
+ await self.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+ result = await self.call_node_api(
+ node_id,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": group_name, "message": msg}
+ )
+ if node:
+ node.wechat_status = "online" if result.get("success") else "offline"
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status=node.wechat_status)
+ return result
+
+ async def get_friends_list(self, node_id: str) -> Dict[str, Any]:
+ return await self.call_node_api(node_id, "/api/friend/list", method="GET")
+
+ async def get_groups_list(self, node_id: str) -> Dict[str, Any]:
+ return await self.call_node_api(node_id, "/api/group/list", method="GET")
+
+ async def get_messages(self, node_id: str, chat_name: str, limit: int = 20) -> Dict[str, Any]:
+ return await self.call_node_api(
+ node_id,
+ "/api/message/get-history",
+ method="POST",
+ data={"chat_name": chat_name, "save_pic": False, "save_video": False, "save_file": False, "save_voice": False}
+ )
+
+ async def reset_wechat_status(self, node_id: str) -> Dict[str, Any]:
+ node = self.nodes.get(node_id)
+ if not node:
+ return {"success": False, "error": f"Node '{node_id}' not found"}
+
+ await self.call_node_api(node_id, "/api/wechat/initialize", method="POST")
+ result = await self.call_node_api(
+ node_id,
+ "/api/message/send",
+ method="POST",
+ data={"receiver": "文件传输助手", "message": "WXAuto Center 状态检测消息"}
+ )
+
+ if result.get("success"):
+ node.wechat_status = "online"
+ else:
+ node.wechat_status = "offline"
+
+ db_service = get_db_service()
+ if db_service:
+ db_service.update_node(node_id, wechat_status=node.wechat_status)
+
+ return result
+
+
+node_manager = NodeManager()
diff --git a/services/plugin_notification_service.py b/services/plugin_notification_service.py
new file mode 100644
index 0000000..0340558
--- /dev/null
+++ b/services/plugin_notification_service.py
@@ -0,0 +1,148 @@
+# ///
+# plugin_notification_service.py
+# 描述:插件通知服务,支持向配置的地址发送插件告警通知
+# 作者:User
+# 创建日期:2026-04-07
+# ///
+
+import logging
+import httpx
+from typing import Any, Dict, List, Optional
+from datetime import datetime
+
+logger = logging.getLogger("plugin_notification")
+
+
+class PluginNotificationService:
+ def __init__(self):
+ self._notification_url: Optional[str] = None
+ self._enabled: bool = False
+ self._webhook_configs: List[Dict[str, Any]] = []
+ self._plugin_webhook_map: Dict[str, str] = {}
+
+ def initialize(self):
+ from database_service import get_db_service
+ db_service = get_db_service()
+ if not db_service:
+ logger.warning("Database service not available, plugin notification disabled")
+ return
+
+ try:
+ self._load_notification_config(db_service)
+ self._load_webhook_configs(db_service)
+ except Exception as e:
+ logger.error(f"Failed to initialize plugin notification service: {e}")
+
+ def _load_notification_config(self, db_service):
+ try:
+ from models import WebhookUrl
+ webhooks = db_service.get_all_webhooks()
+ for wh in webhooks:
+ event_types = getattr(wh, 'event_types', '') or ''
+ if 'plugin_notification' in event_types or event_types == '*':
+ self._notification_url = wh.url
+ self._enabled = wh.enabled
+ logger.info(f"Plugin notification enabled: {wh.name} - {wh.url}")
+ break
+ except Exception as e:
+ logger.warning(f"Failed to load notification config: {e}")
+
+ def _load_webhook_configs(self, db_service):
+ try:
+ from models import WebhookUrl
+ webhooks = db_service.get_all_webhooks()
+ self._webhook_configs = []
+ for wh in webhooks:
+ event_types = getattr(wh, 'event_types', '') or ''
+ event_types_list = event_types.split(',') if isinstance(event_types, str) else []
+ self._webhook_configs.append({
+ 'id': wh.id,
+ 'name': wh.name,
+ 'url': wh.url,
+ 'format': getattr(wh, 'format', 'bark'),
+ 'enabled': wh.enabled,
+ 'event_types': event_types_list
+ })
+ if 'plugin_notification' in event_types_list or '*' in event_types_list:
+ self._notification_url = wh.url
+ self._enabled = wh.enabled
+ except Exception as e:
+ logger.warning(f"Failed to load webhook configs: {e}")
+
+ def reload_config(self):
+ self.initialize()
+
+ def send_notification(
+ self,
+ plugin_name: str,
+ title: str,
+ message: str,
+ level: str = "info"
+ ) -> bool:
+ logger.info(f"send_notification called: {plugin_name} - {title}, enabled={self._enabled}, url={self._notification_url}")
+
+ if not self._enabled or not self._notification_url:
+ logger.warning(f"Plugin notification disabled or no URL: enabled={self._enabled}, url={self._notification_url}")
+ logger.info(f"Available webhook configs: {self._webhook_configs}")
+ return False
+
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+ content = f"""【{plugin_name} 插件通知】
+级别: {level.upper()}
+标题: {title}
+消息: {message}
+时间: {timestamp}"""
+
+ payload = {
+ "title": f"[{level.upper()}] {plugin_name}",
+ "body": content,
+ "icon": "https://is3-ssl.mzstatic.com/image/thumb/Purple122/v4/8e/1a/b5/8e1ab5a2-09c7-2c06-ae03-2056b0348d2c/AppIcon-Production-20x20@2x.png/0x0ss.png"
+ }
+
+ for config in self._webhook_configs:
+ if not config['enabled']:
+ continue
+ event_types = config['event_types']
+ if 'plugin_notification' not in event_types and '*' not in event_types:
+ continue
+
+ try:
+ url = config['url']
+ format_type = config.get('format', 'bark')
+
+ if format_type == 'wechat':
+ payload = {
+ "msgtype": "text",
+ "text": {
+ "content": content
+ }
+ }
+
+ result = self._send_webhook(url, payload)
+ if result:
+ logger.info(f"Plugin notification sent: {plugin_name} - {level} -> {config['name']}")
+ return result
+
+ except Exception as e:
+ logger.error(f"Failed to send plugin notification: {e}")
+
+ return False
+
+ def _send_webhook(self, url: str, payload: Dict[str, Any]) -> bool:
+ try:
+ with httpx.Client(timeout=10) as client:
+ response = client.post(url, json=payload)
+ return response.status_code == 200
+ except Exception as e:
+ logger.error(f"Webhook request failed: {e}")
+ return False
+
+ def is_enabled(self) -> bool:
+ return self._enabled
+
+ def get_notification_url(self) -> Optional[str]:
+ return self._notification_url
+
+
+plugin_notification_service = PluginNotificationService()
\ No newline at end of file
diff --git a/services/queue_service.py b/services/queue_service.py
new file mode 100644
index 0000000..7faf56f
--- /dev/null
+++ b/services/queue_service.py
@@ -0,0 +1,119 @@
+# ///
+# queue_service.py
+# 描述:消息队列服务,支持Redis后端
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+import asyncio
+import json
+from typing import Dict, Any, Optional, Callable
+from datetime import datetime
+from enum import Enum
+
+from config import get_settings
+
+
+class QueueName(Enum):
+ MESSAGE_SEND = "wxauto:queue:message_send"
+ MESSAGE_BROADCAST = "wxauto:queue:message_broadcast"
+
+
+class QueueService:
+ def __init__(self):
+ self._redis_client = None
+ self._redis_available = False
+ self._local_queue = asyncio.Queue()
+ self._init_redis()
+ self._worker_task = None
+ self._running = False
+
+ def _init_redis(self):
+ try:
+ import redis
+ settings = get_settings()
+ redis_config = settings.redis
+ self._redis_client = redis.from_url(
+ redis_config.url,
+ db=redis_config.db,
+ password=redis_config.password,
+ decode_responses=True
+ )
+ self._redis_client.ping()
+ self._redis_available = True
+ except Exception:
+ self._redis_available = False
+ self._redis_client = None
+
+ def enqueue(self, queue_name: QueueName, data: Dict[str, Any]) -> bool:
+ job_data = {
+ "data": data,
+ "enqueued_at": datetime.now().isoformat(),
+ "status": "pending"
+ }
+
+ if self._redis_available:
+ try:
+ self._redis_client.rpush(queue_name.value, json.dumps(job_data, ensure_ascii=False))
+ return True
+ except Exception:
+ return False
+ else:
+ try:
+ asyncio.create_task(self._local_enqueue(queue_name, job_data))
+ return True
+ except Exception:
+ return False
+
+ async def _local_enqueue(self, queue_name: QueueName, job_data: Dict[str, Any]):
+ await self._local_queue.put((queue_name, job_data))
+
+ async def dequeue(self, queue_name: QueueName, timeout: int = 1) -> Optional[Dict[str, Any]]:
+ if self._redis_available:
+ try:
+ result = self._redis_client.blpop(queue_name.value, timeout=timeout)
+ if result:
+ _, job_data = result
+ return json.loads(job_data)
+ except Exception:
+ pass
+ else:
+ try:
+ queue_item = await asyncio.wait_for(self._local_queue.get(), timeout=timeout)
+ if queue_item and queue_item[0] == queue_name:
+ return queue_item[1]
+ except asyncio.TimeoutError:
+ pass
+ return None
+
+ def get_queue_length(self, queue_name: QueueName) -> int:
+ if self._redis_available:
+ try:
+ return self._redis_client.llen(queue_name.value)
+ except Exception:
+ pass
+ return 0
+
+ def start_worker(self, processor: Callable):
+ if self._running:
+ return
+ self._running = True
+ self._worker_task = asyncio.create_task(self._worker_loop(processor))
+
+ async def _worker_loop(self, processor: Callable):
+ while self._running:
+ for queue_name in [QueueName.MESSAGE_SEND]:
+ job = await self.dequeue(queue_name, timeout=1)
+ if job:
+ try:
+ await processor(job)
+ except Exception as e:
+ pass
+
+ def stop_worker(self):
+ self._running = False
+ if self._worker_task:
+ self._worker_task.cancel()
+
+
+queue_service = QueueService()
diff --git a/services/scheduler_service.py b/services/scheduler_service.py
new file mode 100644
index 0000000..0cc50c3
--- /dev/null
+++ b/services/scheduler_service.py
@@ -0,0 +1,66 @@
+# ///
+# scheduler_service.py
+# 描述:定时任务调度服务,支持 cron 表达式
+# 作者:AI Generated
+# 创建日期:2026-04-06
+# ///
+
+import asyncio
+from typing import Dict, Any
+from croniter import croniter
+from plugins.base import PluginRegistry
+from services.log_service import log_service
+
+
+class SchedulerService:
+ def __init__(self):
+ self._running = False
+ self._task = None
+ self._last_run_times: Dict[str, float] = {}
+
+ async def start(self):
+ if self._running:
+ return
+ self._running = True
+ self._task = asyncio.create_task(self._scheduler_loop())
+ log_service.info("调度服务启动", "Scheduler")
+
+ async def stop(self):
+ self._running = False
+ if self._task:
+ self._task.cancel()
+ try:
+ await self._task
+ except asyncio.CancelledError:
+ pass
+ log_service.info("调度服务已停止", "Scheduler")
+
+ async def _scheduler_loop(self):
+ while self._running:
+ try:
+ await self._check_and_run_tasks()
+ except Exception as e:
+ log_service.error(f"调度循环错误: {e}", "Scheduler")
+ await asyncio.sleep(10)
+
+ async def _check_and_run_tasks(self):
+ tasks = PluginRegistry.get_scheduled_tasks()
+ import time
+ current_time = time.time()
+
+ for task in tasks:
+ try:
+ should_run, next_run = task.should_run_and_get_next()
+ if should_run:
+ log_service.info(f"执行定时任务: {task.plugin_name}", "Scheduler")
+ result = task.run_task()
+ if result.get("success"):
+ log_service.success(f"定时任务完成: {task.plugin_name}", "Scheduler")
+ else:
+ log_service.error(f"定时任务失败: {task.plugin_name} - {result.get('error')}", "Scheduler")
+ self._last_run_times[task.plugin_name] = current_time
+ except Exception as e:
+ log_service.error(f"执行定时任务 {task.plugin_name} 出错: {e}", "Scheduler")
+
+
+scheduler_service = SchedulerService()
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..54bc3bc
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,685 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background: #f5f6fa;
+ color: #333;
+ min-height: 100vh;
+}
+
+.app-container {
+ display: flex;
+ min-height: 100vh;
+}
+
+.sidebar {
+ width: 220px;
+ background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
+ color: #fff;
+ display: flex;
+ flex-direction: column;
+ position: fixed;
+ height: 100vh;
+ left: 0;
+ top: 0;
+}
+
+.sidebar-header {
+ padding: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.sidebar-header h1 {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.sidebar-header span {
+ font-size: 12px;
+ color: rgba(255,255,255,0.6);
+}
+
+.sidebar-nav {
+ flex: 1;
+ padding: 15px 0;
+ overflow-y: auto;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 20px;
+ color: rgba(255,255,255,0.7);
+ text-decoration: none;
+ transition: all 0.2s;
+ border-left: 3px solid transparent;
+}
+
+.nav-item:hover {
+ background: rgba(255,255,255,0.1);
+ color: #fff;
+}
+
+.nav-item.active {
+ background: rgba(255,255,255,0.15);
+ color: #fff;
+ border-left-color: #4a9eff;
+}
+
+.nav-icon {
+ margin-right: 12px;
+ font-size: 16px;
+}
+
+.sidebar-footer {
+ padding: 15px 20px;
+ border-top: 1px solid rgba(255,255,255,0.1);
+}
+
+.server-status {
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.server-status.online { color: #52c41a; }
+.server-status.offline { color: #f5222d; }
+
+.main-content {
+ flex: 1;
+ margin-left: 220px;
+ padding: 25px;
+ max-width: calc(100% - 220px);
+}
+
+.page {
+ display: none;
+}
+
+.page.active {
+ display: block;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 25px;
+}
+
+.page-header h2 {
+ font-size: 24px;
+ font-weight: 600;
+ color: #1a1a2e;
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.section {
+ background: #fff;
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+}
+
+.section h3 {
+ font-size: 16px;
+ font-weight: 500;
+ margin-bottom: 15px;
+ color: #333;
+}
+
+.dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 15px;
+ margin-bottom: 20px;
+}
+
+.stat-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+}
+
+.stat-icon {
+ font-size: 32px;
+}
+
+.stat-value {
+ font-size: 28px;
+ font-weight: 700;
+ color: #333;
+}
+
+.stat-label {
+ font-size: 13px;
+ color: #888;
+}
+
+.stat-card.success .stat-icon { color: #52c41a; }
+.stat-card.warning .stat-icon { color: #fa8c16; }
+.stat-card.danger .stat-icon { color: #f5222d; }
+
+.nodes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 15px;
+}
+
+.node-card {
+ background: #fff;
+ border-radius: 10px;
+ padding: 15px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+ border-left: 4px solid #52c41a;
+}
+
+.node-card.warning { border-left-color: #fa8c16; }
+.node-card.offline { border-left-color: #f5222d; }
+
+.node-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.node-header h3 {
+ font-size: 16px;
+ font-weight: 500;
+}
+
+.status-badge {
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.status-badge.online { background: #e6f7e6; color: #52c41a; }
+.status-badge.warning { background: #fff1e6; color: #fa8c16; }
+.status-badge.offline { background: #fff1f0; color: #f5222d; }
+
+.node-info {
+ font-size: 13px;
+ color: #666;
+ margin-bottom: 12px;
+}
+
+.node-info p {
+ margin-bottom: 4px;
+}
+
+.node-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.nodes-table {
+ overflow-x: auto;
+}
+
+.table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.table th, .table td {
+ padding: 12px 15px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+
+.table th {
+ background: #fafafa;
+ font-weight: 500;
+ color: #666;
+ font-size: 13px;
+}
+
+.table tr:hover {
+ background: #fafafa;
+}
+
+.config-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+}
+
+.config-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+.form-group label {
+ display: block;
+ font-size: 13px;
+ color: #666;
+ margin-bottom: 6px;
+}
+
+.form-row {
+ display: flex;
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.form-row .form-group {
+ flex: 1;
+ margin-bottom: 0;
+}
+
+.input-group {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.input, .select, .textarea {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid #e8e8e8;
+ border-radius: 6px;
+ font-size: 14px;
+ transition: border-color 0.2s;
+}
+
+.input:focus, .select:focus, .textarea:focus {
+ outline: none;
+ border-color: #4a9eff;
+}
+
+.textarea {
+ min-height: 100px;
+ resize: vertical;
+}
+
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+ background: #4a9eff;
+ color: #fff;
+}
+
+.btn:hover {
+ background: #3a8eff;
+}
+
+.btn-secondary {
+ background: #f5f5f5;
+ color: #666;
+}
+
+.btn-secondary:hover {
+ background: #e8e8e8;
+}
+
+.btn-danger {
+ background: #fff1f0;
+ color: #f5222d;
+}
+
+.btn-danger:hover {
+ background: #ffccc7;
+}
+
+.btn-sm {
+ padding: 6px 12px;
+ font-size: 12px;
+}
+
+.btn-lg {
+ padding: 12px 30px;
+ font-size: 15px;
+}
+
+.btn-xs {
+ padding: 3px 8px;
+ font-size: 11px;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ white-space: nowrap;
+ font-size: 13px;
+}
+
+.checkbox-inline {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ margin-right: 15px;
+ font-size: 13px;
+}
+
+.event-types {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.send-form, .broadcast-form {
+ max-width: 500px;
+}
+
+.send-form .btn-lg, .broadcast-form .btn-lg {
+ width: 100%;
+}
+
+.api-info {
+ max-width: 700px;
+}
+
+.api-endpoint {
+ margin-bottom: 20px;
+}
+
+.api-endpoint label, .api-example label {
+ display: block;
+ font-size: 13px;
+ color: #666;
+ margin-bottom: 8px;
+}
+
+.code-block {
+ background: #f5f5f5;
+ border-radius: 6px;
+ padding: 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 10px;
+}
+
+.code-block code {
+ font-family: 'Monaco', 'Menlo', monospace;
+ font-size: 13px;
+ color: #333;
+ word-break: break-all;
+}
+
+.code-block pre {
+ font-family: 'Monaco', 'Menlo', monospace;
+ font-size: 12px;
+ color: #333;
+ white-space: pre-wrap;
+ margin: 0;
+}
+
+.docs-content h3 {
+ margin-top: 25px;
+ margin-bottom: 12px;
+}
+
+.docs-content h3:first-child {
+ margin-top: 0;
+}
+
+.docs-content p {
+ color: #666;
+ margin-bottom: 10px;
+}
+
+.docs-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+}
+
+.docs-table th, .docs-table td {
+ padding: 10px 12px;
+ text-align: left;
+ border: 1px solid #e8e8e8;
+}
+
+.docs-table th {
+ background: #fafafa;
+ font-weight: 500;
+}
+
+.about-info {
+ color: #666;
+ line-height: 1.8;
+}
+
+.modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0,0,0,0.5);
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background: #fff;
+ border-radius: 12px;
+ padding: 25px;
+ width: 450px;
+ max-width: 90%;
+ max-height: 85vh;
+ overflow-y: auto;
+ position: relative;
+}
+
+.modal-content h2 {
+ margin-bottom: 20px;
+ font-size: 18px;
+}
+
+.close {
+ position: absolute;
+ right: 20px;
+ top: 20px;
+ font-size: 24px;
+ cursor: pointer;
+ color: #999;
+}
+
+.close:hover {
+ color: #333;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 15px 0 10px;
+ padding: 8px 12px;
+ background: #f5f5f5;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ color: var(--text-color);
+}
+
+.section-header:hover {
+ background: #e8e8e8;
+}
+
+.section-arrow {
+ font-size: 12px;
+ color: #999;
+}
+
+.section-content {
+ padding-left: 5px;
+}
+
+.field-desc {
+ color: var(--text-secondary);
+ font-size: 12px;
+}
+
+.form-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 20px;
+}
+
+.toast {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ padding: 12px 25px;
+ border-radius: 8px;
+ font-size: 14px;
+ opacity: 0;
+ transform: translateY(20px);
+ transition: all 0.3s;
+ z-index: 2000;
+}
+
+.toast.show {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.toast.success {
+ background: #e6f7e6;
+ color: #52c41a;
+}
+
+.toast.error {
+ background: #fff1f0;
+ color: #f5222d;
+}
+
+.status-online { color: #52c41a; }
+.status-inactive { color: #999; }
+.status-error { color: #f5222d; }
+.status-wechat-online { color: #52c41a; }
+.status-wechat-offline { color: #f5222d; }
+.status-wechat-unknown { color: #999; }
+
+small {
+ font-size: 12px;
+ color: #999;
+ display: block;
+ margin-top: 4px;
+}
+
+.empty {
+ text-align: center;
+ color: #999;
+ padding: 40px;
+}
+
+@media (max-width: 1024px) {
+ .dashboard-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ width: 60px;
+ }
+
+ .sidebar-header h1, .sidebar-header span, .nav-item span {
+ display: none;
+ }
+
+ .nav-item {
+ justify-content: center;
+ padding: 15px;
+ }
+
+ .nav-icon {
+ margin-right: 0;
+ }
+
+ .main-content {
+ margin-left: 60px;
+ max-width: calc(100% - 60px);
+ }
+
+ .config-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.log-section {
+ max-height: calc(100vh - 200px);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.log-container {
+ flex: 1;
+ overflow-y: auto;
+ font-family: 'Monaco', 'Menlo', monospace;
+ font-size: 12px;
+ line-height: 1.6;
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 15px;
+ border-radius: 8px;
+ min-height: 400px;
+ max-height: calc(100vh - 250px);
+}
+
+.log-entry {
+ padding: 4px 0;
+ border-bottom: 1px solid #333;
+ display: flex;
+ gap: 10px;
+ align-items: flex-start;
+}
+
+.log-entry:last-child {
+ border-bottom: none;
+}
+
+.log-time {
+ color: #888;
+ white-space: nowrap;
+}
+
+.log-level {
+ font-weight: 600;
+ min-width: 60px;
+}
+
+.log-source {
+ color: #9cdcfe;
+ min-width: 80px;
+}
+
+.log-message {
+ flex: 1;
+ word-break: break-all;
+}
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..70393a6
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ WXAuto Center - 中控系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/js/api.js b/static/js/api.js
new file mode 100644
index 0000000..4837081
--- /dev/null
+++ b/static/js/api.js
@@ -0,0 +1,155 @@
+const API_KEY = 'your-external-api-key';
+const API_BASE = '/api/v1';
+const REQUEST_TIMEOUT = 30000;
+
+let pendingRequests = 0;
+let loadingToast = null;
+let loadingTimer = null;
+
+function showNonBlockingLoading(message = '加载中...') {
+ pendingRequests++;
+ updateLoadingUI(message);
+
+ if (loadingTimer) clearTimeout(loadingTimer);
+ loadingTimer = setTimeout(() => {
+ console.warn('Some requests may be stuck, force clearing');
+ }, 60000);
+}
+
+function hideNonBlockingLoading() {
+ pendingRequests = Math.max(0, pendingRequests - 1);
+ updateLoadingUI(pendingRequests > 0 ? '处理中...' : null);
+}
+
+function updateLoadingUI(message) {
+ if (!message) {
+ if (loadingToast) {
+ loadingToast.remove();
+ loadingToast = null;
+ }
+ return;
+ }
+
+ if (!loadingToast) {
+ loadingToast = document.createElement('div');
+ loadingToast.id = 'loading-toast';
+ loadingToast.style.cssText = `
+ position: fixed;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(0,0,0,0.8);
+ color: white;
+ padding: 12px 24px;
+ border-radius: 8px;
+ font-size: 14px;
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ `;
+ document.body.appendChild(loadingToast);
+ }
+
+ const spinner = '';
+ const style = '';
+ if (!document.getElementById('loading-spinner-style')) {
+ document.head.insertAdjacentHTML('beforeend', style);
+ }
+ loadingToast.innerHTML = spinner + `${message}(${pendingRequests})`;
+}
+
+async function api(endpoint, options = {}) {
+ const method = options.method || 'GET';
+ const isMutation = ['POST', 'PUT', 'DELETE'].includes(method);
+ const startMsg = `请求${isMutation ? '修改' : '获取'}中...`;
+
+ showNonBlockingLoading(startMsg);
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => {
+ controller.abort();
+ showToast('请求超时,请稍后重试', 'error');
+ }, REQUEST_TIMEOUT);
+
+ try {
+ const headers = {
+ 'X-API-Key': API_KEY,
+ 'Content-Type': 'application/json',
+ ...options.headers
+ };
+
+ const response = await fetch(API_BASE + endpoint, {
+ ...options,
+ headers,
+ signal: controller.signal
+ });
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ const error = `HTTP ${response.status}: ${response.statusText}`;
+ if (isMutation) {
+ showToast('操作失败: ' + error, 'error');
+ }
+ return { success: false, error };
+ }
+
+ const data = await response.json();
+
+ if (isMutation && data.success === false && data.error) {
+ showToast(data.error, 'error');
+ }
+
+ return data;
+
+ } catch (err) {
+ clearTimeout(timeoutId);
+ if (err.name === 'AbortError') {
+ return { success: false, error: '请求超时' };
+ }
+ const errorMsg = err.message || '网络错误';
+ if (isMutation) {
+ showToast(errorMsg, 'error');
+ }
+ return { success: false, error: errorMsg };
+
+ } finally {
+ hideNonBlockingLoading();
+ }
+}
+
+const apiClient = {
+ get: (endpoint) => api(endpoint),
+ post: (endpoint, data) => api(endpoint, { method: 'POST', body: JSON.stringify(data) }),
+ put: (endpoint, data) => api(endpoint, { method: 'PUT', body: JSON.stringify(data) }),
+ delete: (endpoint) => api(endpoint, { method: 'DELETE' }),
+
+ nodes: {
+ list: () => api('/nodes/'),
+ get: (name) => api(`/nodes/${name}`),
+ create: (data) => api('/nodes/', { method: 'POST', body: JSON.stringify(data) }),
+ update: (name, data) => api(`/nodes/${name}`, { method: 'PUT', body: JSON.stringify(data) }),
+ delete: (name) => api(`/nodes/${name}`, { method: 'DELETE' }),
+ status: (name) => api(`/nodes/${name}/status`)
+ },
+
+ messages: {
+ send: (data) => api('/messages/send', { method: 'POST', body: JSON.stringify(data) }),
+ broadcast: (data) => api('/messages/broadcast', { method: 'POST', body: JSON.stringify(data) })
+ },
+
+ config: {
+ getMonitor: () => api('/config/monitor'),
+ updateMonitor: (data) => api('/config/monitor', { method: 'POST', body: JSON.stringify(data) }),
+ getWebhook: () => api('/config/webhook'),
+ updateWebhook: (data) => api('/config/webhook', { method: 'POST', body: JSON.stringify(data) })
+ },
+
+ logs: {
+ recent: (count = 50) => api(`/logs/recent?count=${count}`)
+ }
+};
+
+window.api = api;
+window.apiClient = apiClient;
diff --git a/static/js/app.js b/static/js/app.js
new file mode 100644
index 0000000..df35715
--- /dev/null
+++ b/static/js/app.js
@@ -0,0 +1,759 @@
+// ///
+// app.js
+// 描述:中控系统前端主逻辑
+// 作者:AI Generated
+// 创建日期:2026-04-05
+// ///
+
+let currentPage = 'dashboard';
+let logPollInterval = null;
+let lastLogTime = null;
+
+document.addEventListener('DOMContentLoaded', () => {
+ renderApp();
+});
+
+function renderApp() {
+ document.getElementById('app').innerHTML = APP_TEMPLATE;
+ initNavEvents();
+ showPage('dashboard');
+}
+
+function initNavEvents() {
+ document.querySelectorAll('.nav-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault();
+ showPage(item.dataset.page);
+ });
+ });
+}
+
+function showPage(pageName) {
+ currentPage = pageName;
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
+ document.querySelector(`[data-page="${pageName}"]`).classList.add('active');
+
+ const content = document.getElementById('page-content');
+ content.innerHTML = `${PAGES[pageName] || '
页面不存在
'}
`;
+
+ if (pageName === 'dashboard') loadDashboard();
+ if (pageName === 'nodes') loadNodesForTable();
+ if (pageName === 'monitor') { loadMonitorConfig(); loadWebhookTable(); }
+ if (pageName === 'send') loadNodesForSend();
+ if (pageName === 'logs') startLogStream();
+ if (pageName === 'external') initExternalPage();
+ if (pageName === 'plugins') setTimeout(loadPluginTable, 50);
+}
+
+async function loadDashboard() {
+ const result = await api('/nodes/');
+ if (result.success) renderDashboard(result.data);
+}
+
+async function loadNodesForTable() {
+ const result = await api('/nodes/');
+ if (result.success) renderNodesTable(result.data);
+}
+
+function renderDashboard(nodes) {
+ const total = nodes.length;
+ const online = nodes.filter(n => n.status === 'active' && n.wechat_status === 'online').length;
+ const offline = nodes.filter(n => n.status !== 'active' || n.wechat_status !== 'online').length;
+
+ const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
+ setText('onlineCount', online);
+ setText('offlineCount', offline);
+ setText('totalMessages', total);
+ setText('todayMessages', '-');
+}
+
+function renderNodesTable(nodes) {
+ const container = document.getElementById('nodesTable');
+ if (!container) return;
+ if (!nodes || !nodes.length) {
+ container.innerHTML = '暂无节点
';
+ return;
+ }
+ container.innerHTML = `
+
+
+ | ID | 名称 | 分组 | API状态 | 微信状态 | 操作 |
+
+
+ ${nodes.map(n => {
+ const statusClass = n.status !== 'active' ? 'offline' : (n.wechat_status !== 'online' ? 'warning' : 'online');
+ return `
+ | ${n.node_id} |
+ ${n.name} |
+ ${n.group} |
+ ${n.status} |
+ ${n.wechat_status} |
+
+
+
+
+ |
+
`;
+ }).join('')}
+
+
+ `;
+}
+
+async function loadNodesForSend() {
+ const result = await api('/nodes/');
+ if (result.success) {
+ const options = '' +
+ result.data.map(n => ``).join('');
+ document.getElementById('sendNode').innerHTML = options;
+ }
+}
+
+async function refreshAll() {
+ await loadDashboard();
+ showToast('刷新完成');
+}
+
+async function checkNodeStatus(nodeId) {
+ const result = await api(`/nodes/${nodeId}/status`);
+ if (result.success) {
+ showToast(`检测完成: API=${result.data.api_status}, 微信=${result.data.wechat_status || 'N/A'}`);
+ }
+ await loadNodesForTable();
+}
+
+async function sendQuickMessage() {
+ const nodeEl = document.getElementById('sendNode');
+ const whoEl = document.getElementById('sendWho');
+ const msgEl = document.getElementById('quickMsg');
+ if (!nodeEl || !whoEl || !msgEl) {
+ showToast('页面未正确加载', 'error');
+ return;
+ }
+ const nodeId = nodeEl.value;
+ const who = whoEl.value;
+ const msg = msgEl.value;
+ if (!nodeId || !who || !msg) {
+ showToast('请填写完整信息', 'error');
+ return;
+ }
+ const result = await api('/messages/send', {
+ method: 'POST',
+ body: JSON.stringify({ node_id: nodeId, who, msg })
+ });
+ if (result.success) {
+ showToast('发送成功');
+ msgEl.value = '';
+ } else {
+ showToast('发送失败: ' + (result.error || result.detail), 'error');
+ }
+}
+
+async function loadMonitorConfig() {
+ const result = await api('/config/monitor');
+ if (result.success) {
+ const enabledEl = document.getElementById('monitorEnabled');
+ const intervalEl = document.getElementById('checkInterval');
+ if (enabledEl) enabledEl.checked = result.data.enabled;
+ if (intervalEl) intervalEl.value = result.data.check_interval;
+ }
+}
+
+async function updateMonitorConfig() {
+ const config = {
+ enabled: document.getElementById('monitorEnabled').checked,
+ check_interval: parseInt(document.getElementById('checkInterval').value) || 60
+ };
+ const result = await api('/config/monitor', { method: 'POST', body: JSON.stringify(config) });
+ if (result.success) showToast('监控配置已更新');
+}
+
+async function loadWebhookTable() {
+ const result = await api('/webhook/');
+ if (result.success) renderWebhookTable(result.data || []);
+}
+
+function renderWebhookTable(webhooks) {
+ const container = document.getElementById('webhookTable');
+ if (!container) return;
+ if (!webhooks.length) {
+ container.innerHTML = '暂无 Webhook,请点击添加
';
+ return;
+ }
+ container.innerHTML = `
+
+
+ | 名称 | URL | 格式 | 事件 | 状态 | 操作 |
+
+
+ ${webhooks.map(w => {
+ const eventLabels = {
+ 'api_error': 'API错误',
+ 'wechat_offline': '微信离线',
+ 'wechat_online': '微信上线',
+ 'node_offline': '节点离线',
+ 'node_online': '节点上线'
+ };
+ const events = Array.isArray(w.event_types) ? w.event_types.map(e => eventLabels[e] || e).join(', ') : '-';
+ return `
+ | ${w.name || '-'} |
+ ${w.url || '-'} |
+ ${w.format === 'bark' ? 'Bark' : '企业微信'} |
+ ${events} |
+ ${w.enabled ? '启用' : '禁用'} |
+
+
+
+ |
+
`;
+ }).join('')}
+
+
+ `;
+}
+
+function showAddWebhookModal(webhook = null) {
+ const modal = document.getElementById('modal');
+ const title = document.getElementById('modalTitle');
+ const form = document.getElementById('nodeForm');
+
+ title.textContent = webhook ? '编辑 Webhook' : '添加 Webhook';
+ form.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ modal.style.display = 'flex';
+}
+
+async function editWebhook(id) {
+ const result = await api(`/webhook/${id}`);
+ if (result.success && result.data) {
+ showAddWebhookModal(result.data);
+ }
+}
+
+async function deleteWebhook(id) {
+ if (!confirm('确定删除此 Webhook?')) return;
+ const result = await api(`/webhook/${id}`, { method: 'DELETE' });
+ if (result.success) {
+ showToast('Webhook 已删除');
+ loadWebhookTable();
+ } else {
+ showToast('删除失败', 'error');
+ }
+}
+
+async function saveWebhook() {
+ const id = document.getElementById('webhookId').value;
+ const eventTypes = Array.from(document.querySelectorAll('.webhook-event:checked')).map(cb => cb.value);
+ const data = {
+ name: document.getElementById('webhookName').value,
+ url: document.getElementById('webhookUrl').value,
+ format: document.getElementById('webhookFormat').value,
+ enabled: document.getElementById('webhookEnabled').checked,
+ event_types: eventTypes
+ };
+
+ if (!data.url) {
+ showToast('请输入 URL', 'error');
+ return;
+ }
+
+ let result;
+ if (id) {
+ result = await api(`/webhook/${id}`, { method: 'PUT', body: JSON.stringify(data) });
+ } else {
+ result = await api('/webhook/', { method: 'POST', body: JSON.stringify(data) });
+ }
+
+ if (result.success) {
+ showToast(id ? 'Webhook 已更新' : 'Webhook 已添加');
+ closeModal();
+ loadWebhookTable();
+ } else {
+ showToast('保存失败: ' + (result.error || ''), 'error');
+ }
+}
+
+async function startLogStream() {
+ const result = await api('/logs/recent?count=200');
+ if (result.success && result.data) {
+ const container = document.getElementById('logContainer');
+ if (container) {
+ container.innerHTML = '';
+ if (result.data.length > 0) {
+ lastLogTime = result.data[0].timestamp;
+ result.data.forEach(entry => addLogEntry(entry));
+ }
+ }
+ }
+ if (logPollInterval) clearInterval(logPollInterval);
+ logPollInterval = setInterval(async () => {
+ const res = await api('/logs/recent?count=20');
+ if (res.success && res.data && res.data.length > 0) {
+ const newLogs = res.data.filter(log => {
+ if (!lastLogTime) return true;
+ return new Date(log.timestamp) > new Date(lastLogTime);
+ });
+ if (newLogs.length > 0) {
+ lastLogTime = newLogs[0].timestamp;
+ const container = document.getElementById('logContainer');
+ if (container) newLogs.forEach(entry => addLogEntry(entry));
+ }
+ }
+ }, 3000);
+}
+
+function addLogEntry(entry) {
+ const container = document.getElementById('logContainer');
+ if (!container) return;
+ const colors = { 'INFO': '#4a9eff', 'WARNING': '#fa8c16', 'ERROR': '#f5222d', 'SUCCESS': '#52c41a' };
+ const color = colors[entry.level] || '#666';
+ const html = `
+ ${entry.timestamp}
+ ${entry.level}
+ [${entry.source}]
+ ${entry.message}
+
`;
+ container.insertAdjacentHTML('afterbegin', html);
+ if (container.children.length > 500) container.lastChild.remove();
+}
+
+function clearLogs() {
+ const container = document.getElementById('logContainer');
+ if (container) container.innerHTML = '';
+}
+
+function initExternalPage() {
+ document.getElementById('externalApiUrl').textContent = `${window.location.origin}/api/v1/external/send`;
+ document.getElementById('webhookReceiveUrl').textContent = `${window.location.origin}/api/v1/external/webhook/{event}`;
+}
+
+async function loadPluginTable() {
+ const result = await api('/plugins/');
+ if (result.success) renderPluginTable(result.plugins || []);
+}
+
+function renderPluginTable(plugins) {
+ const container = document.getElementById('pluginTable');
+ if (!container) return;
+ if (!plugins.length) {
+ container.innerHTML = '暂无插件
';
+ return;
+ }
+ const typeLabels = {
+ 'message_handler': '消息处理',
+ 'data_source': '数据源',
+ 'action_trigger': '动作触发',
+ 'ai_agent': 'AI智能体',
+ 'custom': '自定义'
+ };
+ container.innerHTML = `
+
+
+ | 名称 | 版本 | 类型 | 描述 | 状态 | 操作 |
+
+
+ ${plugins.map(p => `
+
+ | ${p.name || '-'} |
+ ${p.version || '-'} |
+ ${typeLabels[p.type] || p.type || '-'} |
+ ${p.description || '-'} |
+ ${p.enabled ? '启用' : '禁用'} |
+
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+}
+
+async function togglePlugin(name) {
+ const result = await api(`/plugins/${name}`);
+ if (result.success && result.plugin) {
+ const action = result.plugin.enabled ? 'disable' : 'enable';
+ const res = await api(`/plugins/${name}/${action}`, { method: 'POST' });
+ if (res.success) {
+ showToast(res.message);
+ loadPluginTable();
+ }
+ }
+}
+
+function configPlugin(name) {
+ Promise.all([
+ api(`/plugins/${name}`),
+ api(`/plugins/${name}/schema`)
+ ]).then(([pluginResult, schemaResult]) => {
+ if (pluginResult.success) {
+ if (schemaResult.success && schemaResult.has_form && schemaResult.schema) {
+ showPluginConfigForm(pluginResult.plugin, schemaResult.schema);
+ } else {
+ showPluginConfigModal(pluginResult.plugin);
+ }
+ }
+ });
+}
+
+function showPluginConfigForm(plugin, schema) {
+ const modal = document.getElementById('modal');
+ const title = document.getElementById('modalTitle');
+ const form = document.getElementById('nodeForm');
+
+ title.textContent = `配置插件: ${plugin.name}`;
+ let formHtml = '';
+
+ const currentConfig = plugin.config || {};
+
+ if (schema.sections) {
+ schema.sections.forEach((section, sIdx) => {
+ const sectionId = `plugin-section-${sIdx}`;
+ formHtml += ``;
+ });
+ }
+
+ formHtml += ``;
+ formHtml += ``;
+
+ form.innerHTML = formHtml + `
+
+
+
+
+ `;
+ modal.style.display = 'flex';
+}
+
+function togglePluginSection(sectionId) {
+ const content = document.getElementById(sectionId);
+ const arrow = document.getElementById(sectionId + '-arrow');
+ if (content.style.display === 'none') {
+ content.style.display = 'block';
+ arrow.textContent = '▼';
+ } else {
+ content.style.display = 'none';
+ arrow.textContent = '▶';
+ }
+}
+
+function escapeHtml(text) {
+ if (typeof text !== 'string') text = String(text);
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+async function savePluginConfigFromForm() {
+ const name = document.getElementById('pluginName').value;
+ const schema = JSON.parse(document.getElementById('pluginSchema').value);
+
+ const config = {};
+
+ if (schema.sections) {
+ schema.sections.forEach(section => {
+ section.fields.forEach(field => {
+ const input = document.getElementById(`plugin_field_${field.name}`);
+ if (!input) return;
+
+ if (field.type === 'boolean') {
+ config[field.name] = input.checked;
+ } else if (field.type === 'number') {
+ config[field.name] = parseFloat(input.value) || 0;
+ } else {
+ config[field.name] = input.value;
+ }
+ });
+ });
+ }
+
+ const result = await api(`/plugins/${name}/config`, {
+ method: 'PUT',
+ body: JSON.stringify({ name, config })
+ });
+ if (result.success) {
+ showToast('配置已保存');
+ closeModal();
+ loadPluginTable();
+ } else {
+ showToast('保存失败: ' + (result.error || ''), 'error');
+ }
+}
+
+function showPluginConfigModal(plugin) {
+ const modal = document.getElementById('modal');
+ const title = document.getElementById('modalTitle');
+ const form = document.getElementById('nodeForm');
+
+ title.textContent = `配置插件: ${plugin.name} (JSON)`;
+ const configJson = JSON.stringify(plugin.config || {}, null, 2);
+ form.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+ modal.style.display = 'flex';
+}
+
+async function savePluginConfig() {
+ const name = document.getElementById('pluginName').value;
+ let config;
+ try {
+ config = JSON.parse(document.getElementById('pluginConfigJson').value);
+ } catch (e) {
+ showToast('JSON格式错误', 'error');
+ return;
+ }
+ const result = await api(`/plugins/${name}/config`, {
+ method: 'PUT',
+ body: JSON.stringify({ name, config })
+ });
+ if (result.success) {
+ showToast('配置已保存');
+ closeModal();
+ loadPluginTable();
+ } else {
+ showToast('保存失败: ' + (result.error || ''), 'error');
+ }
+}
+
+async function showPluginLogs(pluginName) {
+ const modal = document.getElementById('modal');
+ const title = document.getElementById('modalTitle');
+ const form = document.getElementById('nodeForm');
+
+ title.textContent = `插件日志: ${pluginName}`;
+ form.innerHTML = `
+
+
+
+
+
+ `;
+ modal.style.display = 'flex';
+
+ await refreshPluginLogs(pluginName);
+}
+
+async function refreshPluginLogs(pluginName) {
+ const logsContent = document.getElementById('pluginLogsContent');
+ if (!logsContent) return;
+
+ logsContent.textContent = '加载中...';
+
+ const result = await api(`/plugins/${pluginName}/logs?lines=100`);
+
+ if (result.success) {
+ if (result.logs && result.logs.length > 0) {
+ logsContent.textContent = result.logs.join('\n');
+ logsContent.scrollTop = logsContent.scrollHeight;
+ } else {
+ logsContent.textContent = '暂无日志';
+ }
+ } else {
+ logsContent.textContent = '加载失败: ' + (result.error || '未知错误');
+ }
+}
+
+function showAddNodeModal(node = null) {
+ document.getElementById('modalTitle').textContent = '添加节点';
+ document.getElementById('nodeForm').reset();
+ document.getElementById('nodeIdOld').value = '';
+ document.getElementById('nodeId').disabled = false;
+ document.getElementById('modal').style.display = 'flex';
+}
+
+async function editNode(nodeId) {
+ const result = await api(`/nodes/${nodeId}`);
+ if (result.success) {
+ const node = result.data;
+ document.getElementById('modalTitle').textContent = '编辑节点';
+ document.getElementById('nodeIdOld').value = node.node_id;
+ document.getElementById('nodeId').value = node.node_id;
+ document.getElementById('nodeId').disabled = true;
+ document.getElementById('nodeName').value = node.name;
+ document.getElementById('nodeApiUrl').value = node.api_url;
+ document.getElementById('nodeApiKey').value = node.api_key;
+ document.getElementById('nodeGroup').value = node.group;
+ document.getElementById('nodeDesc').value = node.description || '';
+ document.getElementById('modal').style.display = 'flex';
+ }
+}
+
+async function deleteNode(nodeId) {
+ if (!confirm(`确定删除节点 ${nodeId}?`)) return;
+ const result = await api(`/nodes/${nodeId}`, { method: 'DELETE' });
+ if (result.success) {
+ showToast('节点已删除');
+ await loadNodesForTable();
+ }
+}
+
+document.getElementById('nodeForm').onsubmit = async (e) => {
+ e.preventDefault();
+ const oldId = document.getElementById('nodeIdOld').value;
+ const nodeData = {
+ node_id: document.getElementById('nodeId').value,
+ name: document.getElementById('nodeName').value,
+ api_url: document.getElementById('nodeApiUrl').value,
+ api_key: document.getElementById('nodeApiKey').value,
+ group: document.getElementById('nodeGroup').value,
+ description: document.getElementById('nodeDesc').value
+ };
+ let result;
+ if (oldId) {
+ result = await api(`/nodes/${oldId}`, { method: 'PUT', body: JSON.stringify(nodeData) });
+ } else {
+ result = await api('/nodes/', { method: 'POST', body: JSON.stringify(nodeData) });
+ }
+ if (result.success) {
+ showToast(oldId ? '节点已更新' : '节点已添加');
+ closeModal();
+ await loadNodesForTable();
+ } else {
+ showToast('失败: ' + (result.error || result.detail), 'error');
+ }
+};
+
+function closeModal() {
+ document.getElementById('modal').style.display = 'none';
+}
+
+function showToast(message, type = 'success') {
+ const toast = document.getElementById('toast');
+ toast.textContent = message;
+ toast.className = 'toast show ' + type;
+ setTimeout(() => toast.classList.remove('show'), 3000);
+}
+
+document.addEventListener('DOMContentLoaded', renderApp);
diff --git a/static/js/pages/templates.js b/static/js/pages/templates.js
new file mode 100644
index 0000000..7017595
--- /dev/null
+++ b/static/js/pages/templates.js
@@ -0,0 +1,190 @@
+const APP_TEMPLATE = `
+
+`;
+
+const PAGES = {
+ dashboard: `
+
+
+ `,
+ nodes: `
+
+
+ `,
+ monitor: `
+
+
+
+
🔔 监控设置
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ send: `
+
+
+ `,
+ logs: `
+
+
+ `,
+ external: `
+
+
+ `,
+ plugins: `
+
+
+ `
+};
+
+window.PAGES = PAGES;
+window.APP_TEMPLATE = APP_TEMPLATE;
+
+function getPage(name) {
+ return PAGES[name] || '页面不存在
';
+}
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..d7f1a9f
--- /dev/null
+++ b/utils/__init__.py
@@ -0,0 +1,17 @@
+# ///
+# __init__.py
+# 描述:工具模块
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from utils.common import require_api_key, require_external_key
+from utils.helpers import check_node_health, build_success_response, build_error_response
+
+__all__ = [
+ "require_api_key",
+ "require_external_key",
+ "check_node_health",
+ "build_success_response",
+ "build_error_response",
+]
\ No newline at end of file
diff --git a/utils/common.py b/utils/common.py
new file mode 100644
index 0000000..a59c79a
--- /dev/null
+++ b/utils/common.py
@@ -0,0 +1,50 @@
+# ///
+# common.py
+# 描述:公共工具函数和依赖注入
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from typing import Optional
+from fastapi import Header, HTTPException, Depends
+from config import get_settings
+
+
+def require_api_key(x_api_key: Optional[str] = Header(None, alias="X-API-Key")):
+ """
+ API Key 验证依赖
+
+ 用于验证 X-API-Key Header 是否有效
+
+ Raises:
+ HTTPException: 401 - API Key 无效或缺失
+
+ Returns:
+ str: 有效的 API Key
+ """
+ if not x_api_key:
+ raise HTTPException(status_code=401, detail="API Key required")
+ settings = get_settings()
+ if x_api_key != settings.external_api_key:
+ raise HTTPException(status_code=401, detail="Invalid API Key")
+ return x_api_key
+
+
+def require_external_key(x_externalkey: Optional[str] = Header(None, alias="X-ExternalKey")):
+ """
+ 外部 API Key 验证依赖
+
+ 用于验证 X-ExternalKey Header 是否有效
+
+ Raises:
+ HTTPException: 401 - External API Key 无效或缺失
+
+ Returns:
+ str: 有效的 External API Key
+ """
+ if not x_externalkey:
+ raise HTTPException(status_code=401, detail="External API Key required")
+ settings = get_settings()
+ if x_externalkey != settings.external_api_key:
+ raise HTTPException(status_code=401, detail="Invalid External API Key")
+ return x_externalkey
\ No newline at end of file
diff --git a/utils/helpers.py b/utils/helpers.py
new file mode 100644
index 0000000..f81aae3
--- /dev/null
+++ b/utils/helpers.py
@@ -0,0 +1,77 @@
+# ///
+# helpers.py
+# 描述:业务辅助函数
+# 作者:AI Generated
+# 创建日期:2026-04-05
+# ///
+
+from typing import Dict, Any, Optional
+from fastapi import HTTPException
+
+from services.node_manager import node_manager
+
+
+async def check_node_health(node_id: str) -> None:
+ """
+ 检查节点健康状态
+
+ 如果节点不健康,抛出 503 异常
+
+ Args:
+ node_id: 节点ID
+
+ Raises:
+ HTTPException: 503 - 节点不健康
+ """
+ is_healthy, reason = await node_manager.is_node_healthy(node_id)
+ if not is_healthy:
+ raise HTTPException(
+ status_code=503,
+ detail=f"Node '{node_id}' is not healthy: {reason}. Message sending failed."
+ )
+
+
+def build_success_response(data: Any = None, message: str = "操作成功") -> Dict[str, Any]:
+ """
+ 构建成功响应
+
+ Args:
+ data: 响应数据
+ message: 响应消息
+
+ Returns:
+ Dict[str, Any]: 成功响应格式
+ """
+ response = {"success": True, "message": message}
+ if data is not None:
+ response["data"] = data
+ return response
+
+
+def build_error_response(error: str, code: int = 400) -> Dict[str, Any]:
+ """
+ 构建错误响应
+
+ Args:
+ error: 错误信息
+ code: HTTP 状态码
+
+ Returns:
+ Dict[str, Any]: 错误响应格式
+ """
+ return {"success": False, "error": error}
+
+
+def parse_node_result(result: Dict[str, Any]) -> tuple[bool, Optional[str]]:
+ """
+ 解析节点 API 调用结果
+
+ Args:
+ result: 节点返回结果
+
+ Returns:
+ tuple[bool, Optional[str]]: (是否成功, 错误信息)
+ """
+ if result.get("success"):
+ return True, None
+ return False, result.get("error", "Unknown error")
\ No newline at end of file