excel识别
This commit is contained in:
@@ -135,21 +135,37 @@ class WecomAiCallback(http.Controller):
|
||||
db_name = request.env.cr.dbname
|
||||
threading.Thread(
|
||||
target=self._async_process_ai_message,
|
||||
args=(db_name, wecom_userid, "请帮我看看这张图片里有什么", media_id),
|
||||
args=(db_name, wecom_userid, "请帮我看看这张图片里有什么", media_id, 'image'),
|
||||
daemon=True,
|
||||
).start()
|
||||
return Response("", content_type='text/plain')
|
||||
|
||||
# 文件消息(Excel/CSV):下载 → 解析 → 交给 AI
|
||||
if msg_type == 'file':
|
||||
media_id = msg.get('media_id', '')
|
||||
if not wecom_userid or not media_id:
|
||||
return Response("", content_type='text/plain')
|
||||
|
||||
_logger.info("收到企微文件消息: user=%s, media_id=%s", wecom_userid, media_id[:20])
|
||||
db_name = request.env.cr.dbname
|
||||
threading.Thread(
|
||||
target=self._async_process_ai_message,
|
||||
args=(db_name, wecom_userid, "请帮我分析这个文件的内容", media_id, 'file'),
|
||||
daemon=True,
|
||||
).start()
|
||||
return Response("", content_type='text/plain')
|
||||
|
||||
# 其他类型暂不处理
|
||||
_logger.info("AI回调收到非文本/图片消息类型: %s,暂不处理", msg_type)
|
||||
_logger.info("AI回调收到非文本/图片/文件消息类型: %s,暂不处理", msg_type)
|
||||
return Response("", content_type='text/plain')
|
||||
|
||||
def _async_process_ai_message(self, db_name, wecom_userid, content, media_id=None):
|
||||
def _async_process_ai_message(self, db_name, wecom_userid, content, media_id=None, media_type='image'):
|
||||
"""
|
||||
后台线程: 处理 AI 对话并推送回复
|
||||
使用独立的数据库游标和环境
|
||||
|
||||
:param media_id: 可选,图片消息的 media_id。不为空时先下载→OCR→拼入消息
|
||||
:param media_id: 可选,图片/文件消息的 media_id
|
||||
:param media_type: 'image' | 'file'
|
||||
"""
|
||||
try:
|
||||
registry = api.Registry(db_name)
|
||||
@@ -158,11 +174,19 @@ class WecomAiCallback(http.Controller):
|
||||
|
||||
attachment_ids = None
|
||||
if media_id:
|
||||
attachment_ids = self._download_and_ocr_media(env, wecom_userid, media_id)
|
||||
if media_type == 'file':
|
||||
attachment_ids = self._download_and_parse_file(env, wecom_userid, media_id)
|
||||
else:
|
||||
attachment_ids = self._download_and_ocr_media(env, wecom_userid, media_id)
|
||||
|
||||
if not attachment_ids:
|
||||
# 下载/OCR失败,给用户一个提示,不再调AI
|
||||
env['wecom.ai.chat']._send_wecom_reply(
|
||||
wecom_userid, "图片已收到,但未能从中提取到文字信息。请尝试发送更清晰的图片(含印刷文字)。")
|
||||
# 下载/解析失败,给用户一个提示,不再调AI
|
||||
if media_type == 'file':
|
||||
env['wecom.ai.chat']._send_wecom_reply(
|
||||
wecom_userid, "文件已收到,但解析失败。请确保发送的是 .xlsx/.xls/.csv 格式的 Excel 文件。")
|
||||
else:
|
||||
env['wecom.ai.chat']._send_wecom_reply(
|
||||
wecom_userid, "图片已收到,但未能从中提取到文字信息。请尝试发送更清晰的图片(含印刷文字)。")
|
||||
cr.commit()
|
||||
return
|
||||
|
||||
@@ -245,3 +269,87 @@ class WecomAiCallback(http.Controller):
|
||||
except Exception as e:
|
||||
_logger.exception('[WECOM-OCR] 下载/OCR 异常: %s', e)
|
||||
return None
|
||||
|
||||
def _download_and_parse_file(self, env, wecom_userid, media_id):
|
||||
"""下载企微文件 → 创建 documents.document → Excel解析 → 返回 [document_id]
|
||||
|
||||
支持 .xlsx / .xls / .csv
|
||||
:return: [int] 或 None
|
||||
"""
|
||||
try:
|
||||
# 1. 下载文件
|
||||
content, error = env['wecom.apps'].sudo().download_agent_media(media_id)
|
||||
if content is None:
|
||||
_logger.error('[WECOM-FILE] 下载失败: %s', error)
|
||||
return None
|
||||
|
||||
_logger.info('[WECOM-FILE] 下载成功: %d bytes', len(content))
|
||||
|
||||
# 2. 确定 mimetype(根据魔数)
|
||||
mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
ext = '.xlsx'
|
||||
if content[:2] == b'PK':
|
||||
mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
ext = '.xlsx'
|
||||
elif content[:2] == b'\xd0\xcf':
|
||||
mimetype = 'application/vnd.ms-excel'
|
||||
ext = '.xls'
|
||||
else:
|
||||
# 尝试按文本解码 → CSV
|
||||
try:
|
||||
_ = content.decode('utf-8-sig')
|
||||
mimetype = 'text/csv'
|
||||
ext = '.csv'
|
||||
except:
|
||||
_logger.warning('[WECOM-FILE] 无法识别文件类型,降级为 xlsx')
|
||||
filename = f'wecom_file_{media_id[:8]}{ext}'
|
||||
|
||||
# 3. 创建 documents.document
|
||||
DocFolder = env['documents.folder'].sudo()
|
||||
folder = DocFolder.search([('name', '=', 'AI 聊天附件')], limit=1)
|
||||
if not folder:
|
||||
folder = DocFolder.create({'name': 'AI 聊天附件'})
|
||||
|
||||
document = env['documents.document'].sudo().create({
|
||||
'name': filename,
|
||||
'folder_id': folder.id,
|
||||
'mimetype': mimetype,
|
||||
'raw': content,
|
||||
})
|
||||
|
||||
_logger.info('[WECOM-FILE] 文档已创建: id=%s', document.id)
|
||||
|
||||
# 4. Excel 解析
|
||||
try:
|
||||
from odoo.addons.yuthon_ai_agent.utils.excel_parser import parse_excel_to_markdown
|
||||
except ImportError:
|
||||
_logger.warning('[WECOM-FILE] 无法导入 Excel 解析器')
|
||||
return [document.id]
|
||||
|
||||
markdown_text, meta = parse_excel_to_markdown(content, filename)
|
||||
if meta.get('error'):
|
||||
_logger.error('[WECOM-FILE] 解析失败: %s', meta['error'])
|
||||
return [document.id]
|
||||
|
||||
document.write({'ai_ocr_content': markdown_text})
|
||||
_logger.info('[WECOM-FILE] ✅ 解析成功: %d字符 | sheets=%s | rows=%s',
|
||||
len(markdown_text), meta.get('sheets'), meta.get('total_rows'))
|
||||
|
||||
# 5. 创建导入记录
|
||||
try:
|
||||
env['ai.import.record'].sudo().create({
|
||||
'document_id': document.id,
|
||||
'filename': filename,
|
||||
'file_type': 'excel',
|
||||
'user_id': env.ref('base.user_root').id,
|
||||
'wecom_userid': wecom_userid,
|
||||
'ocr_status': 'success' if not meta.get('truncated') else 'success',
|
||||
'ocr_content': markdown_text,
|
||||
})
|
||||
except Exception as e:
|
||||
_logger.warning('[WECOM-FILE] 创建导入记录失败: %s', e)
|
||||
|
||||
return [document.id]
|
||||
except Exception as e:
|
||||
_logger.exception('[WECOM-FILE] 下载/解析异常: %s', e)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user