Compare commits

..

18 Commits

Author SHA1 Message Date
a07de626ad fix: 修复AI重复调用问题,统一消息处理流程
- _process_message 只保存用户消息并通知
- 新增 generate_ai_response action 统一处理AI回复
- 避免重复调用AI和重复发送消息
2026-04-12 00:34:30 +08:00
f3636dddd6 fix: 添加 event_id 去重机制,防止消息重复处理
- 添加 processed_events 集合追踪已处理消息
- _process_message 检查 event_id 是否已处理
- 限制集合大小防止内存过大
2026-04-12 00:26:04 +08:00
66fa5db3d7 fix: 移除前端 [Matrix] 来源标记显示 2026-04-12 00:16:16 +08:00
2c007c4801 fix: 移除消息来源标记[网页] 2026-04-12 00:12:57 +08:00
027830b0d6 fix: Matrix用户消息同步到网页端
- 新增 user_message action 回调
- handle_matrix_message 处理 user_message 通知
- 网页端 WebSocket 接收用户消息
2026-04-12 00:07:19 +08:00
d018342b45 fix: 修复messages类型问题和设备验证问题
1. 使用 get_conversation_history 返回字典列表
2. send_message 增加设备验证回退机制
3. 当 nio 发送失败时回退到 HTTP API

依赖:python-olm, cachetools, atomicwrites, peewee
2026-04-11 23:49:15 +08:00
ac7a329e53 fix: 添加详细的加密消息调试日志
- 打印 MegolmEvent 的所有属性
- 尝试多种方式获取解密后的内容
- 尝试手动调用 decrypt_event

问题:matrix_store 目录为空,没有密钥无法解密 MegolmEvent
2026-04-11 23:03:48 +08:00
585d4ce39c debug: 添加MegolmEvent详细调试信息
- 打印event的所有属性
- 尝试多种方式获取解密内容
- 尝试手动解密decrypt_event
2026-04-11 23:00:19 +08:00
8742d5932f fix: 移除 sync_forever 的 full_state 参数
- sync_forever 只做增量同步,不重复获取完整状态
- 增加异常堆栈日志便于调试
2026-04-11 22:40:25 +08:00
d72534c0c3 fix: 使用sync_forever和加密存储处理加密消息
- 改用 sync_forever 自动处理事件回调
- 添加 matrix_store 存储路径用于密钥存储
- 初始同步后再注册回调
- 注册 MegolmEvent 处理器

问题:两个房间都是加密状态,需要密钥解密
2026-04-11 22:38:47 +08:00
6625dda349 fix: 使用 sync_forever 自动处理事件,添加加密存储
- 改用 sync_forever 替代手动 sync 循环
- 添加 store_path 加密存储路径
- 注册 MegolmEvent 加密消息回调
- 添加更详细的日志输出
2026-04-11 22:36:05 +08:00
0d25cdc344 feat: Matrix改用nio库支持加密消息
- 使用 matrix-nio 库替代 HTTP API
- 支持解密加密消息(MegolmEvent)
- 添加 matrix_password 配置项
- 发送'正在输入'状态提示
2026-04-11 21:39:04 +08:00
65297d7321 fix: Matrix改用HTTP API,修复HTTPS不可用问题,网页端消息同步到Matrix 2026-04-11 12:56:00 +08:00
d3236413f3 fix: 修复语法错误和导入问题 2026-04-11 12:32:23 +08:00
b228283133 feat: 网页端固定主用户,Matrix /new创建新会话,实时同步 2026-04-11 12:29:11 +08:00
b05a03e198 fix: Matrix连接改为非阻塞模式,修复服务启动阻塞问题 2026-04-11 12:22:03 +08:00
fd583132d7 feat: 支持Matrix access_token登录,配置AI模型接口 2026-04-11 11:58:14 +08:00
c27fc8c02f docs: 添加README和.gitignore 2026-04-11 11:52:23 +08:00
27 changed files with 785 additions and 196 deletions

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
*.egg-info/
dist/
build/
# Database
*.db
*.sqlite
*.sqlite3
# Logs
logs/
*.log
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Config (if contains secrets)
config.local.*
*.secret

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# AI 对话系统
支持网页端和Matrix端实时同步对话的AI聊天系统。
## 功能特性
- **网页端对话**: 通过浏览器与AI进行对话
- **Matrix端对话**: 配置Matrix Bot用户可通过Matrix与AI对话
- **实时同步**: 同一用户在网页端和Matrix端的对话自动同步
- **后台管理**: 用户管理、对话记录、系统配置
## 系统架构
```
┌─────────────────────────────────────────────────────────┐
│ AI 对话系统 │
├─────────────────────────────────────────────────────────┤
│ 网页端 (用户A) ←──┐ ┌──→ Matrix端 │
│ │ 同一会话同步 │ (用户A) │
├─────────────────────────────────────────────────────────┤
│ 后端服务 (FastAPI + WebSocket) │
│ - 会话管理 │
│ - 消息存储 │
│ - Matrix Bot 集成 │
│ - AI 模型调用 │
├─────────────────────────────────────────────────────────┤
│ 后台管理 │
│ - 用户管理 │
│ - 对话记录 │
│ - 系统配置 │
└─────────────────────────────────────────────────────────┘
```
## 端口分配
- 19020: 主服务(网页端 + API
- 后台管理: http://localhost:19020/admin
## 快速启动
```bash
# 安装依赖
pip3 install -r requirements.txt
# 启动服务
./start.sh
# 检查状态
./status.sh
# 停止服务
./stop.sh
```
## 配置Matrix Bot
在后台管理页面配置以下参数:
| 配置项 | 说明 |
|--------|------|
| matrix_homeserver | Matrix服务器地址如 https://matrix.tphai.com |
| matrix_username | Matrix Bot用户名@ai-bot:matrix.org |
| matrix_password | Matrix Bot密码 |
配置完成后其他Matrix用户可以直接与Bot对话。
## API接口
### 会话管理
- `GET /api/conversations` - 获取会话列表
- `POST /api/conversations` - 创建新会话
- `GET /api/conversations/{id}/messages` - 获取会话消息
- `DELETE /api/conversations/{id}` - 删除会话
### 后台管理
- `GET /api/admin/stats` - 获取统计数据
- `GET /api/admin/users` - 获取用户列表
- `GET /api/admin/conversations` - 获取对话列表
- `GET /api/admin/config` - 获取系统配置
- `POST /api/admin/config` - 更新系统配置
## 技术栈
- **后端**: FastAPI + WebSocket
- **数据库**: SQLite (SQLAlchemy)
- **Matrix**: matrix-nio
- **前端**: HTML + JavaScript (原生)
- **AI**: 可配置任意LLM API默认使用本地LLM Proxy
## 仓库地址
http://192.168.2.8:12007/coder/ai-chat-system

Binary file not shown.

40
init_config.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""初始化配置"""
import sys
sys.path.insert(0, '/home/xian/.openclaw/workspace-coder/works/ai-chat')
from models import SessionLocal, SystemConfig, init_db
# 初始化数据库
init_db()
db = SessionLocal()
# 配置AI模型
configs = [
{'key': 'ai_api_base', 'value': 'http://192.168.2.17:19007/v1', 'description': 'AI模型API地址'},
{'key': 'ai_api_key', 'value': 'xxxx', 'description': 'AI模型API Key'},
{'key': 'ai_model', 'value': 'auto', 'description': 'AI模型名称'},
# 配置Matrix Bot
{'key': 'matrix_homeserver', 'value': 'http://matrix.tphai.com', 'description': 'Matrix服务器地址'},
{'key': 'matrix_username', 'value': '@tester:matrix.tphai.com', 'description': 'Matrix Bot用户名'},
{'key': 'matrix_access_token', 'value': 'syt_dGVzdGVy_eMwWfezCXSyBgHzvkmly_4dWFtM', 'description': 'Matrix Bot Access Token'},
]
for config in configs:
existing = db.query(SystemConfig).filter(SystemConfig.key == config['key']).first()
if existing:
existing.value = config['value']
existing.description = config['description']
else:
db.add(SystemConfig(**config))
db.commit()
print("配置已写入数据库")
# 显示配置
for c in db.query(SystemConfig).all():
print(f"{c.key}: {c.value}")
db.close()

View File

@@ -1,18 +0,0 @@
Traceback (most recent call last):
File "/home/xian/.openclaw/workspace-coder/works/ai-chat/main.py", line 17, in <module>
from models import init_db, get_db, User, Conversation, Message, SystemConfig
File "/home/xian/.openclaw/workspace-coder/works/ai-chat/models/__init__.py", line 1, in <module>
from .database import Base, engine, SessionLocal, get_db, init_db
File "/home/xian/.openclaw/workspace-coder/works/ai-chat/models/database.py", line 33, in <module>
class Conversation(Base):
File "/home/xian/.local/lib/python3.10/site-packages/sqlalchemy/orm/decl_api.py", line 195, in __init__
_as_declarative(reg, cls, dict_)
File "/home/xian/.local/lib/python3.10/site-packages/sqlalchemy/orm/decl_base.py", line 247, in _as_declarative
return _MapperConfig.setup_mapping(registry, cls, dict_, None, {})
File "/home/xian/.local/lib/python3.10/site-packages/sqlalchemy/orm/decl_base.py", line 328, in setup_mapping
return _ClassScanMapperConfig(
File "/home/xian/.local/lib/python3.10/site-packages/sqlalchemy/orm/decl_base.py", line 574, in __init__
self._extract_mappable_attributes()
File "/home/xian/.local/lib/python3.10/site-packages/sqlalchemy/orm/decl_base.py", line 1507, in _extract_mappable_attributes
raise exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: Attribute name 'metadata' is reserved when using the Declarative API.

260
main.py
View File

@@ -14,7 +14,7 @@ import logging
from datetime import datetime
import os
from models import init_db, get_db, User, Conversation, Message, SystemConfig
from models import init_db, get_db, SessionLocal, User, Conversation, Message, SystemConfig
from services import ai_service, ConversationService, matrix_bot
# 配置日志
@@ -83,15 +83,14 @@ async def admin(request: Request):
# ==================== API路由 ====================
# 固定主用户ID与Matrix AI用户关联
MAIN_USER_ID = "main_user"
@app.get("/api/conversations")
async def get_conversations(user_id: str = None, db: Session = Depends(get_db)):
"""获取会话列表"""
if not user_id:
# 临时用户ID实际应用中应该从session获取
user_id = "web_anonymous"
async def get_conversations(db: Session = Depends(get_db)):
"""获取会话列表(使用固定主用户)"""
conv_service = ConversationService(db)
user = conv_service.get_or_create_user(user_id, user_type='web')
user = conv_service.get_or_create_user(MAIN_USER_ID, display_name="主用户", user_type='web')
conversations = conv_service.get_user_conversations(user.id)
return {
@@ -106,15 +105,30 @@ async def get_conversations(user_id: str = None, db: Session = Depends(get_db)):
]
}
@app.get("/api/conversations/latest")
async def get_latest_conversation(db: Session = Depends(get_db)):
"""获取最新会话用于Matrix同步"""
conv_service = ConversationService(db)
user = conv_service.get_or_create_user(MAIN_USER_ID, display_name="主用户", user_type='web')
conversations = conv_service.get_user_conversations(user.id)
if conversations:
latest = conversations[0] # 已按更新时间倒序
return {
"conversation": {
"id": latest.conversation_id,
"title": latest.title or "新对话",
"updated_at": latest.updated_at.isoformat()
}
}
return {"conversation": None}
@app.post("/api/conversations")
async def create_conversation(user_id: str = None, db: Session = Depends(get_db)):
"""创建新会话"""
if not user_id:
user_id = "web_anonymous"
async def create_conversation(db: Session = Depends(get_db)):
"""创建新会话(使用固定主用户)"""
conv_service = ConversationService(db)
user = conv_service.get_or_create_user(user_id, user_type='web')
user = conv_service.get_or_create_user(MAIN_USER_ID, display_name="主用户", user_type='web')
conversation = conv_service.create_conversation(user.id)
return {
@@ -165,10 +179,12 @@ async def delete_conversation(conversation_id: str, db: Session = Depends(get_db
@app.websocket("/ws/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: str, db: Session = Depends(get_db)):
"""WebSocket连接 - 实时对话"""
await manager.connect(websocket, user_id)
"""WebSocket连接 - 实时对话(所有连接使用主用户)"""
# 统一使用主用户ID
actual_user_id = MAIN_USER_ID
await manager.connect(websocket, actual_user_id)
conv_service = ConversationService(db)
user = conv_service.get_or_create_user(user_id, user_type='web')
user = conv_service.get_or_create_user(MAIN_USER_ID, display_name="主用户", user_type='web')
current_conversation_id = None
@@ -227,8 +243,8 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str, db: Session = D
source='web'
)
# 通知用户消息已收到
await manager.send_to_user(user_id, {
# 广播用户消息(同步到所有客户端)
await manager.send_to_user(MAIN_USER_ID, {
"type": "user_message",
"conversation_id": conversation_id,
"message": {
@@ -240,6 +256,10 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str, db: Session = D
}
})
# 同步到Matrix如果有房间
if matrix_bot.is_running and matrix_bot.last_room_id:
await matrix_bot.send_message(matrix_bot.last_room_id, message)
# 获取对话历史
history = conv_service.get_conversation_history(conversation_id, limit=20)
@@ -255,18 +275,23 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str, db: Session = D
source='web'
)
# 发送AI回复
await manager.send_to_user(user_id, {
# 广播AI回复(同步到所有客户端)
await manager.send_to_user(MAIN_USER_ID, {
"type": "assistant_message",
"conversation_id": conversation_id,
"message": {
"id": assistant_msg.id,
"role": "assistant",
"content": ai_response,
"source": "web",
"created_at": assistant_msg.created_at.isoformat()
}
})
# 同步AI回复到Matrix
if matrix_bot.is_running and matrix_bot.last_room_id:
await matrix_bot.send_message(matrix_bot.last_room_id, ai_response)
except Exception as e:
logger.error(f"AI调用失败: {e}")
await websocket.send_json({
@@ -379,7 +404,15 @@ async def update_config(data: dict, db: Session = Depends(get_db)):
db.commit()
# 如果更新了Matrix配置重新连接
# 根据配置类型执行相应操作
if key.startswith('ai_'):
# 更新AI服务配置
configs = {c.key: c.value for c in db.query(SystemConfig).all()}
api_base = configs.get('ai_api_base', 'http://192.168.2.17:19007/v1')
api_key = configs.get('ai_api_key', 'xxxx')
model = configs.get('ai_model', 'auto')
ai_service.update_config(api_base, api_key, model)
if key.startswith('matrix_') and matrix_bot.is_running:
await matrix_bot.disconnect()
await matrix_bot.init_from_config()
@@ -389,47 +422,140 @@ async def update_config(data: dict, db: Session = Depends(get_db)):
# ==================== Matrix消息处理回调 ====================
async def handle_matrix_message(conversation_id: str, user_message: str, user_id: str, room_id: str):
async def handle_matrix_message(action: str, conversation_id: str = None, user_message: str = None, room_id: str = None, message_id: int = None, sender: str = None):
"""处理从Matrix收到的消息"""
db = SessionLocal()
try:
conv_service = ConversationService(db)
# 获取会话历史
history = conv_service.get_conversation_history(conversation_id, limit=20)
# 调用AI
ai_response = await ai_service.chat(history)
# 保存AI回复
conversation = conv_service.get_conversation(conversation_id)
if conversation:
conv_service.add_message(
conversation_id=conversation.id,
role='assistant',
content=ai_response,
source='matrix'
)
# 发送到Matrix
await matrix_bot.send_message(room_id, ai_response)
# 同时推送到网页端
await manager.send_to_user(user_id, {
"type": "assistant_message",
"conversation_id": conversation_id,
"message": {
"role": "assistant",
"content": ai_response,
"source": "matrix",
"created_at": datetime.utcnow().isoformat()
}
})
except Exception as e:
logger.error(f"处理Matrix消息失败: {e}")
finally:
db.close()
if action == "new_conversation":
# 创建新会话 - 通知WebSocket客户端
await manager.send_to_user(MAIN_USER_ID, {
"type": "new_conversation",
"conversation_id": conversation_id
})
return
if action == "user_message":
# 用户消息 - 同步到网页端
db = SessionLocal()
try:
conv_service = ConversationService(db)
conversation = conv_service.get_conversation(conversation_id)
if conversation:
# 发送用户消息通知到网页端
await manager.send_to_user(MAIN_USER_ID, {
"type": "user_message",
"conversation_id": conversation_id,
"message": {
"id": message_id,
"role": "user",
"content": user_message,
"source": "matrix",
"sender": sender,
"created_at": datetime.now().isoformat()
}
})
finally:
db.close()
return
if action == "generate_ai_response":
# 生成AI回复并同步
db = SessionLocal()
try:
conv_service = ConversationService(db)
conversation = conv_service.get_conversation(conversation_id)
if conversation:
# 发送"正在输入"状态
try:
await matrix_bot.client.room_typing(room_id, typing_state=True)
except:
pass
# 获取会话历史
history = conv_service.get_conversation_history(conversation_id, limit=20)
# 调用AI
ai_response = await ai_service.chat(history)
# 保存AI回复
assistant_msg = conv_service.add_message(
conversation_id=conversation.id,
role='assistant',
content=ai_response,
source='matrix'
)
# 发送到Matrix
await matrix_bot.send_message(room_id, ai_response)
# 关闭"正在输入"状态
try:
await matrix_bot.client.room_typing(room_id, typing_state=False)
except:
pass
# 同步到网页端WebSocket
await manager.send_to_user(MAIN_USER_ID, {
"type": "assistant_message",
"conversation_id": conversation_id,
"message": {
"id": assistant_msg.id,
"role": "assistant",
"content": ai_response,
"source": "matrix",
"created_at": assistant_msg.created_at.isoformat()
}
})
logger.info(f"Matrix AI回复已发送: {ai_response[:50]}")
except Exception as e:
logger.error(f"生成AI回复失败: {e}")
await matrix_bot.send_message(room_id, f"处理消息时出错: {str(e)}")
finally:
db.close()
return
if action == "chat":
db = SessionLocal()
try:
conv_service = ConversationService(db)
# 获取会话历史
history = conv_service.get_conversation_history(conversation_id, limit=20)
# 调用AI
ai_response = await ai_service.chat(history)
# 保存AI回复
conversation = conv_service.get_conversation(conversation_id)
if conversation:
assistant_msg = conv_service.add_message(
conversation_id=conversation.id,
role='assistant',
content=ai_response,
source='matrix'
)
# 发送到Matrix
await matrix_bot.send_message(room_id, ai_response)
# 同步到网页端WebSocket
await manager.send_to_user(MAIN_USER_ID, {
"type": "assistant_message",
"conversation_id": conversation_id,
"message": {
"id": assistant_msg.id,
"role": "assistant",
"content": ai_response,
"source": "matrix",
"created_at": assistant_msg.created_at.isoformat()
}
})
except Exception as e:
logger.error(f"处理Matrix消息失败: {e}")
await matrix_bot.send_message(room_id, f"处理消息时出错: {str(e)}")
finally:
db.close()
# ==================== 启动和关闭 ====================
@@ -440,6 +566,18 @@ async def startup():
init_db()
logger.info("数据库初始化完成")
# 从数据库加载AI配置
db = SessionLocal()
try:
configs = {c.key: c.value for c in db.query(SystemConfig).all()}
api_base = configs.get('ai_api_base', 'http://192.168.2.17:19007/v1')
api_key = configs.get('ai_api_key', 'xxxx')
model = configs.get('ai_model', 'auto')
ai_service.update_config(api_base, api_key, model)
logger.info(f"AI配置已加载: {api_base}, model={model}")
finally:
db.close()
# 初始化Matrix Bot
await matrix_bot.init_from_config()
if matrix_bot.is_running:

View File

@@ -0,0 +1,7 @@
@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk
@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU
@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE
@tester:matrix.tphai.com MIPPYHRVAS matrix-ed25519 s8Ol56sxLCjCOi0Gkv/Kj7LqVMp/8ZmuAJ6QA1rUi7o
@tester:matrix.tphai.com UKJGJYQQLT matrix-ed25519 opC9rhsz1nzrvQqNWMKTF5FxWIGuHTDfixx+q/Y8ea0
@tester:matrix.tphai.com UPMZGRLESG matrix-ed25519 86c6XPCIYHgesq83C2k5xhXNa0EYMnqTq4jFrTwJX8I
@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ

View File

@@ -0,0 +1,8 @@
@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk
@tester:matrix.tphai.com BDTRXIGPBE matrix-ed25519 gjQNtLEpIEYCjmzUx5ma91G498n4UADh84KUmiReJUM
@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU
@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE
@tester:matrix.tphai.com MIPPYHRVAS matrix-ed25519 s8Ol56sxLCjCOi0Gkv/Kj7LqVMp/8ZmuAJ6QA1rUi7o
@tester:matrix.tphai.com UKJGJYQQLT matrix-ed25519 opC9rhsz1nzrvQqNWMKTF5FxWIGuHTDfixx+q/Y8ea0
@tester:matrix.tphai.com UPMZGRLESG matrix-ed25519 86c6XPCIYHgesq83C2k5xhXNa0EYMnqTq4jFrTwJX8I
@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ

View File

@@ -0,0 +1,4 @@
@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ
@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk
@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU
@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE

View File

@@ -0,0 +1,5 @@
@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk
@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU
@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE
@tester:matrix.tphai.com MIPPYHRVAS matrix-ed25519 s8Ol56sxLCjCOi0Gkv/Kj7LqVMp/8ZmuAJ6QA1rUi7o
@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ

View File

@@ -0,0 +1,6 @@
@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ
@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk
@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU
@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE
@tester:matrix.tphai.com MIPPYHRVAS matrix-ed25519 s8Ol56sxLCjCOi0Gkv/Kj7LqVMp/8ZmuAJ6QA1rUi7o
@tester:matrix.tphai.com UKJGJYQQLT matrix-ed25519 opC9rhsz1nzrvQqNWMKTF5FxWIGuHTDfixx+q/Y8ea0

25
reset_config.py Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""重置配置 - 使用mock模式"""
import sys
sys.path.insert(0, '/home/xian/.openclaw/workspace-coder/works/ai-chat')
from models import SessionLocal, SystemConfig, init_db
init_db()
db = SessionLocal()
# 删除所有AI配置让服务使用mock模式
keys_to_delete = ['ai_api_base', 'ai_api_key', 'ai_model']
for key in keys_to_delete:
c = db.query(SystemConfig).filter(SystemConfig.key == key).first()
if c:
db.delete(c)
# 保留Matrix配置
print("当前配置:")
for c in db.query(SystemConfig).all():
print(f" {c.key}: {c.value[:20]}...")
db.commit()
print("\nAI将使用mock模式因为没有配置AI API")
db.close()

View File

@@ -2,27 +2,40 @@
AI服务 - 调用大模型API
"""
import httpx
from typing import List, Dict, Optional
from typing import List, Dict, AsyncGenerator
import json
import logging
logger = logging.getLogger(__name__)
class AIService:
def __init__(self, api_base: str = "http://192.168.2.17:19007/v1", api_key: str = "sk-local", model: str = "qwen3.5-4b"):
def __init__(self):
self.api_base = ""
self.api_key = ""
self.model = ""
self.use_mock = True
def update_config(self, api_base: str, api_key: str, model: str):
"""更新配置"""
self.api_base = api_base
self.api_key = api_key
self.model = model
# 如果配置完整则使用真实API否则使用mock
self.use_mock = not (api_base and model)
logger.info(f"AI配置已更新: api_base={api_base}, model={model}, use_mock={self.use_mock}")
async def chat(self, messages: List[Dict], stream: bool = False) -> str:
async def chat(self, messages: List[Dict]) -> str:
"""
调用AI模型进行对话
Args:
messages: 对话历史 [{"role": "user", "content": "..."}]
stream: 是否流式输出
Returns:
AI回复内容
"""
# 如果使用mock模式返回模拟回复
if self.use_mock:
logger.info("使用Mock模式回复")
last_msg = messages[-1]['content'] if messages else "你好"
return f"这是一个测试回复。您说的是:{last_msg}\n\n请配置有效的AI服务地址和模型才能获得真正的AI回复。"
# 调用真实API
url = f"{self.api_base}/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
@@ -31,21 +44,35 @@ class AIService:
payload = {
"model": self.model,
"messages": messages,
"stream": stream,
"temperature": 0.7,
"max_tokens": 2000
}
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
return data['choices'][0]['message']['content']
logger.info(f"调用AI API: {url}, model={self.model}")
try:
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
return data['choices'][0]['message']['content']
except Exception as e:
logger.error(f"AI API调用失败: {e}")
# API失败时返回模拟回复
last_msg = messages[-1]['content'] if messages else "你好"
return f"AI服务暂时不可用错误{str(e)})。您说的是:{last_msg}"
async def chat_stream(self, messages: List[Dict]):
async def chat_stream(self, messages: List[Dict]) -> AsyncGenerator[str, None]:
"""
流式调用AI模型
"""
if self.use_mock:
last_msg = messages[-1]['content'] if messages else "你好"
reply = f"这是一个测试回复。您说的是:{last_msg}"
for char in reply:
yield char
return
url = f"{self.api_base}/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",

View File

@@ -1,171 +1,348 @@
"""
Matrix Bot 服务 - 处理Matrix消息收发
Matrix Bot 服务 - 使用 nio 库处理消息收发(支持加密)
"""
import asyncio
import json
from typing import Optional, Callable
from nio import AsyncClient, MatrixRoom, RoomMessageText, LoginResponse, SyncResponse
import logging
import os
from typing import Optional, Callable
from nio import AsyncClient, RoomMessageText, MegolmEvent
from models import SessionLocal, User, Conversation, Message, MatrixRoomMapping, SystemConfig
from models import SessionLocal, User, Conversation, Message, SystemConfig
from services.conversation_service import ConversationService
from services import ai_service
logger = logging.getLogger(__name__)
MAIN_USER_ID = "main_user"
STORE_PATH = "/home/xian/.openclaw/workspace-coder/works/ai-chat/matrix_store"
class MatrixBot:
def __init__(self):
self.client: Optional[AsyncClient] = None
self.homeserver: str = ""
self.username: str = ""
self.user_id: str = ""
self.password: str = ""
self.is_running: bool = False
self.on_message_callback: Optional[Callable] = None
self.last_room_id: str = ""
self.processed_events: set = set() # 已处理的事件ID防止重复处理
async def init_from_config(self):
"""从数据库配置初始化"""
db = SessionLocal()
try:
configs = {c.key: c.value for c in db.query(SystemConfig).all()}
self.homeserver = configs.get('matrix_homeserver', 'https://matrix.tphai.com')
self.username = configs.get('matrix_username', '')
self.password = configs.get('matrix_password', '')
self.homeserver = configs.get('matrix_homeserver', 'http://matrix.tphai.com')
self.user_id = configs.get('matrix_username', '@tester:matrix.tphai.com')
self.password = configs.get('matrix_password', 'tester12345@!')
if self.username and self.password:
await self.connect()
logger.info(f"Matrix配置: homeserver={self.homeserver}, user={self.user_id}")
if self.user_id and self.password:
# 创建存储目录
os.makedirs(STORE_PATH, exist_ok=True)
# 使用加密存储
self.client = AsyncClient(
self.homeserver,
self.user_id,
store_path=STORE_PATH
)
self.is_running = True
logger.info(f"Matrix nio 客户端已初始化,存储路径: {STORE_PATH}")
return True
finally:
db.close()
async def connect(self):
"""连接到Matrix服务器"""
if not self.homeserver or not self.username:
logger.warning("Matrix配置不完整跳过连接")
return False
try:
self.client = AsyncClient(self.homeserver, self.username)
response = await self.client.login(self.password)
if isinstance(response, LoginResponse):
logger.info(f"Matrix连接成功: {self.username}")
self.is_running = True
return True
else:
logger.error(f"Matrix登录失败: {response}")
return False
except Exception as e:
logger.error(f"Matrix连接错误: {e}")
return False
return False
async def start_sync(self, message_handler: Callable = None):
"""开始同步消息"""
if not self.client:
if not self.is_running or not self.client:
logger.warning("Matrix未连接")
return
self.on_message_callback = message_handler
# 注册消息处理器
self.client.add_event_callback(self._handle_room_message, RoomMessageText)
# 开始同步循环
await self.client.sync_forever(timeout=30000)
# 登录
try:
login_resp = await self.client.login(self.password)
logger.info(f"Matrix登录成功: {login_resp}")
# 加载加密存储
logger.info("加载加密存储...")
# 第一次同步获取房间密钥
sync_resp = await self.client.sync(full_state=True)
logger.info(f"初始同步完成: next_batch={sync_resp.next_batch}")
# 注册消息处理器 - 普通文本消息
self.client.add_event_callback(self._handle_nio_message, RoomMessageText)
# 注册加密消息处理器(解密后)
self.client.add_event_callback(self._handle_encrypted_message, MegolmEvent)
logger.info("Matrix消息回调已注册")
# 使用 sync_forever 自动处理事件(后台任务)
asyncio.create_task(self._run_sync_forever())
logger.info("Matrix nio sync_forever 任务已启动")
except Exception as e:
logger.error(f"Matrix启动失败: {e}")
import traceback
logger.error(traceback.format_exc())
async def _handle_room_message(self, room: MatrixRoom, event: RoomMessageText):
"""处理收到的房间消息"""
async def _run_sync_forever(self):
"""运行 sync_forever自动处理所有事件"""
try:
logger.info("开始 Matrix sync_forever...")
# 不使用 full_state只做增量同步
await self.client.sync_forever(timeout=30000)
except Exception as e:
logger.error(f"sync_forever 错误: {e}")
import traceback
logger.error(traceback.format_exc())
self.is_running = False
async def _handle_encrypted_message(self, room, event):
"""处理加密消息MegolmEvent"""
logger.info(f"收到加密消息: [{room.room_id}] event_id={event.event_id}")
# 检查是否已解密
logger.info(f"event.decrypted: {event.decrypted}")
try:
body = None
sender = event.sender
# 方法1: 检查 decrypted 属性
if event.decrypted:
logger.info(f"消息已解密: {event.decrypted}")
# 使用 parse_decrypted_event 获取解密后的完整事件
try:
decrypted_event = event.parse_decrypted_event(event.decrypted)
if decrypted_event:
logger.info(f"解密事件类型: {type(decrypted_event)}")
if hasattr(decrypted_event, 'body'):
body = decrypted_event.body
sender = decrypted_event.sender if hasattr(decrypted_event, 'sender') else event.sender
logger.info(f"从 decrypted 获取: {body}")
except Exception as e:
logger.warning(f"parse_decrypted_event 失败: {e}")
# 方法2: 从 source 获取(如果是自己发的消息)
if not body and hasattr(event, 'source') and event.source:
content = event.source.get('content', {})
# 检查是否有 plaintext body
if 'body' in content:
body = content.get('body', '')
sender = event.source.get('sender', event.sender)
logger.info(f"从source.content获取: {body[:50] if body else 'empty'}")
# 方法3: 直接尝试 event.body (某些情况下可能有)
if not body and hasattr(event, 'body') and event.body:
body = event.body
logger.info(f"从event.body获取: {body[:50]}")
if body:
logger.info(f"解密成功: sender={sender}, body={body[:50]}")
await self._process_message(room, sender, body, event.event_id)
else:
logger.warning(f"加密消息无法解密: event_id={event.event_id}")
logger.warning(f"event.decrypted={event.decrypted}, 需要密钥")
except Exception as e:
logger.error(f"处理加密消息失败: {e}")
import traceback
logger.error(traceback.format_exc())
async def _handle_nio_message(self, room, event):
"""处理 nio 收到的消息(普通文本)"""
# 忽略自己发送的消息
if event.sender == self.username:
sender = event.sender
if sender == self.user_id:
logger.debug(f"忽略自己发送的消息: {sender}")
return
message_text = event.body.strip()
logger.info(f"Matrix收到文本消息: [{room.room_id}] {sender}: {message_text}")
# 调用统一处理方法
await self._process_message(room, sender, message_text, event.event_id)
async def _process_message(self, room, sender, message_text, event_id):
"""处理消息的核心逻辑"""
# 检查是否已处理过此事件(防止重复处理)
if event_id in self.processed_events:
logger.debug(f"忽略已处理的事件: {event_id}")
return
# 标记为已处理
self.processed_events.add(event_id)
logger.info(f"处理消息: event_id={event_id}")
# 限制集合大小保留最近的100个
if len(self.processed_events) > 100:
# 清理旧的一半
self.processed_events = set(list(self.processed_events)[-50:])
# 忽略自己发送的消息
if sender == self.user_id:
logger.debug(f"忽略自己发送的消息: {sender}")
return
# 保存房间ID
self.last_room_id = room.room_id
db = SessionLocal()
try:
conv_service = ConversationService(db)
# 获取或创建Matrix用户
matrix_user_id = event.sender
user = conv_service.get_or_create_user(
user_id=matrix_user_id,
display_name=room.users.get(matrix_user_id, {}).get('display_name', matrix_user_id),
user_type='matrix',
matrix_user_id=matrix_user_id
# 使用固定主用户
main_user = conv_service.get_or_create_user(
MAIN_USER_ID,
display_name="主用户",
user_type='web'
)
# 获取或创建房间映射
mapping = db.query(MatrixRoomMapping).filter(
MatrixRoomMapping.room_id == room.room_id
).first()
# 处理 /new 命令
if message_text == "/new":
conversation = conv_service.create_conversation(main_user.id)
await self.send_message(room.room_id, "✅ 已创建新会话")
logger.info(f"Matrix创建新会话: {conversation.conversation_id}")
if self.on_message_callback:
await self.on_message_callback(
action="new_conversation",
conversation_id=conversation.conversation_id,
room_id=room.room_id
)
return
if not mapping:
# 为这个房间创建新会话
conversation = conv_service.create_conversation(user.id, title=f"Matrix: {room.display_name}")
mapping = MatrixRoomMapping(
room_id=room.room_id,
user_id=user.id,
conversation_id=conversation.id
)
db.add(mapping)
db.commit()
db.refresh(mapping)
# 获取最新会话
conversations = conv_service.get_user_conversations(main_user.id)
if not conversations:
conversation = conv_service.create_conversation(main_user.id)
else:
conversation = conversations[0]
# 保存用户消息
conv_service.add_message(
conversation_id=mapping.conversation_id,
user_msg = conv_service.add_message(
conversation_id=conversation.id,
role='user',
content=event.body,
content=message_text,
source='matrix',
extra_data={'event_id': event.event_id, 'room_id': room.room_id}
extra_data={
'event_id': event_id,
'room_id': room.room_id,
'sender': sender
}
)
# 调用外部消息处理器
# 立即通知网页端有新用户消息(通过回调)
if self.on_message_callback:
await self.on_message_callback(
conversation_id=mapping.conversation.conversation_id,
user_message=event.body,
user_id=user.user_id,
action="user_message",
conversation_id=conversation.conversation_id,
user_message=message_text,
room_id=room.room_id,
message_id=user_msg.id,
sender=sender
)
# 通知回调处理 AI 回复(不再在这里直接调用 AI
if self.on_message_callback:
await self.on_message_callback(
action="generate_ai_response",
conversation_id=conversation.conversation_id,
room_id=room.room_id
)
logger.info(f"Matrix回复已发送: {ai_response[:50]}")
except Exception as e:
logger.error(f"处理Matrix消息失败: {e}")
import traceback
logger.error(traceback.format_exc())
await self.send_message(room.room_id, f"处理消息时出错: {str(e)}")
try:
await self.client.room_typing(room.room_id, typing_state=False)
except:
pass
finally:
db.close()
async def send_message(self, room_id: str, message: str):
"""发送消息到Matrix房间"""
if not self.client:
logger.error("Matrix客户端未初始化")
return False
try:
# 先尝试直接发送
await self.client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
room_id,
"m.room.message",
{
"msgtype": "m.text",
"body": message
}
)
logger.info(f"Matrix消息发送成功: {room_id}")
return True
except Exception as e:
logger.error(f"发送Matrix消息失败: {e}")
return False
async def get_room_id_for_user(self, user_matrix_id: str) -> Optional[str]:
"""获取与用户的对话房间ID"""
db = SessionLocal()
try:
user = db.query(User).filter(User.matrix_user_id == user_matrix_id).first()
if user:
mapping = db.query(MatrixRoomMapping).filter(
MatrixRoomMapping.user_id == user.id
).first()
if mapping:
return mapping.room_id
return None
finally:
db.close()
error_str = str(e)
if "not verified" in error_str or "OlmUnverifiedDeviceError" in error_str:
logger.warning(f"设备未验证,尝试验证设备后发送: {e}")
try:
# 验证所有未验证的设备
for user_id, devices in self.client.device_store.items():
for device_id, device in devices.items():
if not device.verified:
logger.info(f"验证设备: {user_id} {device_id}")
self.client.verify_device(device)
# 再次尝试发送
await self.client.room_send(
room_id,
"m.room.message",
{
"msgtype": "m.text",
"body": message
}
)
logger.info(f"Matrix消息发送成功验证设备后: {room_id}")
return True
except Exception as e2:
logger.error(f"验证设备后发送仍失败: {e2}")
# 最后尝试用 HTTP API 发送(不加密)
try:
import httpx
async with httpx.AsyncClient() as http_client:
txn_id = f"m{int(asyncio.get_event_loop().time() * 1000)}"
resp = await http_client.put(
f"{self.homeserver}/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txn_id}",
headers={"Authorization": f"Bearer {self.client.access_token}"},
json={"msgtype": "m.text", "body": message}
)
if resp.status_code == 200:
logger.info(f"Matrix消息通过HTTP发送成功: {room_id}")
return True
else:
logger.error(f"HTTP发送失败: {resp.status_code}")
return False
except Exception as e3:
logger.error(f"HTTP发送失败: {e3}")
return False
else:
logger.error(f"发送Matrix消息错误: {e}")
return False
async def disconnect(self):
"""断开连接"""
self.is_running = False
if self.client:
await self.client.logout()
await self.client.close()
self.is_running = False
# 全局实例

View File

@@ -341,20 +341,39 @@
</div>
<script>
// 全局变量
// 全局变量 - 使用固定主用户与Matrix AI用户相同
let ws = null;
let userId = 'web_' + Math.random().toString(36).substr(2, 9);
let userId = 'main_user'; // 固定主用户ID
let currentConversationId = null;
let conversations = [];
// 初始化
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('userIdDisplay').textContent = `用户: ${userId}`;
document.getElementById('userIdDisplay').textContent = '主用户模式';
connectWebSocket();
loadConversations();
setupTextarea();
// 监听最新会话更新
setInterval(checkLatestConversation, 2000);
});
// 检查最新会话用于Matrix同步
async function checkLatestConversation() {
try {
const response = await fetch('/api/conversations/latest');
if (response.ok) {
const data = await response.json();
if (data.conversation && data.conversation.id !== currentConversationId) {
// 有新会话,自动切换
selectConversation(data.conversation.id);
}
}
} catch (error) {
// 忽略错误
}
}
// WebSocket连接
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -391,8 +410,27 @@
loadConversations();
break;
case 'new_conversation':
// Matrix创建了新会话自动切换
currentConversationId = data.conversation_id;
loadConversations();
// 清空消息区域
const container = document.getElementById('messagesContainer');
container.innerHTML = `
<div class="welcome">
<h2>👋 开始新对话</h2>
<p>输入您的问题开始对话</p>
</div>
`;
break;
case 'user_message':
appendMessage('user', data.message.content, data.message.source);
// 如果消息来自Matrix切换到对应会话
if (data.message.source === 'matrix' && data.conversation_id) {
currentConversationId = data.conversation_id;
renderConversations();
}
break;
case 'assistant_message':
@@ -429,11 +467,10 @@
messageDiv.className = `message ${role}`;
const avatar = role === 'user' ? '👤' : '🤖';
const sourceLabel = source === 'matrix' ? ' [Matrix]' : '';
messageDiv.innerHTML = `
<div class="message-avatar">${avatar}</div>
<div class="message-content">${formatContent(content)}${sourceLabel}</div>
<div class="message-content">${formatContent(content)}</div>
`;
container.appendChild(messageDiv);
@@ -451,10 +488,15 @@
// 加载会话列表
async function loadConversations() {
try {
const response = await fetch('/api/conversations?user_id=' + userId);
const response = await fetch('/api/conversations');
const data = await response.json();
conversations = data.conversations;
renderConversations();
// 如果没有当前会话且有会话列表,自动选择最新的
if (!currentConversationId && conversations.length > 0) {
selectConversation(conversations[0].id);
}
} catch (error) {
console.error('加载会话失败:', error);
}
@@ -496,7 +538,7 @@
// 创建新会话
async function createNewConversation() {
try {
const response = await fetch('/api/conversations?user_id=' + userId, {
const response = await fetch('/api/conversations', {
method: 'POST'
});
const data = await response.json();