Files
gzth/yuthon_ai_agent/controllers/main.py
T
2026-06-12 14:47:38 +08:00

241 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
import json
import base64
from odoo import http
from odoo.http import request
from odoo.tools import format_datetime
class AiChatController(http.Controller):
"""AI 聊天 API"""
def _check_ai_access(self):
"""检查用户是否有 AI Agent 使用权限"""
if not request.env.user.has_group('yuthon_ai_agent.group_ai_agent_user'):
return {'error': '您没有使用 AI 助手的权限,请联系管理员开通。'}
return None
@http.route('/ai/chat/send', type='json', auth='user', methods=['POST'], csrf=False)
def chat_send(self, conversation_id, message, attachment_ids=None):
"""发送消息(支持附件 OCR)"""
err = self._check_ai_access()
if err:
return err
conv = request.env['ai.conversation'].browse(conversation_id)
if not conv.exists():
return {'error': '对话不存在'}
reply = conv.send_message(message, attachment_ids=attachment_ids)
if isinstance(reply, dict) and 'error' in reply:
return reply
last_msg = conv.message_ids.filtered(lambda m: m.role == 'assistant')[-1:]
return {
'content': reply,
'message_id': last_msg.id if last_msg else False,
}
@http.route('/ai/chat/history', type='json', auth='user')
def chat_history(self, conversation_id):
"""获取对话历史"""
conv = request.env['ai.conversation'].browse(conversation_id)
if not conv.exists():
return {'error': '对话不存在'}
user_tz = request.env.user.tz or 'Asia/Shanghai'
from pytz import timezone, utc
local_tz = timezone(user_tz)
messages = []
for msg in conv.message_ids:
if msg.create_date:
utc_dt = msg.create_date.replace(tzinfo=utc)
local_dt = utc_dt.astimezone(local_tz)
time_str = local_dt.strftime('%H:%M')
else:
time_str = ''
messages.append({
'id': msg.id,
'role': msg.role,
'content': msg.content,
'date': time_str,
})
return {'messages': messages}
@http.route('/ai/chat/clear', type='json', auth='user', methods=['POST'], csrf=False)
def chat_clear(self, conversation_id):
"""清空对话"""
conv = request.env['ai.conversation'].browse(conversation_id)
if not conv.exists():
return {'error': '对话不存在'}
conv.message_ids.unlink()
return {'success': True}
@http.route('/ai/conversation/create', type='json', auth='user', methods=['POST'], csrf=False)
def conversation_create(self):
"""创建新对话并返回ID"""
provider = request.env['ai.provider'].search([('active', '=', True)], limit=1)
if not provider:
return {'error': '请先在服务商配置中设置 AI 服务'}
conv = request.env['ai.conversation'].create({
'name': '新对话',
'provider_id': provider.id,
})
return {'id': conv.id}
@http.route('/ai/conversation/get_or_create_personal', type='json', auth='user')
def get_or_create_personal(self):
"""获取或创建当前用户的个人对话(浮动聊天用)"""
err = self._check_ai_access()
if err:
return err
conv = request.env['ai.conversation'].search([
('create_uid', '=', request.env.uid),
], limit=1, order='create_date desc')
if not conv:
provider = request.env['ai.provider'].search([('active', '=', True)], limit=1)
if not provider:
return {'error': '请先在服务商配置中设置 AI 服务'}
conv = request.env['ai.conversation'].create({
'name': '个人对话',
'provider_id': provider.id,
})
return {'id': conv.id, 'name': conv.name}
@http.route('/ai/conversation/list', type='json', auth='user')
def conversation_list(self):
"""获取当前用户的所有对话列表"""
err = self._check_ai_access()
if err:
return err
convs = request.env['ai.conversation'].search([
('create_uid', '=', request.env.uid),
], order='create_date desc', limit=50)
return {
'conversations': [{
'id': c.id,
'name': c.name,
'message_count': c.message_count,
} for c in convs]
}
@http.route('/ai/conversation/new', type='json', auth='user', methods=['POST'], csrf=False)
def conversation_new(self):
"""新建对话"""
err = self._check_ai_access()
if err:
return err
provider = request.env['ai.provider'].search([('active', '=', True)], limit=1)
if not provider:
return {'error': '请先在服务商配置中设置 AI 服务'}
conv = request.env['ai.conversation'].create({
'name': '新对话',
'provider_id': provider.id,
})
return {'id': conv.id, 'name': conv.name}
@http.route('/ai/conversation/delete', type='json', auth='user', methods=['POST'], csrf=False)
def conversation_delete(self, conversation_id):
"""删除对话"""
err = self._check_ai_access()
if err:
return err
conv = request.env['ai.conversation'].browse(conversation_id)
if not conv.exists():
return {'error': '对话不存在'}
if conv.create_uid.id != request.env.uid:
return {'error': '只能删除自己的对话'}
conv.unlink()
return {'success': True}
@http.route('/ai/profile/get', type='json', auth='user')
def get_profile(self):
"""获取当前用户的 AI 画像(含记忆摘要)"""
err = self._check_ai_access()
if err:
return err
profile = request.env['ai.user.profile'].get_or_create()
return {
'id': profile.id,
'nickname': profile.nickname or '',
'role': profile.role or '',
'department': profile.department or '',
'style_preference': profile.style_preference or 'concise',
'memory_count': profile.memory_count,
}
@http.route('/ai/attachment/upload', type='http', auth='user', methods=['POST'], csrf=False)
def attachment_upload(self, **kwargs):
"""上传聊天附件(图片/PDF),调 OCR 提取文字并返回结果。
请求:multipart/form-data,字段名 'file'
返回:JSON { success, attachment_id, filename, mimetype, ocr_text, error }
"""
err = self._check_ai_access()
if err:
return request.make_json_response(err)
uploaded = request.httprequest.files.get('file')
if not uploaded:
return request.make_json_response({'success': False, 'error': '未收到文件'})
# 读取文件内容
raw_bytes = uploaded.read()
filename = uploaded.filename or 'untitled'
mimetype = uploaded.content_type or 'application/octet-stream'
# 限制文件类型
allowed = {'image/png', 'image/jpeg', 'image/jpg', 'image/bmp',
'image/gif', 'application/pdf'}
if mimetype not in allowed:
return request.make_json_response({
'success': False,
'error': f'不支持的文件格式:{mimetype},仅支持 PNG/JPG/BMP/GIF/PDF'
})
# 限制文件大小 (10MB)
max_size = 10 * 1024 * 1024
if len(raw_bytes) > max_size:
return request.make_json_response({
'success': False,
'error': f'文件过大({len(raw_bytes) / 1024 / 1024:.1f}MB),上限 10MB'
})
# 获取当前对话的 Provider OCR 配置
conv_id = kwargs.get('conversation_id')
provider = None
if conv_id:
conv = request.env['ai.conversation'].sudo().browse(int(conv_id))
if conv.exists():
provider = conv.provider_id
# 创建 ir.attachment
attachment = request.env['ir.attachment'].sudo().create({
'name': filename,
'datas': base64.b64encode(raw_bytes).decode('utf-8'),
'mimetype': mimetype,
'res_model': 'ai.message',
'res_id': 0,
})
# OCR 提取文字
ocr_text = ''
if provider and provider.ocr_enabled:
from ..models.ocr_provider import get_ocr_instance
ocr = get_ocr_instance(provider)
if ocr:
result = ocr.smart_recognize(raw_bytes, mimetype)
if result.get('success'):
ocr_text = result.get('text') or ''
attachment.sudo().write({'ai_ocr_content': ocr_text})
else:
# OCR 失败不阻塞上传,前端仍可发送(只是没有文字提取)
pass
return request.make_json_response({
'success': True,
'attachment_id': attachment.id,
'filename': filename,
'mimetype': mimetype,
'ocr_text': ocr_text,
})