!11931 Merge remote-tracking branch 'origin/dev' into dev
Merge pull request !11931 from 李鹏宇/dev
This commit is contained in:
@@ -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': [
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user