Merge branch 'Mai-with-u:dev' into dev

pull/1374/head
2829798842 2025-11-21 08:36:24 +08:00 committed by GitHub
commit 6d97f1831f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 292 additions and 564 deletions

2
.gitignore vendored
View File

@ -11,7 +11,6 @@ run_maibot_core.bat
run_voice.bat run_voice.bat
run_napcat_adapter.bat run_napcat_adapter.bat
run_ad.bat run_ad.bat
s4u.s4u
llm_tool_benchmark_results.json llm_tool_benchmark_results.json
MaiBot-Napcat-Adapter-main MaiBot-Napcat-Adapter-main
MaiBot-Napcat-Adapter MaiBot-Napcat-Adapter
@ -27,6 +26,7 @@ run.bat
log_debug/ log_debug/
run_amds.bat run_amds.bat
run_none.bat run_none.bat
docs-mai/
run.py run.py
message_queue_content.txt message_queue_content.txt
message_queue_content.bat message_queue_content.bat

View File

@ -45,7 +45,9 @@
## 🔥 更新和安装 ## 🔥 更新和安装
**最新版本: v0.11.6** ([更新日志](changelogs/changelog.md))
**最新版本: v0.11.5** ([更新日志](changelogs/changelog.md))
可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本 可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器 可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器
@ -112,7 +114,7 @@
## 🙋 贡献和致谢 ## 🙋 贡献和致谢
你可以阅读[开发文档](https://docs.mai-mai.org/develop/)来更好的了解麦麦! 你可以阅读[开发文档](https://docs.mai-mai.org/develop/)来更好的了解麦麦!
MaiCore 是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交 bug 报告、功能需求还是代码 pr都对项目非常宝贵。我们非常感谢你的支持🎉 MaiCore 是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交 bug 报告、功能需求还是代码 pr都对项目非常宝贵。我们非常感谢你的支持🎉
但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](docs/CONTRIBUTE.md)。(待补完) 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](docs-src/CONTRIBUTE.md)。(待补完)
### 贡献者 ### 贡献者

9
bot.py
View File

@ -75,6 +75,15 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
try: try:
logger.info("正在优雅关闭麦麦...") logger.info("正在优雅关闭麦麦...")
# 关闭 WebUI 服务器
try:
from src.webui.webui_server import get_webui_server
webui_server = get_webui_server()
if webui_server and webui_server._server:
await webui_server.shutdown()
except Exception as e:
logger.warning(f"关闭 WebUI 服务器时出错: {e}")
from src.plugin_system.core.events_manager import events_manager from src.plugin_system.core.events_manager import events_manager
from src.plugin_system.base.component_types import EventType from src.plugin_system.base.component_types import EventType

View File

@ -1,8 +1,46 @@
# Changelog # Changelog
## [0.11.5] - 2025-11-21 ## [0.11.5] - 2025-11-21
### 功能更改和修复 ### 🌟 重大更新
- 优化planner和replyer的协同 - WebUI 现支持手动重启麦麦,曲线救国版“热重载”
- 细化debug的log - 新增麦麦 QQ 适配器可视化编辑 UI独立进程需手动上传/下载并覆盖适配器文件)
- 麦麦主程序配置支持可视化模式与源代码模式双模式编辑,后端执行 TOML 校验
- 优化 planner 与 replyer 协同机制,调试日志更细
### 新增
- 表情包管理、人物信息管理、表达方式管理界面手机端适配
- 配置页“重启麦麦”提示
- 详细的debug prompt显示配置
- 麦麦界面操作主题色按钮
- 前端集成 CodeMirrorPython/JSON/TOML 语法高亮)并对 JSON 配置提供自动纠错提示
### 修复
- 表情包缩略图过小
- 添加模型后无法立即显示
- 插件市场分类错误
- 浅色模式下日志查看器背景异常
- 表情包详情窗口无法关闭
- 情绪标签无法正常读取issues/1373
- 模型任务配置界面模型温度不可更改issues/1369
- 模型任务配置界面部分输入框无法删除默认值
- 插件商店默认勾选“仅显示兼容插件”
- 插件商店标签页数量显示错误
- 日志查看器日志换行异常
- 主题色未正确应用
- 侧边栏滚动条问题
- 移除前端 TOML 解析库导致的兼容问题
### 优化
- 首页加载用户体验
- 首页加载速度9s → <1s
- 整个界面的初屏加载速度
### 更新
- 适配器配置支持上传模式与指定路径模式;指定路径模式可免去反复上传/下载配置文件
- 适配器配置界面标签页响应式优化,小屏仅显示简短标签
- 麦麦资源管理所有界面的批量删除能力
- 资源管理所有界面的分页、每页数量选择与页跳转
- 插件市场新增点赞、点踩、评分与下载量统计(基于 Cloudflare保证国内可访问
- 麦麦表情包查看界面的描述部分支持 Markdown 渲染
## [0.11.4] - 2025-11-19 ## [0.11.4] - 2025-11-19
### 🌟 主要更新内容 ### 🌟 主要更新内容

View File

@ -29,7 +29,8 @@ services:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
# - EULA_AGREE=99f08e0cab0190de853cb6af7d64d4de # 同意EULA # - EULA_AGREE=99f08e0cab0190de853cb6af7d64d4de # 同意EULA
# - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA # - PRIVACY_AGREE=9943b855e72199d0f5016ea39052f1b6 # 同意EULA
# ports: ports:
- "18001:8001" # webui端口
# - "8000:8000" # - "8000:8000"
volumes: volumes:
- ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件 - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,336 +0,0 @@
# 模型配置指南
本文档将指导您如何配置 `model_config.toml` 文件,该文件用于配置 MaiBot 的各种AI模型和API服务提供商。
## 配置文件结构
配置文件主要包含以下几个部分:
- 版本信息
- API服务提供商配置
- 模型配置
- 模型任务配置
## 1. 版本信息
```toml
[inner]
version = "1.1.1"
```
用于标识配置文件的版本,遵循语义化版本规则。
## 2. API服务提供商配置
### 2.1 基本配置
使用 `[[api_providers]]` 数组配置多个API服务提供商
```toml
[[api_providers]]
name = "DeepSeek" # 服务商名称(自定义)
base_url = "https://api.deepseek.com/v1" # API服务的基础URL
api_key = "your-api-key-here" # API密钥
client_type = "openai" # 客户端类型
max_retry = 2 # 最大重试次数
timeout = 30 # 超时时间(秒)
retry_interval = 10 # 重试间隔(秒)
```
### 2.2 配置参数说明
| 参数 | 必填 | 说明 | 默认值 |
|------|------|------|--------|
| `name` | ✅ | 服务商名称,需要在模型配置中引用 | - |
| `base_url` | ✅ | API服务的基础URL | - |
| `api_key` | ✅ | API密钥请替换为实际密钥 | - |
| `client_type` | ❌ | 客户端类型:`openai`OpenAI格式`gemini`Gemini格式 | `openai` |
| `max_retry` | ❌ | API调用失败时的最大重试次数 | 2 |
| `timeout` | ❌ | API请求超时时间 | 30 |
| `retry_interval` | ❌ | 重试间隔时间(秒) | 10 |
**请注意,对于`client_type`为`gemini`的模型,`retry`字段由`gemini`自己决定。**
### 2.3 支持的服务商示例
#### DeepSeek
```toml
[[api_providers]]
name = "DeepSeek"
base_url = "https://api.deepseek.com/v1"
api_key = "your-deepseek-api-key"
client_type = "openai"
```
#### SiliconFlow
```toml
[[api_providers]]
name = "SiliconFlow"
base_url = "https://api.siliconflow.cn/v1"
api_key = "your-siliconflow-api-key"
client_type = "openai"
```
#### Google Gemini
```toml
[[api_providers]]
name = "Google"
base_url = "https://generativelanguage.googleapis.com/v1beta"
api_key = "your-google-api-key"
client_type = "gemini" # 注意Gemini需要使用特殊客户端
```
## 3. 模型配置
### 3.1 基本模型配置
使用 `[[models]]` 数组配置多个模型:
```toml
[[models]]
model_identifier = "deepseek-chat" # 模型在API服务商中的标识符
name = "deepseek-v3" # 自定义模型名称
api_provider = "DeepSeek" # 引用的API服务商名称
price_in = 2.0 # 输入价格(元/M token
price_out = 8.0 # 输出价格(元/M token
```
### 3.2 高级模型配置
#### 强制流式输出
对于不支持非流式输出的模型:
```toml
[[models]]
model_identifier = "some-model"
name = "custom-name"
api_provider = "Provider"
force_stream_mode = true # 启用强制流式输出
```
#### 额外参数配置`extra_params`
```toml
[[models]]
model_identifier = "Qwen/Qwen3-8B"
name = "qwen3-8b"
api_provider = "SiliconFlow"
[models.extra_params]
enable_thinking = false # 禁用思考
```
这里的 `extra_params` 可以包含任何API服务商支持的额外参数配置**配置时应参考相应的API文档**。
比如上面就是参考SiliconFlow的文档配置配置的`Qwen3`禁用思考参数。
![SiliconFlow文档截图](image-1.png)
以豆包文档为另一个例子
![豆包文档截图](image.png)
得到豆包`"doubao-seed-1-6-250615"`的禁用思考配置方法为
```toml
[[models]]
# 你的模型
[models.extra_params]
thinking = {type = "disabled"} # 禁用思考
```
而对于`gemini`需要单独进行配置
```toml
[[models]]
model_identifier = "gemini-2.5-flash"
name = "gemini-2.5-flash"
api_provider = "Google"
[models.extra_params]
thinking_budget = 0 # 禁用思考
# thinking_budget = -1 由模型自己决定
```
请注意,`extra_params` 的配置应该构成一个合法的TOML字典结构具体内容取决于API服务商的要求。
### 3.3 配置参数说明
| 参数 | 必填 | 说明 |
|------|------|------|
| `model_identifier` | ✅ | API服务商提供的模型标识符 |
| `name` | ✅ | 自定义模型名称,用于在任务配置中引用 |
| `api_provider` | ✅ | 对应的API服务商名称 |
| `price_in` | ❌ | 输入价格(元/M token用于成本统计 |
| `price_out` | ❌ | 输出价格(元/M token用于成本统计 |
| `force_stream_mode` | ❌ | 是否强制使用流式输出 |
| `extra_params` | ❌ | 额外的模型参数配置 |
## 4. 模型任务配置
### utils - 工具模型
用于表情包模块、取名模块、关系模块等核心功能:
```toml
[model_task_config.utils]
model_list = ["siliconflow-deepseek-v3"]
temperature = 0.2
max_tokens = 800
```
### utils_small - 小型工具模型
用于高频率调用的场景,建议使用速度快的小模型:
```toml
[model_task_config.utils_small]
model_list = ["qwen3-8b"]
temperature = 0.7
max_tokens = 800
```
### replyer - 主要回复模型
首要回复模型,也用于表达器和表达方式学习:
```toml
[model_task_config.replyer]
model_list = ["siliconflow-deepseek-v3"]
temperature = 0.2
max_tokens = 800
```
### planner - 决策模型
负责决定MaiBot该做什么
```toml
[model_task_config.planner]
model_list = ["siliconflow-deepseek-v3"]
temperature = 0.3
max_tokens = 800
```
### emotion - 情绪模型
负责MaiBot的情绪变化
```toml
[model_task_config.emotion]
model_list = ["siliconflow-deepseek-v3"]
temperature = 0.3
max_tokens = 800
```
### memory - 记忆模型
```toml
[model_task_config.memory]
model_list = ["qwen3-30b"]
temperature = 0.7
max_tokens = 800
```
### vlm - 视觉语言模型
用于图像识别:
```toml
[model_task_config.vlm]
model_list = ["qwen2.5-vl-72b"]
max_tokens = 800
```
### voice - 语音识别模型
```toml
[model_task_config.voice]
model_list = ["sensevoice-small"]
```
### embedding - 嵌入模型
```toml
[model_task_config.embedding]
model_list = ["bge-m3"]
```
### tool_use - 工具调用模型
需要使用支持工具调用的模型:
```toml
[model_task_config.tool_use]
model_list = ["qwen3-14b"]
temperature = 0.7
max_tokens = 800
```
### lpmm_entity_extract - 实体提取模型
```toml
[model_task_config.lpmm_entity_extract]
model_list = ["siliconflow-deepseek-v3"]
temperature = 0.2
max_tokens = 800
```
### lpmm_rdf_build - RDF构建模型
```toml
[model_task_config.lpmm_rdf_build]
model_list = ["siliconflow-deepseek-v3"]
temperature = 0.2
max_tokens = 800
```
### lpmm_qa - 问答模型
```toml
[model_task_config.lpmm_qa]
model_list = ["deepseek-r1-distill-qwen-32b"]
temperature = 0.7
max_tokens = 800
```
## 5. 配置建议
### 5.1 Temperature 参数选择
| 任务类型 | 推荐温度 | 说明 |
|----------|----------|------|
| 精确任务(工具调用、实体提取) | 0.1-0.3 | 需要准确性和一致性 |
| 创意任务(对话、记忆) | 0.5-0.8 | 需要多样性和创造性 |
| 平衡任务(决策、情绪) | 0.3-0.5 | 平衡准确性和灵活性 |
### 5.2 模型选择建议
| 任务类型 | 推荐模型类型 | 示例 |
|----------|--------------|------|
| 高精度任务 | 大模型 | DeepSeek-V3, GPT-4 |
| 高频率任务 | 小模型 | Qwen3-8B |
| 多模态任务 | 专用模型 | Qwen2.5-VL, SenseVoice |
| 工具调用 | 支持Function Call的模型 | Qwen3-14B |
### 5.3 成本优化
1. **分层使用**:核心功能使用高质量模型,辅助功能使用经济模型
2. **合理配置max_tokens**:根据实际需求设置,避免浪费
3. **选择免费模型**对于测试环境优先使用price为0的模型
## 6. 配置验证
### 6.1 必要检查项
1. ✅ API密钥是否正确配置
2. ✅ 模型标识符是否与API服务商提供的一致
3. ✅ 任务配置中引用的模型名称是否在models中定义
4. ✅ 多模态任务是否配置了对应的专用模型
### 6.2 测试配置
建议在正式使用前:
1. 使用少量测试数据验证配置
2. 检查API调用是否正常
3. 确认成本统计功能正常工作
## 7. 故障排除
### 7.1 常见问题
**问题1**: API调用失败
- 检查API密钥是否正确
- 确认base_url是否可访问
- 检查模型标识符是否正确
**问题2**: 模型未找到
- 确认模型名称在任务配置和模型定义中一致
- 检查api_provider名称是否匹配
**问题3**: 响应异常
- 检查温度参数是否合理0-1之间
- 确认max_tokens设置是否合适
- 验证模型是否支持所需功能
### 7.2 日志查看
查看 `logs/` 目录下的日志文件,寻找相关错误信息。
## 8. 更新和维护
1. **定期更新**: 关注API服务商的模型更新及时调整配置
2. **性能监控**: 监控模型调用的成本和性能
3. **备份配置**: 在修改前备份当前配置文件

View File

@ -1,5 +1,4 @@
from fastapi import FastAPI, APIRouter from fastapi import FastAPI, APIRouter
from fastapi.middleware.cors import CORSMiddleware # 新增导入
from typing import Optional from typing import Optional
from uvicorn import Config, Server as UvicornServer from uvicorn import Config, Server as UvicornServer
import asyncio import asyncio
@ -17,21 +16,6 @@ class Server:
self._server: Optional[UvicornServer] = None self._server: Optional[UvicornServer] = None
self.set_address(host, port) self.set_address(host, port)
# 配置 CORS
origins = [
"http://localhost:7999", # 允许的前端源
"http://127.0.0.1:7999",
# 在生产环境中,您应该添加实际的前端域名
]
self.app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # 是否支持 cookie
allow_methods=["*"], # 允许所有 HTTP 方法
allow_headers=["*"], # 允许所有 HTTP 请求头
)
def register_router(self, router: APIRouter, prefix: str = ""): def register_router(self, router: APIRouter, prefix: str = ""):
"""注册路由 """注册路由

View File

@ -56,7 +56,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
# 对该字段的更新请严格参照语义化版本规范https://semver.org/lang/zh-CN/ # 对该字段的更新请严格参照语义化版本规范https://semver.org/lang/zh-CN/
MMC_VERSION = "0.11.4" MMC_VERSION = "0.11.5"
def get_key_comment(toml_table, key): def get_key_comment(toml_table, key):

View File

@ -36,25 +36,13 @@ class MainSystem:
# 使用消息API替代直接的FastAPI实例 # 使用消息API替代直接的FastAPI实例
self.app: MessageServer = get_global_api() self.app: MessageServer = get_global_api()
self.server: Server = get_global_server() self.server: Server = get_global_server()
self.webui_server = None # 独立的 WebUI 服务器
# 注册 WebUI API 路由 # 设置独立的 WebUI 服务器
self._register_webui_routes() self._setup_webui_server()
# 设置 WebUI开发/生产模式) def _setup_webui_server(self):
self._setup_webui() """设置独立的 WebUI 服务器"""
def _register_webui_routes(self):
"""注册 WebUI API 路由"""
try:
from src.webui.routes import router as webui_router
self.server.register_router(webui_router)
logger.info("WebUI API 路由已注册")
except Exception as e:
logger.warning(f"注册 WebUI API 路由失败: {e}")
def _setup_webui(self):
"""设置 WebUI根据环境变量决定模式"""
import os import os
webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true" webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true"
@ -65,11 +53,22 @@ class MainSystem:
webui_mode = os.getenv("WEBUI_MODE", "production").lower() webui_mode = os.getenv("WEBUI_MODE", "production").lower()
try: try:
from src.webui.manager import setup_webui from src.webui.webui_server import get_webui_server
self.webui_server = get_webui_server()
if webui_mode == "development":
logger.info("📝 WebUI 开发模式已启用")
logger.info("🌐 后端 API 将运行在 http://0.0.0.0:8001")
logger.info("💡 请手动启动前端开发服务器: cd MaiBot-Dashboard && bun dev")
logger.info("💡 前端将运行在 http://localhost:7999")
else:
logger.info("✅ WebUI 生产模式已启用")
logger.info(f"🌐 WebUI 将运行在 http://0.0.0.0:8001")
logger.info("💡 请确保已构建前端: cd MaiBot-Dashboard && bun run build")
setup_webui(mode=webui_mode)
except Exception as e: except Exception as e:
logger.error(f"设置 WebUI 失败: {e}") logger.error(f"❌ 初始化 WebUI 服务器失败: {e}")
async def initialize(self): async def initialize(self):
"""初始化系统组件""" """初始化系统组件"""
@ -164,6 +163,10 @@ class MainSystem:
self.server.run(), self.server.run(),
] ]
# 如果 WebUI 服务器已初始化,添加到任务列表
if self.webui_server:
tasks.append(self.webui_server.start())
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
# async def forget_memory_task(self): # async def forget_memory_task(self):

View File

@ -28,7 +28,7 @@ class EmojiResponse(BaseModel):
query_count: int query_count: int
is_registered: bool is_registered: bool
is_banned: bool is_banned: bool
emotion: Optional[List[str]] # 解析后的 JSON emotion: Optional[str] # 直接返回字符串
record_time: float record_time: float
register_time: Optional[float] register_time: Optional[float]
usage_count: int usage_count: int
@ -58,7 +58,7 @@ class EmojiUpdateRequest(BaseModel):
description: Optional[str] = None description: Optional[str] = None
is_registered: Optional[bool] = None is_registered: Optional[bool] = None
is_banned: Optional[bool] = None is_banned: Optional[bool] = None
emotion: Optional[List[str]] = None emotion: Optional[str] = None
class EmojiUpdateResponse(BaseModel): class EmojiUpdateResponse(BaseModel):
@ -106,16 +106,6 @@ def verify_auth_token(authorization: Optional[str]) -> bool:
return True return True
def parse_emotion(emotion_str: Optional[str]) -> Optional[List[str]]:
"""解析情感标签 JSON 字符串"""
if not emotion_str:
return None
try:
return json.loads(emotion_str)
except (json.JSONDecodeError, TypeError):
return None
def emoji_to_response(emoji: Emoji) -> EmojiResponse: def emoji_to_response(emoji: Emoji) -> EmojiResponse:
"""将 Emoji 模型转换为响应对象""" """将 Emoji 模型转换为响应对象"""
return EmojiResponse( return EmojiResponse(
@ -127,7 +117,7 @@ def emoji_to_response(emoji: Emoji) -> EmojiResponse:
query_count=emoji.query_count, query_count=emoji.query_count,
is_registered=emoji.is_registered, is_registered=emoji.is_registered,
is_banned=emoji.is_banned, is_banned=emoji.is_banned,
emotion=parse_emotion(emoji.emotion), emotion=str(emoji.emotion) if emoji.emotion is not None else None,
record_time=emoji.record_time, record_time=emoji.record_time,
register_time=emoji.register_time, register_time=emoji.register_time,
usage_count=emoji.usage_count, usage_count=emoji.usage_count,
@ -264,12 +254,7 @@ async def update_emoji(emoji_id: int, request: EmojiUpdateRequest, authorization
if not update_data: if not update_data:
raise HTTPException(status_code=400, detail="未提供任何需要更新的字段") raise HTTPException(status_code=400, detail="未提供任何需要更新的字段")
# 处理情感标签(转换为 JSON # emotion 字段直接使用字符串,无需转换
if "emotion" in update_data:
if update_data["emotion"] is None:
update_data["emotion"] = None
else:
update_data["emotion"] = json.dumps(update_data["emotion"], ensure_ascii=False)
# 如果注册状态从 False 变为 True记录注册时间 # 如果注册状态从 False 变为 True记录注册时间
if "is_registered" in update_data and update_data["is_registered"] and not emoji.is_registered: if "is_registered" in update_data and update_data["is_registered"] and not emoji.is_registered:

View File

@ -1,109 +0,0 @@
"""WebUI 管理器 - 处理开发/生产环境的 WebUI 启动"""
import os
from pathlib import Path
from src.common.logger import get_logger
from .token_manager import get_token_manager
logger = get_logger("webui")
def setup_webui(mode: str = "production") -> bool:
"""
设置 WebUI
Args:
mode: 运行模式"development" "production"
Returns:
bool: 是否成功设置
"""
# 初始化 Token 管理器(确保 token 文件存在)
token_manager = get_token_manager()
current_token = token_manager.get_token()
logger.info(f"🔑 WebUI Access Token: {current_token}")
logger.info("💡 请使用此 Token 登录 WebUI")
if mode == "development":
return setup_dev_mode()
else:
return setup_production_mode()
def setup_dev_mode() -> bool:
"""设置开发模式 - 仅启用 CORS前端自行启动"""
from src.common.server import get_global_server
from .logs_ws import router as logs_router
# 注册 WebSocket 日志路由(开发模式也需要)
server = get_global_server()
server.register_router(logs_router)
logger.info("✅ WebSocket 日志推送路由已注册")
logger.info("📝 WebUI 开发模式已启用")
logger.info("🌐 请手动启动前端开发服务器: cd webui && npm run dev")
logger.info("💡 前端将运行在 http://localhost:7999")
return True
def setup_production_mode() -> bool:
"""设置生产模式 - 挂载静态文件"""
try:
from src.common.server import get_global_server
from starlette.responses import FileResponse
from .logs_ws import router as logs_router
import mimetypes
# 确保正确的 MIME 类型映射
mimetypes.init()
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("application/javascript", ".mjs")
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("application/json", ".json")
server = get_global_server()
# 注册 WebSocket 日志路由
server.register_router(logs_router)
logger.info("✅ WebSocket 日志推送路由已注册")
base_dir = Path(__file__).parent.parent.parent
static_path = base_dir / "webui" / "dist"
if not static_path.exists():
logger.warning(f"❌ WebUI 静态文件目录不存在: {static_path}")
logger.warning("💡 请先构建前端: cd webui && npm run build")
return False
if not (static_path / "index.html").exists():
logger.warning(f"❌ 未找到 index.html: {static_path / 'index.html'}")
logger.warning("💡 请确认前端已正确构建")
return False
# 处理 SPA 路由
@server.app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""服务单页应用"""
# API 路由不处理
if full_path.startswith("api/"):
return None
# 检查文件是否存在
file_path = static_path / full_path
if file_path.is_file():
# 自动检测 MIME 类型
media_type = mimetypes.guess_type(str(file_path))[0]
return FileResponse(file_path, media_type=media_type)
# 返回 index.htmlSPA 路由)
return FileResponse(static_path / "index.html", media_type="text/html")
host = os.getenv("HOST", "127.0.0.1")
port = os.getenv("PORT", "8000")
logger.info("✅ WebUI 生产模式已挂载")
logger.info(f"🌐 访问 http://{host}:{port} 查看 WebUI")
return True
except Exception as e:
logger.error(f"挂载 WebUI 静态文件失败: {e}")
return False

View File

@ -0,0 +1,148 @@
"""独立的 WebUI 服务器 - 运行在 0.0.0.0:8001"""
import os
import asyncio
import mimetypes
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from uvicorn import Config, Server as UvicornServer
from src.common.logger import get_logger
logger = get_logger("webui_server")
class WebUIServer:
"""独立的 WebUI 服务器"""
def __init__(self, host: str = "0.0.0.0", port: int = 8001):
self.host = host
self.port = port
self.app = FastAPI(title="MaiBot WebUI")
self._server = None
# 显示 Access Token
self._show_access_token()
# 重要:先注册 API 路由,再设置静态文件
self._register_api_routes()
self._setup_static_files()
def _show_access_token(self):
"""显示 WebUI Access Token"""
try:
from src.webui.token_manager import get_token_manager
token_manager = get_token_manager()
current_token = token_manager.get_token()
logger.info(f"🔑 WebUI Access Token: {current_token}")
logger.info("💡 请使用此 Token 登录 WebUI")
except Exception as e:
logger.error(f"❌ 获取 Access Token 失败: {e}")
def _setup_static_files(self):
"""设置静态文件服务"""
# 确保正确的 MIME 类型映射
mimetypes.init()
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("application/javascript", ".mjs")
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("application/json", ".json")
base_dir = Path(__file__).parent.parent.parent
static_path = base_dir / "webui" / "dist"
if not static_path.exists():
logger.warning(f"❌ WebUI 静态文件目录不存在: {static_path}")
logger.warning("💡 请先构建前端: cd webui && npm run build")
return
if not (static_path / "index.html").exists():
logger.warning(f"❌ 未找到 index.html: {static_path / 'index.html'}")
logger.warning("💡 请确认前端已正确构建")
return
# 处理 SPA 路由 - 注意:这个路由优先级最低
@self.app.get("/{full_path:path}", include_in_schema=False)
async def serve_spa(full_path: str):
"""服务单页应用 - 只处理非 API 请求"""
# 如果是根路径,直接返回 index.html
if not full_path or full_path == "/":
return FileResponse(static_path / "index.html", media_type="text/html")
# 检查是否是静态文件
file_path = static_path / full_path
if file_path.is_file() and file_path.exists():
# 自动检测 MIME 类型
media_type = mimetypes.guess_type(str(file_path))[0]
return FileResponse(file_path, media_type=media_type)
# 其他路径返回 index.htmlSPA 路由)
return FileResponse(static_path / "index.html", media_type="text/html")
logger.info(f"✅ WebUI 静态文件服务已配置: {static_path}")
def _register_api_routes(self):
"""注册所有 WebUI API 路由"""
try:
# 导入所有 WebUI 路由
from src.webui.routes import router as webui_router
from src.webui.logs_ws import router as logs_router
# 注册路由
self.app.include_router(webui_router)
self.app.include_router(logs_router)
logger.info("✅ WebUI API 路由已注册")
except Exception as e:
logger.error(f"❌ 注册 WebUI API 路由失败: {e}")
async def start(self):
"""启动服务器"""
config = Config(
app=self.app,
host=self.host,
port=self.port,
log_config=None,
access_log=False,
)
self._server = UvicornServer(config=config)
logger.info("🌐 WebUI 服务器启动中...")
logger.info(f"🌐 访问地址: http://{self.host}:{self.port}")
try:
await self._server.serve()
except Exception as e:
logger.error(f"❌ WebUI 服务器运行错误: {e}")
raise
async def shutdown(self):
"""关闭服务器"""
if self._server:
logger.info("正在关闭 WebUI 服务器...")
self._server.should_exit = True
try:
await asyncio.wait_for(self._server.shutdown(), timeout=3.0)
logger.info("✅ WebUI 服务器已关闭")
except asyncio.TimeoutError:
logger.warning("⚠️ WebUI 服务器关闭超时")
except Exception as e:
logger.error(f"❌ WebUI 服务器关闭失败: {e}")
finally:
self._server = None
# 全局 WebUI 服务器实例
_webui_server = None
def get_webui_server() -> WebUIServer:
"""获取全局 WebUI 服务器实例"""
global _webui_server
if _webui_server is None:
# 从环境变量读取配置
host = os.getenv("WEBUI_HOST", "0.0.0.0")
port = int(os.getenv("WEBUI_PORT", "8001"))
_webui_server = WebUIServer(host=host, port=port)
return _webui_server

View File

@ -1,6 +1,9 @@
# 麦麦主程序配置
HOST=127.0.0.1 HOST=127.0.0.1
PORT=8000 PORT=8000
# WebUI 配置 # WebUI 独立服务器配置
WEBUI_ENABLED=true WEBUI_ENABLED=true
WEBUI_MODE=production # 生产模式 WEBUI_MODE=production # 模式: development(开发) 或 production(生产)
WEBUI_HOST=0.0.0.0 # WebUI 服务器监听地址
WEBUI_PORT=8001 # WebUI 服务器端口

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,13 +5,13 @@
<link rel="icon" type="image/x-icon" href="/maimai.ico" /> <link rel="icon" type="image/x-icon" href="/maimai.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MaiBot Dashboard</title> <title>MaiBot Dashboard</title>
<script type="module" crossorigin src="/assets/index-B-xgVyqE.js"></script> <script type="module" crossorigin src="/assets/index-Cga6iLH3.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dtc2IqVY.js"> <link rel="modulepreload" crossorigin href="/assets/react-vendor-Dtc2IqVY.js">
<link rel="modulepreload" crossorigin href="/assets/router-BWgTyY51.js"> <link rel="modulepreload" crossorigin href="/assets/router-BWgTyY51.js">
<link rel="modulepreload" crossorigin href="/assets/charts-B1JvyJzO.js"> <link rel="modulepreload" crossorigin href="/assets/charts-B1JvyJzO.js">
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-nTGLnMlb.js"> <link rel="modulepreload" crossorigin href="/assets/ui-vendor-nTGLnMlb.js">
<link rel="modulepreload" crossorigin href="/assets/icons-D6w7t-x9.js"> <link rel="modulepreload" crossorigin href="/assets/icons-D6w7t-x9.js">
<link rel="stylesheet" crossorigin href="/assets/index-BGzEu9LP.css"> <link rel="stylesheet" crossorigin href="/assets/index-BmybQ2BG.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>