feat: AI对话系统 v1.0.0 - 网页端和Matrix端实时同步

This commit is contained in:
2026-04-11 11:51:54 +08:00
commit 46216205fe
26 changed files with 2110 additions and 0 deletions

9
services/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
from .ai_service import ai_service, AIService
from .conversation_service import ConversationService
from .matrix_service import matrix_bot, MatrixBot
__all__ = [
'ai_service', 'AIService',
'ConversationService',
'matrix_bot', 'MatrixBot'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

80
services/ai_service.py Normal file
View File

@@ -0,0 +1,80 @@
"""
AI服务 - 调用大模型API
"""
import httpx
from typing import List, Dict, Optional
import json
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"):
self.api_base = api_base
self.api_key = api_key
self.model = model
async def chat(self, messages: List[Dict], stream: bool = False) -> str:
"""
调用AI模型进行对话
Args:
messages: 对话历史 [{"role": "user", "content": "..."}]
stream: 是否流式输出
Returns:
AI回复内容
"""
url = f"{self.api_base}/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
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']
async def chat_stream(self, messages: List[Dict]):
"""
流式调用AI模型
"""
url = f"{self.api_base}/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": self.model,
"messages": messages,
"stream": True,
"temperature": 0.7,
"max_tokens": 2000
}
async with httpx.AsyncClient(timeout=60.0) as client:
async with client.stream("POST", url, headers=headers, json=payload) as response:
async for line in response.aiter_lines():
if line.startswith("data: "):
data_str = line[6:]
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
if 'choices' in data and len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
if 'content' in delta:
yield delta['content']
except json.JSONDecodeError:
continue
# 全局实例
ai_service = AIService()

View File

@@ -0,0 +1,111 @@
"""
会话管理服务
"""
from typing import List, Optional
from datetime import datetime
from sqlalchemy.orm import Session
import uuid
from models import User, Conversation, Message
class ConversationService:
def __init__(self, db: Session):
self.db = db
def get_or_create_user(self, user_id: str, display_name: str = None, user_type: str = 'web', matrix_user_id: str = None) -> User:
"""获取或创建用户"""
user = self.db.query(User).filter(User.user_id == user_id).first()
if not user:
user = User(
user_id=user_id,
display_name=display_name or user_id,
user_type=user_type,
matrix_user_id=matrix_user_id
)
self.db.add(user)
self.db.commit()
self.db.refresh(user)
else:
user.last_active_at = datetime.utcnow()
self.db.commit()
return user
def create_conversation(self, user_id: int, title: str = None) -> Conversation:
"""创建新会话"""
conversation_id = f"conv_{uuid.uuid4().hex[:12]}"
conversation = Conversation(
conversation_id=conversation_id,
user_id=user_id,
title=title
)
self.db.add(conversation)
self.db.commit()
self.db.refresh(conversation)
return conversation
def get_conversation(self, conversation_id: str) -> Optional[Conversation]:
"""获取会话"""
return self.db.query(Conversation).filter(
Conversation.conversation_id == conversation_id
).first()
def get_user_conversations(self, user_id: int) -> List[Conversation]:
"""获取用户的所有会话"""
return self.db.query(Conversation).filter(
Conversation.user_id == user_id,
Conversation.is_active == True
).order_by(Conversation.updated_at.desc()).all()
def add_message(self, conversation_id: int, role: str, content: str, source: str = 'web', extra_data: dict = None) -> Message:
"""添加消息"""
message = Message(
conversation_id=conversation_id,
role=role,
content=content,
source=source,
extra_data=extra_data
)
self.db.add(message)
# 更新会话时间
conversation = self.db.query(Conversation).filter(Conversation.id == conversation_id).first()
if conversation:
conversation.updated_at = datetime.utcnow()
# 如果没有标题,用第一条用户消息作为标题
if not conversation.title and role == 'user':
conversation.title = content[:50] + ('...' if len(content) > 50 else '')
self.db.commit()
self.db.refresh(message)
return message
def get_messages(self, conversation_id: int, limit: int = 50) -> List[Message]:
"""获取会话消息"""
return self.db.query(Message).filter(
Message.conversation_id == conversation_id
).order_by(Message.created_at.asc()).limit(limit).all()
def get_conversation_history(self, conversation_id: str, limit: int = 20) -> List[dict]:
"""获取会话历史用于AI上下文"""
conversation = self.get_conversation(conversation_id)
if not conversation:
return []
messages = self.db.query(Message).filter(
Message.conversation_id == conversation.id
).order_by(Message.created_at.desc()).limit(limit).all()
# 反转顺序,最早的在前
messages.reverse()
return [{"role": m.role, "content": m.content} for m in messages]
def delete_conversation(self, conversation_id: str):
"""删除会话(软删除)"""
conversation = self.get_conversation(conversation_id)
if conversation:
conversation.is_active = False
self.db.commit()
return True
return False

172
services/matrix_service.py Normal file
View File

@@ -0,0 +1,172 @@
"""
Matrix Bot 服务 - 处理Matrix消息收发
"""
import asyncio
import json
from typing import Optional, Callable
from nio import AsyncClient, MatrixRoom, RoomMessageText, LoginResponse, SyncResponse
import logging
from models import SessionLocal, User, Conversation, Message, MatrixRoomMapping, SystemConfig
from services.conversation_service import ConversationService
logger = logging.getLogger(__name__)
class MatrixBot:
def __init__(self):
self.client: Optional[AsyncClient] = None
self.homeserver: str = ""
self.username: str = ""
self.password: str = ""
self.is_running: bool = False
self.on_message_callback: Optional[Callable] = None
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', '')
if self.username and self.password:
await self.connect()
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
async def start_sync(self, message_handler: Callable = None):
"""开始同步消息"""
if not self.client:
return
self.on_message_callback = message_handler
# 注册消息处理器
self.client.add_event_callback(self._handle_room_message, RoomMessageText)
# 开始同步循环
await self.client.sync_forever(timeout=30000)
async def _handle_room_message(self, room: MatrixRoom, event: RoomMessageText):
"""处理收到的房间消息"""
# 忽略自己发送的消息
if event.sender == self.username:
return
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
)
# 获取或创建房间映射
mapping = db.query(MatrixRoomMapping).filter(
MatrixRoomMapping.room_id == room.room_id
).first()
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)
# 保存用户消息
conv_service.add_message(
conversation_id=mapping.conversation_id,
role='user',
content=event.body,
source='matrix',
extra_data={'event_id': event.event_id, 'room_id': room.room_id}
)
# 调用外部消息处理器
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,
room_id=room.room_id
)
finally:
db.close()
async def send_message(self, room_id: str, message: str):
"""发送消息到Matrix房间"""
if not self.client:
return False
try:
await self.client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": message
}
)
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()
async def disconnect(self):
"""断开连接"""
if self.client:
await self.client.logout()
await self.client.close()
self.is_running = False
# 全局实例
matrix_bot = MatrixBot()