日程和日志
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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}'}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user