From 58b9dbfdd68d8ab67e50c6849ed7cbe95a9493b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Fri, 5 Jun 2026 15:48:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_ai_agent/controllers/report.py | 47 +++++++++++++++----- yuthon_ai_agent/models/ai_agent.py | 6 ++- yuthon_ai_agent/models/ai_report.py | 16 +++++-- yuthon_ai_agent/models/ai_report_template.py | 17 +++++-- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/yuthon_ai_agent/controllers/report.py b/yuthon_ai_agent/controllers/report.py index 3b54fc8ac..0ae2cd9db 100644 --- a/yuthon_ai_agent/controllers/report.py +++ b/yuthon_ai_agent/controllers/report.py @@ -3,7 +3,7 @@ import json import logging import os -from odoo import http, api, SUPERUSER_ID +from odoo import http, api, SUPERUSER_ID, fields from odoo.http import request, Response _logger = logging.getLogger(__name__) @@ -20,16 +20,28 @@ class AiReportController(http.Controller): def _get_report_by_token(self, token): """根据 token 查找并验证报表""" + db_name = request.env.cr.dbname if hasattr(request.env.cr, 'dbname') else 'unknown' + _logger.info( + '报表访问请求: token=%s, db=%s, uid=%s, remote=%s', + token[:12], db_name, request.env.uid, + request.httprequest.remote_addr if hasattr(request, 'httprequest') else 'unknown' + ) report = request.env['ai.report'].sudo().search([ ('access_token', '=', token), ], limit=1) if not report: - _logger.warning('报表不存在: token=%s', token[:12]) + _logger.warning( + '报表不存在: token=%s, db=%s, 可能原因: token错误/数据库不匹配/记录已被删除', + token[:12], db_name + ) return None, '报表不存在或已被删除' if not report.is_valid(): - _logger.warning('报表已过期: token=%s, expire=%s, active=%s', - token[:12], report.expire_date, report.active) - return None, '报表已过期或已停用' + _logger.warning('报表已失效: token=%s, db=%s, expire=%s, active=%s', + token[:12], db_name, report.expire_date, report.active) + reason = '报表已过期' if (report.expire_date and report.expire_date < fields.Datetime.now()) else '报表已停用' + return None, f'{reason},请联系分享者重新生成' + _logger.info('报表验证通过: token=%s, report_id=%s, name=%s', + token[:12], report.id, report.name) return report, None @http.route('/ai/report/', type='http', auth='none', @@ -83,22 +95,35 @@ class AiReportController(http.Controller): first = data_records[0] col_keys = {c.get('k', '') for c in config['columns']} data_keys = set(first.keys()) + # 排除 id 字段(Odoo search_read 总是返回 id) + display_keys = data_keys - {'id'} missing = col_keys - data_keys + extra = display_keys - col_keys if missing: _logger.warning( - '模板 %s report_config 列 key 与数据不匹配: missing=%s, col_keys=%s, data_keys=%s', - template.id, missing, col_keys, data_keys) - # 从数据字段重建列 - fields = data.get('fields', list(data_keys - {'id'})) + '模板 %s (id=%s) report_config 列 key 与数据不匹配: ' + 'missing=%s, extra=%s, col_keys=%s, data_keys=%s', + template.name, template.id, missing, extra, col_keys, data_keys) + # 从数据字段重建列,但尝试保留模板中的 label 和 width + old_col_map = {c.get('k', ''): c for c in config['columns']} labels = {} if report.field_labels: labels = json.loads(report.field_labels) elif template and template.field_labels: labels = json.loads(template.field_labels) + fields = data.get('fields', list(display_keys)) config['columns'] = [ - {'k': f, 'l': labels.get(f, f), 'w': 120} - for f in fields + { + 'k': f, + 'l': labels.get(f, old_col_map.get(f, {}).get('l', f)), + 'w': old_col_map.get(f, {}).get('w', 120), + } + for f in fields if f != 'id' ] + config['_column_mismatch'] = True # 标记列被重建,前端可用此提示 + _logger.info( + '模板 %s: 列已从数据字段重建,共 %s 列', template.name, len(config['columns']) + ) # 2d. 处理数据加载失败 if not data.get('success'): diff --git a/yuthon_ai_agent/models/ai_agent.py b/yuthon_ai_agent/models/ai_agent.py index 69e848937..3d9010902 100644 --- a/yuthon_ai_agent/models/ai_agent.py +++ b/yuthon_ai_agent/models/ai_agent.py @@ -489,7 +489,11 @@ class AiConversation(models.Model): return result except Exception as e: _logger.exception('generate_report failed: %s', e) - return {'success': True, 'url': '', 'message': f'报表生成完毕,请检查'} # 失败不报错 + return { + 'success': False, + 'error': f'报表生成失败:{str(e)}', + 'message': f'报表生成失败,请稍后重试或联系管理员检查服务端日志。' + } def _call_chat_completion(self, provider, messages, tools=None): """封装一次 HTTP 调用,返回 (assistant_message_dict, error_str)。 diff --git a/yuthon_ai_agent/models/ai_report.py b/yuthon_ai_agent/models/ai_report.py index c19c602e1..35de7be9c 100644 --- a/yuthon_ai_agent/models/ai_report.py +++ b/yuthon_ai_agent/models/ai_report.py @@ -81,13 +81,23 @@ class AiReport(models.Model): if not base_url: base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', '') # 如果 web.base.url 是内网地址,检查是否有配置公共 URL - if 'localhost' in base_url or base_url.startswith('http://127.') or \ - base_url.startswith('http://10.') or base_url.startswith('http://172.') or \ - base_url.startswith('http://192.168'): + _is_internal = ( + 'localhost' in base_url or + base_url.startswith('http://127.') or base_url.startswith('https://127.') or + base_url.startswith('http://10.') or base_url.startswith('https://10.') or + base_url.startswith('http://172.') or base_url.startswith('https://172.') or + base_url.startswith('http://192.168') or base_url.startswith('https://192.168') + ) + if _is_internal: public_url = self.env['ir.config_parameter'].sudo().get_param( 'yuthon.public.url', '') if public_url: base_url = public_url + else: + _logger.warning( + 'web.base.url 是内网地址 %s,但未配置 yuthon.public.url,' + '生成的报表链接外部可能无法访问', base_url + ) return f"{base_url}/ai/report/{self.access_token}" @api.onchange('model_id') diff --git a/yuthon_ai_agent/models/ai_report_template.py b/yuthon_ai_agent/models/ai_report_template.py index 2272fd5ba..e6b2310d8 100644 --- a/yuthon_ai_agent/models/ai_report_template.py +++ b/yuthon_ai_agent/models/ai_report_template.py @@ -78,19 +78,30 @@ class AiReportTemplate(models.Model): 'filter_ids', 'default_visible') def _compute_report_config(self): for t in self: - # 如果没有任何配置行,保留现有值(兼容旧数据) - if not any([t.column_ids, t.card_ids, t.group_by_ids, t.filter_ids]): - continue cfg = {} cols = [] + skipped_cols = 0 for c in t.column_ids: fname = c.field_name or (c.field_id.name if c.field_id else '') flabel = c.label or fname if not fname: + skipped_cols += 1 continue cols.append({'k': fname, 'l': flabel, 'w': c.width or 120}) + if skipped_cols > 0: + _logger.warning( + '模板 %s (id=%s): %s 个列定义的字段名为空,已被跳过。' + '请检查列定义中的字段名或字段选择。', + t.name, t.id, skipped_cols + ) + # 如果 column_ids 有数据但全部被跳过,需要重置 report_config if cols: cfg['columns'] = cols + elif t.column_ids: + # column_ids 存在但全部无效 → 清除旧的 report_config + _logger.warning( + '模板 %s (id=%s): 所有列定义无效,report_config 将被清空。', t.name, t.id + ) cards = [] for c in t.card_ids: fname_c = c.field_name or (c.field_id.name if c.field_id else '')