!11931 Merge remote-tracking branch 'origin/dev' into dev

Merge pull request !11931 from 李鹏宇/dev
This commit is contained in:
李鹏宇
2026-06-05 11:00:10 +00:00
committed by Gitee
5 changed files with 212 additions and 10 deletions
+1
View File
@@ -30,6 +30,7 @@
'data/demo_rule.xml',
'data/demo_manual.xml',
'data/demo_report_template.xml',
'data/cron_pinned_reports.xml',
],
'assets': {
'web.assets_backend': [
+60 -9
View File
@@ -286,6 +286,28 @@ class AiConversation(models.Model):
},
},
},
{
'type': 'function',
'function': {
'name': 'get_report_link',
'description': (
'获取报表模板对应的固定报表链接(不需要重新生成)。\n'
'当用户需要某个常规报表时,优先使用此工具而不是 generate_report。\n'
'如果模板有固定链接(后台已预生成),直接返回链接;'
'如果没有固定链接,返回 null 让你知道需要调用 generate_report。'
),
'parameters': {
'type': 'object',
'properties': {
'template_id': {
'type': 'integer',
'description': '报表模板ID,从"可用报表模板"列表中获取',
},
},
'required': ['template_id'],
},
},
},
]
if self.provider_id.allow_write:
tools += [
@@ -370,6 +392,10 @@ class AiConversation(models.Model):
if name == 'generate_report':
return self._execute_generate_report(args)
# get_report_link:获取模板的固定报表链接(无需重新生成)
if name == 'get_report_link':
return self._execute_get_report_link(args)
model_name = args.get('model')
if not model_name or model_name not in self.env:
return {'error': f'未知模型:{model_name}'}
@@ -521,6 +547,25 @@ class AiConversation(models.Model):
'message': f'报表生成失败,请稍后重试或联系管理员检查服务端日志。'
}
def _execute_get_report_link(self, args):
"""获取模板的固定报表链接(不重新生成)"""
template_id = args.get('template_id')
if not template_id:
return {'success': False, 'message': '请提供 template_id'}
template = self.env['ai.report.template'].sudo().browse(template_id)
if not template.exists():
return {'success': False, 'message': f'模板 {template_id} 不存在'}
if template.fixed_report_id and template.fixed_report_id.active:
return {
'success': True,
'url': template.fixed_report_url,
'template_name': template.name,
'message': f'已获取"{template.name}"的固定报表链接',
}
return {'success': False, 'message': f'模板"{template.name}"尚未生成固定链接,请使用 generate_report 临时生成'}
def _call_chat_completion(self, provider, messages, tools=None, retry=2):
"""封装一次 HTTP 调用,返回 (assistant_message_dict, error_str)。
同时记录 Token 使用量。支持自动重试瞬态网络错误。
@@ -710,19 +755,24 @@ class AiConversation(models.Model):
'少量数据(≤5条)直接用编号列表展示,如:\n'
'1. 张三 / 研发部 / 2025-03-01\n'
'2. 李四 / 财务部 / 2025-03-15\n'
'【报表与推送】当查询结果超过5条,或用户要求"列表/报表/明细"时,'
'必须调用 generate_report 工具生成报表链接。\n'
'调用方式:用 odoo_search_read 查到数据后,将返回的 records 数组直接传入 '
'generate_report 的 records 参数,只需再填 name 即可。'
'示例:generate_report({"name":"花名册", "records":<上一步search_read的records>})\n'
'【报表与推送】当用户要求查看列表/报表/明细时,'
'优先调用 get_report_link 工具:\n'
' 1) 从"可用报表模板"中找到匹配的 template_id\n'
' 2) 调用 get_report_link({"template_id": XX}) 获取固定链接\n'
' 3) 如果返回 success=true,直接把 url 给用户(无需重新生成)\n'
' 4) 如果返回 success=false(模板未生成固定链接),再调用 generate_report 临时生成\n'
'【get_report_link 返回值】success=true 时,url 字段是固定链接,直接展示给用户。\n'
'【generate_report 推荐用法】先用 odoo_search_read 查询数据,将返回的 records '
'数组直接传入 records 参数。这样无需重复指定 model/domain/fields,'
'且用户看到的数据与你的查询结果完全一致。\n'
'【严禁编造链接!】你绝对不能自己编造、猜测或拼接报表 URL。'
'报表链接只能从 generate_report 工具返回的 url 字段中获取,'
'报表链接只能从 get_report_link 或 generate_report 工具返回的 url 字段中获取,'
'直接用工具返回的 URL,不要做任何修改。'
'如果你自己拼一个 /ai/report/xxx 或任何自创的链接格式,用户点击后会看到"报表不存在"。\n'
'生成报表后系统会自动尝试推送到用户的企微。无论推送是否成功,'
'回复时都要说明查询结果并附上工具返回的真实链接,如:\n'
'已为您生成报表,共查询到 XX 条记录:\n'
'https://oa.thtzjt.com/ai/report/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n'
'https://oa.thtzjt.com/ai/report/xxxxxxxxxxxxxxxxxxxxxxxx\n'
'查询结果中的时间已经转换为北京时间(UTC+8),直接展示即可,无需再做时区转换。\n\n'
'【重要】你具有记忆能力。当对话中发现用户的新偏好、工作内容、习惯等值得长期记住的信息时,'
'主动调用 save_memory 工具保存。下次对话你会自动获取这些记忆。\n'
@@ -756,9 +806,10 @@ class AiConversation(models.Model):
# —— 注入报表模板 ——
templates_info = self.env['ai.report.template'].get_templates_for_ai()
if templates_info:
system_msg += '\n--- 可用报表模板(调用 generate_report 时可参考)---\n'
system_msg += '\n--- 可用报表模板(调用 get_report_link 或 generate_report 时可参考)---\n'
system_msg += templates_info + '\n'
system_msg += '当用户查询相关数据时,优先使用对应模板的字段和条件生成报表。\n'
system_msg += '当用户查询相关数据时,优先调用 get_report_link(template_id) 获取固定链接;'
'如无固定链接,再使用 generate_report 配合模板生成报表。\n'
if user_context:
system_msg += f'\n--- 当前用户信息 ---\n{user_context}\n'
+33
View File
@@ -219,6 +219,39 @@ class AiReport(models.Model):
_logger.exception('Report fetch_data error: %s', e)
return {'error': str(e)}
def refresh_data(self):
"""强制实时查询并更新 report_data 缓存。
用于定时任务刷新固定报表数据:
- 清空 report_data 缓���
- 执行实时查询
- 将结果写回 report_data
"""
self.ensure_one()
old_data = self.report_data
self.report_data = False # 清除缓存,强制实时查询
try:
result = self.fetch_data()
if result.get('success'):
self.report_data = json.dumps(result, ensure_ascii=False, default=str)
_logger.info(
'固定报表刷新成功: template_id=%s, report_id=%s, count=%s',
self.template_id.id, self.id, result.get('count', 0)
)
return result
else:
# 查询失败,恢复旧数据
self.report_data = old_data
_logger.error(
'固定报表刷新失败: template_id=%s, report_id=%s, error=%s',
self.template_id.id, self.id, result.get('error', 'unknown')
)
return result
except Exception as e:
self.report_data = old_data
_logger.exception('固定报表刷新异常: %s', e)
return {'error': str(e)}
def is_valid(self):
"""检查报表是否有效(未过期、启用状态)"""
self.ensure_one()
+109 -1
View File
@@ -37,6 +37,15 @@ class AiReportTemplate(models.Model):
sequence = fields.Integer('排序', default=10)
active = fields.Boolean(default=True)
# ===== 固定报表 =====
fixed_report_id = fields.Many2one(
'ai.report', string='固定报表',
copy=False, ondelete='set null',
help='点击"生成固定链接"后生成,报表不过期,每天凌晨自动刷新数据')
fixed_report_url = fields.Char(
'固定链接', compute='_compute_fixed_report_url',
help='固定报表的访问链接,可直接发给用户')
# ===== 视觉配置(拆分为 One2many) =====
column_ids = fields.One2many('ai.report.template.column', 'template_id',
string='列定义', copy=True)
@@ -285,4 +294,103 @@ class AiReportTemplate(models.Model):
vals['field_labels'] = json.dumps(
{f.name: f.field_description for f in fields_records},
ensure_ascii=False
)
)
# ===== 固定报表:计算字段 =====
@api.depends('fixed_report_id.access_token', 'fixed_report_id.active')
def _compute_fixed_report_url(self):
for t in self:
if t.fixed_report_id and t.fixed_report_id.active:
t.fixed_report_url = t.fixed_report_id.get_report_url()
else:
t.fixed_report_url = False
# ===== 固定报表:生成/刷新动作 =====
def action_generate_fixed_report(self):
"""生成固定报表链接(按钮动作)
- 若 fixed_report_id 为空:创建一条不过期的 ai.report 记录
- 若已存在:刷新数据(调用 report.refresh_data)
"""
self.ensure_one()
if self.fixed_report_id:
# 已存在:刷新数据
result = self.fixed_report_id.refresh_data()
if result.get('error'):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '刷新失败',
'message': result['error'],
'type': 'danger',
}
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '刷新成功',
'message': f'报表数据已更新,共 {result.get("count", 0)} 条记录',
'type': 'success',
}
}
# 不存在:创建固定报表
report = self.env['ai.report'].sudo().create({
'name': self.name,
'model_name': self.model_name,
'field_list': self.field_list or '[]',
'field_labels': self.field_labels or '{}',
'domain': self.default_domain or '[]',
'order': self.default_order or 'id desc',
'template_id': self.id,
'expire_date': False, # 不过期
'active': True,
})
self.fixed_report_id = report
url = report.get_report_url()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '固定链接已生成',
'message': f'链接:{url}',
'type': 'success',
'sticky': True,
}
}
# ===== 定时任务:刷新所有固定报表 =====
@api.model
def _cron_refresh_pinned_reports(self):
"""凌晨1点定时任务:刷新所有固定报表数据
遍历所有 fixed_report_id 非空且 active 的模板,
调用对应 report.refresh_data() 刷新缓存数据。
"""
templates = self.sudo().search([
('fixed_report_id', '!=', False),
('fixed_report_id.active', '=', True),
])
refreshed, failed = 0, 0
for t in templates:
report = t.fixed_report_id
try:
result = report.refresh_data()
if result.get('error'):
failed += 1
_logger.error(
'固定报表刷新失败: template=%s(%s), report=%s, error=%s',
t.name, t.id, report.id, result['error']
)
else:
refreshed += 1
except Exception as e:
failed += 1
_logger.exception('固定报表刷新异常: template=%s(%s)', t.name, t.id)
_logger.info(
'固定报表定时刷新完成: 成功=%s, 失败=%s, 总计=%s',
refreshed, failed, len(templates)
)
return {'refreshed': refreshed, 'failed': failed}
@@ -107,6 +107,7 @@
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="model_name"/>
<field name="fixed_report_url" widget="url" optional="hide"/>
<field name="description"/>
<field name="active" widget="boolean_toggle"/>
</tree>
@@ -132,6 +133,10 @@
<h1><field name="name" placeholder="如:销售订单列表模板"/></h1>
</td>
<td style="width:40%;text-align:right;vertical-align:bottom">
<button name="action_generate_fixed_report" type="object"
string="生成固定链接" class="btn-secondary"
icon="fa-link"
help="生成永久有效的报表链接,每天凌晨自动刷新"/>
<button name="action_preview" type="object"
string="预览报表" class="oe_highlight"
invisible="not model_name"/>
@@ -148,6 +153,10 @@
<field name="model_name" invisible="1"/>
<field name="description"
placeholder="描述此模板用途,AI 会根据描述判断何时使用"/>
<field name="fixed_report_url" readonly="1"
widget="url"
placeholder="点击『生成固定链接』按钮生成"
help="固定报表访问链接,凌晨自动刷新数据"/>
<!-- <field name="sequence"/>-->
<field name="active" invisible="1"/>
<field name="column_field_ids" invisible="1"/>