Skip to content

Commit 461167f

Browse files
authored
Merge pull request #1364 from Mai-with-u/dev
Dev
2 parents 0f84df7 + 03d4938 commit 461167f

38 files changed

Lines changed: 5224 additions & 261 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ elua.confirmed
6969
.Python
7070
build/
7171
develop-eggs/
72-
dist/
7372
downloads/
7473
eggs/
7574
.eggs/

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@
4444

4545
## 🔥 更新和安装
4646

47-
**最新版本: v0.11.0** ([更新日志](changelogs/changelog.md))
47+
**最新版本: v0.11.3** ([更新日志](changelogs/changelog.md))
4848

4949
可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
5050
可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器
5151
**GitHub 分支说明:**
5252
- `main`: 稳定发布版本(推荐)
53+
54+
5355
- `dev`: 开发测试版本(不稳定)
54-
- `classical`: 旧版本(停止维护)
56+
- `classical`: 经典版本(停止维护)
5557

5658
### 最新版本部署教程
5759
- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于 MaiCore 的新版本部署方式(与旧版本不兼容)

bot.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,6 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
107107

108108
logger.info("麦麦优雅关闭完成")
109109

110-
# 关闭日志系统,释放文件句柄
111-
shutdown_logging()
112-
113110
except Exception as e:
114111
logger.error(f"麦麦关闭失败: {e}", exc_info=True)
115112

@@ -215,6 +212,10 @@ def raw_main():
215212
# 创建事件循环
216213
loop = asyncio.new_event_loop()
217214
asyncio.set_event_loop(loop)
215+
216+
# 初始化 WebSocket 日志推送
217+
from src.common.logger import initialize_ws_handler
218+
initialize_ws_handler(loop)
218219

219220
try:
220221
# 执行初始化和任务调度
@@ -241,14 +242,16 @@ def raw_main():
241242
# 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭)
242243
if "loop" in locals() and loop and not loop.is_closed():
243244
loop.close()
244-
logger.info("事件循环已关闭")
245+
print("[主程序] 事件循环已关闭")
245246

246247
# 关闭日志系统,释放文件句柄
247248
try:
248249
shutdown_logging()
249250
except Exception as e:
250251
print(f"关闭日志系统时出错: {e}")
251252

252-
# 在程序退出前暂停,让你有机会看到输出
253-
# input("按 Enter 键退出...") # <--- 添加这行
254-
sys.exit(exit_code) # <--- 使用记录的退出码
253+
print("[主程序] 准备退出...")
254+
255+
# 使用 os._exit() 强制退出,避免被阻塞
256+
# 由于已经在 graceful_shutdown() 中完成了所有清理工作,这是安全的
257+
os._exit(exit_code)

changelogs/changelog.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Changelog
22

3-
## [0.11.2] - 2025-11-15
3+
## [0.11.3] - 2025-11-17
4+
### 功能更改和修复
5+
- 优化记忆提取策略
6+
- 优化黑话提取
7+
- 优化表达方式学习
8+
- 修改readme
9+
- 加入测试版webui
10+
11+
提示:清理旧的记忆数据和表达方式,表现更好
12+
方法:删除数据库中 expression jargon 和 thinking_back 的全部内容
13+
14+
## [0.11.2] - 2025-11-16
415
### 🌟 主要功能更改
516
- "海马体Agent"记忆系统上线,最新最好的记忆系统,默认已接入lpmm
617
- 添加黑话jargon学习系统

src/chat/knowledge/qa_manager.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,13 @@ async def process_query(
9898

9999
return result, ppr_node_weights
100100

101-
async def get_knowledge(self, question: str) -> Optional[str]:
102-
"""获取知识"""
101+
async def get_knowledge(self, question: str, limit: int = 5) -> Optional[str]:
102+
"""获取知识
103+
104+
Args:
105+
question: 查询问题
106+
limit: 返回的相关知识条数
107+
"""
103108
# 处理查询
104109
processed_result = await self.process_query(question)
105110
if processed_result is not None:
@@ -109,16 +114,27 @@ async def get_knowledge(self, question: str) -> Optional[str]:
109114
logger.debug("知识库查询结果为空,可能是知识库中没有相关内容")
110115
return None
111116

117+
limit = max(1, limit) if isinstance(limit, int) else 5
118+
112119
knowledge = [
113120
(
114121
self.embed_manager.paragraphs_embedding_store.store[res[0]].str,
115122
res[1],
116123
)
117124
for res in query_res
118125
]
119-
found_knowledge = "\n".join(
120-
[f"第{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}" for i, k in enumerate(knowledge)]
121-
)
126+
127+
# max_score = max([k[1] for k in knowledge]) if knowledge else None
128+
selected_knowledge = knowledge[:limit]
129+
130+
formatted_knowledge = [
131+
f"第{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}"
132+
for i, k in enumerate(selected_knowledge)
133+
]
134+
# if max_score is not None:
135+
# formatted_knowledge.insert(0, f"最高相关系数:{max_score}")
136+
137+
found_knowledge = "\n".join(formatted_knowledge)
122138
if len(found_knowledge) > MAX_KNOWLEDGE_LENGTH:
123139
found_knowledge = found_knowledge[:MAX_KNOWLEDGE_LENGTH] + "\n"
124140
return found_knowledge

src/common/database/database_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ class Expression(BaseModel):
311311
context = TextField(null=True)
312312
up_content = TextField(null=True)
313313

314+
content_list = TextField(null=True)
315+
count = IntegerField(default=1)
314316
last_active_time = FloatField()
315317
chat_id = TextField(index=True)
316318
create_date = FloatField(null=True) # 创建日期,允许为空以兼容老数据

src/common/logger.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# 全局handler实例,避免重复创建
2020
_file_handler = None
2121
_console_handler = None
22+
_ws_handler = None
2223

2324

2425
def get_file_handler():
@@ -59,6 +60,35 @@ def get_console_handler():
5960
return _console_handler
6061

6162

63+
def get_ws_handler():
64+
"""获取 WebSocket handler 单例"""
65+
global _ws_handler
66+
if _ws_handler is None:
67+
_ws_handler = WebSocketLogHandler()
68+
# WebSocket handler 推送所有级别的日志
69+
_ws_handler.setLevel(logging.DEBUG)
70+
return _ws_handler
71+
72+
73+
def initialize_ws_handler(loop):
74+
"""初始化 WebSocket handler 的事件循环
75+
76+
Args:
77+
loop: asyncio 事件循环
78+
"""
79+
handler = get_ws_handler()
80+
handler.set_loop(loop)
81+
82+
# 为 WebSocket handler 设置 JSON 格式化器(与文件格式相同)
83+
handler.setFormatter(file_formatter)
84+
85+
# 添加到根日志记录器
86+
root_logger = logging.getLogger()
87+
if handler not in root_logger.handlers:
88+
root_logger.addHandler(handler)
89+
print("[日志系统] ✅ WebSocket 日志推送已启用")
90+
91+
6292
class TimestampedFileHandler(logging.Handler):
6393
"""基于时间戳的文件处理器,简单的轮转份数限制"""
6494

@@ -145,12 +175,78 @@ def close(self):
145175
super().close()
146176

147177

178+
class WebSocketLogHandler(logging.Handler):
179+
"""WebSocket 日志处理器 - 将日志实时推送到前端"""
180+
181+
_log_counter = 0 # 类级别计数器,确保 ID 唯一性
182+
183+
def __init__(self, loop=None):
184+
super().__init__()
185+
self.loop = loop
186+
self._initialized = False
187+
188+
def set_loop(self, loop):
189+
"""设置事件循环"""
190+
self.loop = loop
191+
self._initialized = True
192+
193+
def emit(self, record):
194+
"""发送日志到 WebSocket 客户端"""
195+
if not self._initialized or self.loop is None:
196+
return
197+
198+
try:
199+
# 获取格式化后的消息
200+
# 对于 structlog,formatted message 包含完整的日志信息
201+
formatted_msg = self.format(record) if self.formatter else record.getMessage()
202+
203+
# 如果是 JSON 格式(文件格式化器),解析它
204+
message = formatted_msg
205+
try:
206+
import json
207+
log_dict = json.loads(formatted_msg)
208+
message = log_dict.get('event', formatted_msg)
209+
except (json.JSONDecodeError, ValueError):
210+
# 不是 JSON,直接使用消息
211+
message = formatted_msg
212+
213+
# 生成唯一 ID: 时间戳毫秒 + 自增计数器
214+
WebSocketLogHandler._log_counter += 1
215+
log_id = f"{int(record.created * 1000)}_{WebSocketLogHandler._log_counter}"
216+
217+
# 格式化日志数据
218+
log_data = {
219+
"id": log_id,
220+
"timestamp": datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S"),
221+
"level": record.levelname,
222+
"module": record.name,
223+
"message": message,
224+
}
225+
226+
# 异步广播日志(不阻塞日志记录)
227+
try:
228+
import asyncio
229+
from src.webui.logs_ws import broadcast_log
230+
231+
asyncio.run_coroutine_threadsafe(
232+
broadcast_log(log_data),
233+
self.loop
234+
)
235+
except Exception:
236+
# WebSocket 推送失败不影响日志记录
237+
pass
238+
239+
except Exception:
240+
# 不要让 WebSocket 错误影响日志系统
241+
self.handleError(record)
242+
243+
148244
# 旧的轮转文件处理器已移除,现在使用基于时间戳的处理器
149245

150246

151247
def close_handlers():
152248
"""安全关闭所有handler"""
153-
global _file_handler, _console_handler
249+
global _file_handler, _console_handler, _ws_handler
154250

155251
if _file_handler:
156252
_file_handler.close()
@@ -159,6 +255,10 @@ def close_handlers():
159255
if _console_handler:
160256
_console_handler.close()
161257
_console_handler = None
258+
259+
if _ws_handler:
260+
_ws_handler.close()
261+
_ws_handler = None
162262

163263

164264
def remove_duplicate_handlers(): # sourcery skip: for-append-to-extend, list-comprehension
@@ -843,8 +943,8 @@ def cleanup_task():
843943

844944
def shutdown_logging():
845945
"""优雅关闭日志系统,释放所有文件句柄"""
846-
logger = get_logger("logger")
847-
logger.info("正在关闭日志系统...")
946+
# 先输出到控制台,避免日志系统关闭后无法输出
947+
print("[logger] 正在关闭日志系统...")
848948

849949
# 关闭所有handler
850950
root_logger = logging.getLogger()
@@ -865,4 +965,5 @@ def shutdown_logging():
865965
handler.close()
866966
logger_obj.removeHandler(handler)
867967

868-
logger.info("日志系统已关闭")
968+
# 使用 print 而不是 logger,因为 logger 已经关闭
969+
print("[logger] 日志系统已关闭")

src/common/server.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from fastapi.middleware.cors import CORSMiddleware # 新增导入
33
from typing import Optional
44
from uvicorn import Config, Server as UvicornServer
5+
import asyncio
56
import os
67
from rich.traceback import install
78

@@ -82,8 +83,17 @@ async def shutdown(self):
8283
"""安全关闭服务器"""
8384
if self._server:
8485
self._server.should_exit = True
85-
await self._server.shutdown()
86-
self._server = None
86+
try:
87+
# 添加 3 秒超时,避免 shutdown 永久挂起
88+
await asyncio.wait_for(self._server.shutdown(), timeout=3.0)
89+
except asyncio.TimeoutError:
90+
# 超时就强制标记为 None,让垃圾回收处理
91+
pass
92+
except Exception:
93+
# 忽略其他异常
94+
pass
95+
finally:
96+
self._server = None
8797

8898
def get_app(self) -> FastAPI:
8999
"""获取 FastAPI 实例"""

src/config/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656

5757
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
5858
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
59-
MMC_VERSION = "0.11.2"
59+
MMC_VERSION = "0.11.3"
6060

6161

6262
def get_key_comment(toml_table, key):

0 commit comments

Comments
 (0)