日程和日志

This commit is contained in:
李鹏宇
2026-06-15 09:32:17 +08:00
parent 25a3483fd0
commit bfa149feb0
7 changed files with 151 additions and 29 deletions
+13 -10
View File
@@ -466,26 +466,29 @@ class AiConversation(models.Model):
return {'error': f'{type(e).__name__}: {e}'}
def _execute_skill_tool(self, name, args):
"""尝试执行技能规则工具,找不到返回 None"""
"""尝试执行技能/方法规则工具,找不到返回 None"""
provider = self.provider_id
if not provider:
return None
skill_rule = self.env['ai.rule'].sudo().search([
('rule_type', '=', 'skill'),
# 先查 skill 类型
rule = self.env['ai.rule'].sudo().search([
('rule_type', 'in', ['skill', 'method']),
('skill_name', '=', name),
('active', '=', True),
('provider_ids', 'in', [provider.id]),
], limit=1)
if not skill_rule:
# 也搜索不限服务商的全局技能
skill_rule = self.env['ai.rule'].sudo().search([
('rule_type', '=', 'skill'),
if not rule:
# 也搜索不限服务商的全局规则
rule = self.env['ai.rule'].sudo().search([
('rule_type', 'in', ['skill', 'method']),
('skill_name', '=', name),
('active', '=', True),
('provider_ids', '=', False),
], limit=1)
if skill_rule:
return skill_rule.execute_skill(args)
if rule:
if rule.rule_type == 'method':
return rule.execute_method(args)
return rule.execute_skill(args)
return None
def _execute_generate_report(self, args):
@@ -875,7 +878,7 @@ class AiConversation(models.Model):
applicable_rules = provider.get_applicable_rules()
instruction_rules = applicable_rules.filtered(lambda r: r.rule_type == 'instruction')
restriction_rules = applicable_rules.filtered(lambda r: r.rule_type == 'restriction')
skill_rules = applicable_rules.filtered(lambda r: r.rule_type == 'skill' and r.skill_name)
skill_rules = applicable_rules.filtered(lambda r: r.rule_type in ('skill', 'method') and r.skill_name)
if instruction_rules:
system_msg += '\n--- 回复指令(必须遵守)---\n'
+59 -2
View File
@@ -23,10 +23,12 @@ class AiRule(models.Model):
('instruction', '回复指令'),
('restriction', '限制规则'),
('skill', '技能接口'),
('method', '调用模型方法'),
], string='规则类型', required=True, default='instruction',
help='回复指令:注入到 AI 系统提示词中,控制回复风格和内容。\n'
'限制规则:告诉 AI 哪些话题不能回答。\n'
'技能接口:对接外部 API,AI 可调用获取专业数据。'
'技能接口:对接外部 API,AI 可调用获取专业数据。\n'
'调用模型方法:直接调用 Odoo 模型方法(如撤回消息),无需 HTTP 接口。'
)
active = fields.Boolean(string='启用', default=True)
priority = fields.Integer(string='优先级', default=10, help='数字越小优先级越高')
@@ -81,6 +83,16 @@ class AiRule(models.Model):
string='接口超时(秒)', default=10,
)
# ---- 调用模型方法配置 ----
skill_model = fields.Char(
string='目标模型',
help='方法所在模型的技术名称,如 attendance.message.log。仅 method 类型使用。',
)
skill_method_name = fields.Char(
string='方法名称',
help='要调用的模型方法名,如 action_recall_message。仅 method 类型使用。',
)
# ---- 技能接口执行 ----
def execute_skill(self, arguments):
"""调用技能接口,返回结果字典"""
@@ -121,7 +133,7 @@ class AiRule(models.Model):
return {'error': '技能接口返回格式错误(非 JSON)'}
def get_tool_schema(self):
"""将技能规则转换为 OpenAI 兼容的 tool schema"""
"""将技能规则转换为 OpenAI 兼容的 tool schema(skill 和 method 类型通用)"""
self.ensure_one()
try:
params = json.loads(self.skill_parameters or '{}')
@@ -135,3 +147,48 @@ class AiRule(models.Model):
'parameters': params,
},
}
def execute_method(self, arguments):
"""执行模型方法调用(rule_type='method')。搜索目标模型记录,逐条调用指定方法。"""
self.ensure_one()
if self.rule_type != 'method':
return {'error': '该规则不是调用模型方法类型'}
if not self.skill_model or self.skill_model not in self.env:
return {'error': f'未配置目标模型或模型不存在:{self.skill_model}'}
if not self.skill_method_name:
return {'error': '未配置方法名称'}
Model = self.env[self.skill_model]
method = getattr(Model, self.skill_method_name, None)
if not method:
return {'error': f'模型 {self.skill_model} 中没有方法 {self.skill_method_name}'}
try:
domain = arguments.get('domain') or []
ids = arguments.get('ids') or []
if ids:
records = Model.browse(ids).exists()
else:
records = Model.search(domain)
if not records:
return {'success': True, 'message': '没有找到符合条件的记录', 'count': 0}
results = []
for rec in records:
try:
res = method(rec)
results.append({'id': rec.id, 'result': str(res) if res else None})
except Exception as e:
results.append({'id': rec.id, 'error': str(e)})
return {
'success': True,
'count': len(records),
'results': results,
}
except Exception as e:
_logger.exception('Method rule %s failed: %s', self.skill_name, e)
return {'error': f'{type(e).__name__}: {e}'}
+27 -2
View File
@@ -13,7 +13,8 @@
<field name="rule_type" widget="badge"
decoration-info="rule_type == 'instruction'"
decoration-warning="rule_type == 'restriction'"
decoration-success="rule_type == 'skill'"/>
decoration-success="rule_type == 'skill'"
decoration-danger="rule_type == 'method'"/>
<field name="provider_ids" widget="many2many_tags"/>
<field name="group_ids" widget="many2many_tags"/>
<field name="active" widget="boolean_toggle"/>
@@ -58,7 +59,7 @@
<notebook>
<!-- 回复指令 / 限制规则 -->
<page string="规则内容" name="content"
invisible="rule_type == 'skill'">
invisible="rule_type in ('skill', 'method')">
<field name="content" placeholder="写给 AI 的指令内容,如:所有回复不超过 200 字 / 不得回答薪资相关问题" nolabel="1"/>
</page>
@@ -88,6 +89,28 @@
placeholder='{"Authorization": "Bearer xxx"}'/>
</group>
</page>
<!-- 调用模型方法配置 -->
<page string="方法配置" name="method"
invisible="rule_type != 'method'">
<group>
<group string="方法定义">
<field name="skill_name"
placeholder="如:recall_wecom_message"/>
<field name="skill_description"
placeholder="当用户要撤回企微消息时调用"/>
</group>
<group string="模型配置">
<field name="skill_model"
placeholder="如:attendance.message.log"/>
<field name="skill_method_name"
placeholder="如:action_recall_message"/>
</group>
</group>
<group string="参数定义">
<field name="skill_parameters" nolabel="1"
placeholder='{"type": "object", "properties": {"domain": {"type": "array", "description": "筛选条件"}, "ids": {"type": "array", "items": {"type": "integer"}, "description": "记录ID列表"}}, "required": []}'/>
</group>
</page>
</notebook>
</sheet>
</form>
@@ -110,6 +133,8 @@
domain="[('rule_type','=','restriction')]"/>
<filter name="filter_skill" string="技能接口"
domain="[('rule_type','=','skill')]"/>
<filter name="filter_method" string="模型方法"
domain="[('rule_type','=','method')]"/>
<separator/>
<filter name="filter_active" string="已启用"
domain="[('active','=',True)]"/>
@@ -3556,6 +3556,7 @@ class VacationOtherLine(models.Model):
class AttendanceMessageLog(models.Model):
_name = 'attendance.message.log'
_description = '消息提醒日志'
_order = 'update_date desc'
vacation_id = fields.Many2one('yuthon.vacation.reports', string='关联假期额度记录')
employee_id = fields.Many2one('hr.employee', string='员工')
@@ -3568,6 +3569,34 @@ class AttendanceMessageLog(models.Model):
notice_type = fields.Selection([('attendance', '出勤'), ('schedule', '日程'), ('announcement', '公告'), ('attendance_confirmation', '考勤确认单'), ('salary_slip', '薪资条'),
('process', '流程'), ('other', '其他')], string='类型')
message_url = fields.Char(string="url")
msg_id = fields.Char(string='消息ID')
category = fields.Char(string='消息类别')
def action_recall_message(self):
"""撤回企微消息(调用企微back_message接口)"""
self.ensure_one()
if not self.msg_id:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '撤回失败',
'message': '消息ID为空,无法撤回',
'type': 'warning',
}
}
category = self.category or 'att'
self.env['wecom.apps'].back_message(self.msg_id, category=category)
self.msg_id = '已撤回'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '撤回成功',
'message': f'消息已撤回(msg_id={self.msg_id})',
'type': 'success',
}
}
@api.depends('employee_id')
def _compute_shift_id(self):
@@ -4,6 +4,7 @@
<field name="model">attendance.message.log</field>
<field name="arch" type="xml">
<tree string="消息提醒日志" editable="bottom">
<button name="action_recall_message" type="object" string="撤回" icon="fa-undo" class="oe_highlight"/>
<field name="employee_id"/>
<field name="shift_id"/>
<field name="notice_type"/>
@@ -12,6 +13,8 @@
<field name="content"/>
<field name="service_address"/>
<field name="message_url"/>
<field name="msg_id"/>
<field name="category"/>
<field name="company_id"/>
</tree>
</field>
@@ -33,7 +36,7 @@
<field name="name">消息提醒日志</field>
<field name="res_model">attendance.message.log</field>
<field name="view_mode">tree,search</field>
<field name="context">{'search_default_group_notice_type': 1}</field>
<field name="limit">20</field>
</record>
<menuitem
id="attendance_message_log_menu_root"
+13 -10
View File
@@ -275,19 +275,22 @@ class WeComApps(models.Model):
_logger.info(f"----------地址识别成功,已经发送消息通知---------------------")
_logger.info(f"----------地址识别成功,已经发送消息通知---------------------")
result_data = self.sync_function_api("message_send", "MESSAGE_SEND", data, category)
employee_id = self.sudo().env["res.users"].search([("login", "=", user_id)], limit=1).employee_ids
notice_type = 'attendance' if '记得打卡' in description else 'process'
self.env["attendance.message.log"].create({
"employee_id": employee_id.id,
"content": description,
"notice_type": notice_type,
"service_address":service_address,
'update_date': fields.Datetime.now(),
"message_url": url,
})
_logger.info(f"------------------------接口收到响应返回值{result_data}-------------------------------")
_logger.info(f"------------------------{service_address}-------------------------------")
if result_data.get("errcode", None) == 0:
# 发送成功:写消息提醒日志,记录 msg_id 和 category 用于撤回
employee_id = self.sudo().env["res.users"].search([("login", "=", user_id)], limit=1).employee_ids
notice_type = 'attendance' if '记得打卡' in description else 'process'
self.env["attendance.message.log"].create({
"employee_id": employee_id.id,
"content": description,
"notice_type": notice_type,
"service_address": service_address,
'update_date': fields.Datetime.now(),
"message_url": url,
"msg_id": result_data.get('msgid'),
"category": category,
})
return True, result_data
# 卡片发送失败时,把返回值当文本推送给用户方便排查
if touser:
+6 -4
View File
@@ -285,13 +285,15 @@ class YuthonWillTask(models.Model):
"""日程定时任务提醒"""
_logger.info('-----------日程任务验证--------')
now = fields.datetime.now() + timedelta(hours=8)
dates_task_ids = self.env['yuthon.dates.task'].search([('task_state', '=', 'to_do'), ('reminder_time', '<', fields.datetime.now() + timedelta(days=1))])
today = fields.date.today()
dates_task_ids = self.env['yuthon.dates.task'].search([('task_state', '=', 'to_do'), ('reminder_time', '<', fields.datetime.now())])
for task in dates_task_ids:
hour1 = (task.reminder_time + timedelta(hours=8)).hour
minute1 = (task.reminder_time + timedelta(hours=8)).minute
reminder_local = task.reminder_time + timedelta(hours=8)
hour1 = reminder_local.hour
minute1 = reminder_local.minute
_logger.info(f'-任务时间{hour1}:{minute1}任务分钟{now.hour}:{now.minute}')
_logger.info(f'-状态{minute1 == now.minute and hour1 == now.hour}')
if minute1 == now.minute and hour1 == now.hour and task.reminder_time.day == fields.date.today().day:
if minute1 == now.minute and hour1 == now.hour and reminder_local.date() == today:
task.write({'task_state': 'done'})
will_id = task.yuthon_will_task_id
will_id.task_state = 'done'