1128 lines
43 KiB
Plaintext
1128 lines
43 KiB
Plaintext
# =============================================================================
|
||
# IT智能服务台 — 审批流程 API
|
||
# =============================================================================
|
||
# 说明:提供审批模板管理和API提交功能
|
||
# - 模板详情获取
|
||
# - API提交审批申请
|
||
# - 审批状态回调处理
|
||
# =============================================================================
|
||
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import os
|
||
from typing import Optional
|
||
|
||
import httpx
|
||
from fastapi import APIRouter, Depends, Query
|
||
from pydantic import BaseModel
|
||
import redis.asyncio as aioredis
|
||
|
||
from app.config import settings
|
||
from app.services.asset_service import AssetService
|
||
from app.services.wecom_service import WecomService
|
||
from app.utils.token_manager import TokenManager
|
||
from app.utils.response import success_response
|
||
|
||
logger = logging.getLogger(__name__)
|
||
router = APIRouter()
|
||
|
||
# IT资产升级申请模板ID(回调时用于识别审批类型,触发年限核查推送)
|
||
ASSET_UPGRADE_TEMPLATE_ID = "Bs7ucTGsPuFhxfk8pn8EydxrWxkVetB4JR8Pb6PHS"
|
||
|
||
# Redis客户端(依赖注入)
|
||
async def get_redis() -> aioredis.Redis:
|
||
"""获取Redis客户端依赖"""
|
||
from app.main import redis_client
|
||
return redis_client
|
||
|
||
|
||
# =============================================================================
|
||
# 审批模板配置(静态硬编码 — 18个审批流程,12种审批类型)
|
||
# =============================================================================
|
||
# 数据来源:docs/02-产品需求/approval_templates.json
|
||
# 所有模板 type 统一为 "jump"(直接URL跳转)
|
||
|
||
APPROVAL_TEMPLATES: dict[str, dict] = {
|
||
# --- 办公用品申请 ---
|
||
"office_supplies": {
|
||
"id": "office_supplies",
|
||
"name": "办公用品超额领用审批",
|
||
"type": "jump",
|
||
"keywords": ["办公用品", "超额", "领用"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=3WN6zRucbjycdnR94gBvkSVuXRamX7pKW4PrmNFh&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
# --- 设备申请 ---
|
||
# IT资产领用 改用ITSM工单系统(企微审批模板 C4c8qt31... 已失效)
|
||
"asset_receive": {
|
||
"id": "asset_receive",
|
||
"name": "IT资产领用申请",
|
||
"type": "jump",
|
||
"keywords": ["资产领用", "领用登记", "设备领用"],
|
||
"url": "https://itsm.servyou.com.cn/itsm-miniapp-mobile/",
|
||
"location": "运维平台",
|
||
},
|
||
# IT资产借用 改用ITSM工单系统(企微审批模板 3TmACnFs... 已失效)
|
||
"asset_borrow": {
|
||
"id": "asset_borrow",
|
||
"name": "IT资产借用申请",
|
||
"type": "jump",
|
||
"keywords": ["资产借用", "借用", "借用设备"],
|
||
"url": "https://itsm.servyou.com.cn/itsm-miniapp-mobile/",
|
||
"location": "运维平台",
|
||
},
|
||
# IT资产升级 改用ITSM工单系统(企微审批模板 Bs7ucTGs... 已失效)
|
||
"asset_upgrade": {
|
||
"id": "asset_upgrade",
|
||
"name": "IT资产升级申请",
|
||
"type": "jump",
|
||
"keywords": ["资产升级", "设备升级", "升级"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=IT%E8%AE%BE%E5%A4%87%E5%8D%87%E7%BA%A7%E4%B8%8E%E7%A1%AC%E4%BB%B6%E7%BB%B4%E4%BF%AE",
|
||
"location": "运维平台",
|
||
},
|
||
"it_device_repair": {
|
||
"id": "it_device_repair",
|
||
"name": "IT设备升级与硬件维修",
|
||
"type": "jump",
|
||
"keywords": ["设备升级", "硬件维修", "设备维修"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=IT%E8%AE%BE%E5%A4%87%E5%8D%87%E7%BA%A7%E4%B8%8E%E7%A1%AC%E4%BB%B6%E7%BB%B4%E4%BF%AE",
|
||
"location": "运维平台",
|
||
},
|
||
# --- 账号权限申请 ---
|
||
"wecom_external": {
|
||
"id": "wecom_external",
|
||
"name": "企微外联权限申请",
|
||
"type": "jump",
|
||
"keywords": ["外联", "企微外联", "外部联系人"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=C4WrCbZd214XrDMZJiHDho7ZQHWX7gsabb7x2fF72&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
"zero_trust_vpn": {
|
||
"id": "zero_trust_vpn",
|
||
"name": "员工零信任(原VPN)账号",
|
||
"type": "jump",
|
||
"keywords": ["VPN", "vpn", "零信任", "VPN账号"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=%E5%91%98%E5%B7%A5%E9%9B%B6%E4%BF%A1%E4%BB%BB%EF%BC%88%E5%8E%9FVPN%EF%BC%89%E8%B4%A6%E5%8F%B7%E7%94%B3%E8%AF%B7",
|
||
"location": "运维平台",
|
||
},
|
||
# --- 软件服务申请 --- 改用ITSM工单系统(企微审批模板 3TmACf8D... 已失效)
|
||
"software_service": {
|
||
"id": "software_service",
|
||
"name": "商业软件服务申请",
|
||
"type": "jump",
|
||
"keywords": ["软件", "商业软件", "软件服务"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=%E5%95%86%E4%B8%9A%E8%BD%AF%E4%BB%B6%E6%9C%8D%E5%8A%A1%E7%94%B3%E8%AF%B7",
|
||
"location": "运维平台",
|
||
},
|
||
# --- 资产处置申请 ---
|
||
"asset_repair": {
|
||
"id": "asset_repair",
|
||
"name": "IT资产外修申请",
|
||
"type": "jump",
|
||
"keywords": ["外修", "资产外修", "设备维修"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=Bs7ucTLPo42dtj8Y1LzBoujijsa6geRWaRxZJjk4X&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
"asset_scrap": {
|
||
"id": "asset_scrap",
|
||
"name": "IT资产报废申请",
|
||
"type": "jump",
|
||
"keywords": ["报废", "资产报废", "设备报废"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=C4WroCDfWuHKyujQjatjm3AjNv67imXk5C6WNooFkb&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
"asset_return": {
|
||
"id": "asset_return",
|
||
"name": "资产退还登记",
|
||
"type": "jump",
|
||
"keywords": ["退还", "资产退还", "退还设备"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=C4c8qt33AZ52a7n9BBWDh6PmsDnpM5B6w8geqqqoHz&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
# --- 会议室故障报修 ---
|
||
"meeting_room_repair": {
|
||
"id": "meeting_room_repair",
|
||
"name": "会议室故障报修",
|
||
"type": "jump",
|
||
"keywords": ["会议室", "故障报修", "报修"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=C4ZXJMtjQJiPXo6N5vNMK26uPRT3KTi9VvkH2NScg&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
# --- 企业应用管理 ---
|
||
"app_management": {
|
||
"id": "app_management",
|
||
"name": "企业应用管理",
|
||
"type": "jump",
|
||
"keywords": ["企业应用", "应用管理", "应用开通"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=3WLJnRg2Se1fwizQtNvFtcYMgci1mhRJZhMw2FFKb&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
# --- 资产变更确认 ---
|
||
"asset_change": {
|
||
"id": "asset_change",
|
||
"name": "资产变更确认",
|
||
"type": "jump",
|
||
"keywords": ["资产变更", "变更确认", "变更"],
|
||
"url": "https://app.work.weixin.qq.com/wework_admin/approval_v3#/?template_id=C4cA2owjRcXRRPQ46otZvUHoWNEKL5t25tHHfeePip&sp_id=&from=template_list",
|
||
"location": "企微审批",
|
||
},
|
||
# --- 终端设备网络准入 ---
|
||
"network_access": {
|
||
"id": "network_access",
|
||
"name": "终端设备网络准入申请",
|
||
"type": "jump",
|
||
"keywords": ["网络准入", "终端准入", "准入申请"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=%E7%BB%88%E7%AB%AF%E8%AE%BE%E5%A4%87%E7%BD%91%E7%BB%9C%E5%87%86%E5%85%A5%E7%94%B3%E8%AF%B7",
|
||
"location": "运维平台",
|
||
},
|
||
# --- 活动与会议技术支持 ---
|
||
"event_support": {
|
||
"id": "event_support",
|
||
"name": "活动与会议技术支持",
|
||
"type": "jump",
|
||
"keywords": ["活动支持", "会议支持", "技术支持", "活动技术"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=%E6%B4%BB%E5%8A%A8%E4%B8%8E%E4%BC%9A%E8%AE%AE%E6%8A%80%E6%9C%AF%E6%94%AF%E6%8C%81",
|
||
"location": "运维平台",
|
||
},
|
||
# --- 员工IT支持与故障报修 ---
|
||
"it_support_repair": {
|
||
"id": "it_support_repair",
|
||
"name": "员工IT支持与故障报修",
|
||
"type": "jump",
|
||
"keywords": ["故障报修", "IT支持", "技术支持", "报修"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=%E5%91%98%E5%B7%A5IT%E6%94%AF%E6%8C%81%E4%B8%8E%E6%95%85%E9%9A%9C%E6%8A%A5%E4%BF%AE",
|
||
"location": "运维平台",
|
||
},
|
||
# --- 公共邮箱账号申请 ---
|
||
"public_email": {
|
||
"id": "public_email",
|
||
"name": "公共邮箱账号申请",
|
||
"type": "jump",
|
||
"keywords": ["公共邮箱", "公共账号", "共享邮箱"],
|
||
"url": "http://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=%E5%85%AC%E5%85%B1%E9%82%AE%E7%AE%B1%E8%B4%A6%E5%8F%B7%E7%94%B3%E8%AF%B7",
|
||
"location": "运维平台",
|
||
},
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# 审批意图识别 — 关键词预过滤 & 降级兜底
|
||
# =============================================================================
|
||
# 说明:覆盖 12 种审批类型、18 个审批流程的关键词,用于快速过滤非审批消息,
|
||
# 避免 Dify 被每条消息调用。当 Dify 不可用时降级为关键词匹配。
|
||
|
||
# 审批关键词预过滤列表(v2.0 收窄版 — 2026-07-13)
|
||
# ------------------------------------------------------------
|
||
# 设计原则:只保留「强意图词」和「复合专有词」,移除高频泛化词。
|
||
#
|
||
# 改造前 ~40 词(含"设备""电脑""邮箱""权限""软件""报修"等),
|
||
# 几乎覆盖所有 IT 消息 → 预过滤命中率 ~60% → 大量无效 Dify 调用。
|
||
#
|
||
# 改造后策略:
|
||
# - 强意图动词:用户明确表达"申请/提交"意愿时才触发
|
||
# - 复合专有词:仅在审批场景出现,不会在普通 IT 咨询中出现
|
||
# - 移除的泛化词(设备/电脑/邮箱/权限/软件/报修/变更/会议室等)
|
||
# 改由 Dify 意图识别内部判断,不作为预过滤触发条件
|
||
#
|
||
# 预期效果:预过滤命中率从 ~60% 降至 ~15%,减少 75% 无效 Dify 调用
|
||
APPROVAL_PREFILTER_KEYWORDS: list[str] = [
|
||
# === 强意图动词/短语(用户明确表达申请意愿)===
|
||
"申请", "审批", "提交", "表单", "走流程",
|
||
"帮我申请", "我要申请", "需要申请", "想申请",
|
||
# === 复合专有词(仅在审批场景出现,日常 IT 咨询不会用)===
|
||
"资产领用", "资产借用", "资产升级", "资产变更", "资产处置",
|
||
"网络准入", "终端准入",
|
||
"办公用品", "超额领用",
|
||
"企业应用管理", "应用开通",
|
||
"公共邮箱", "共享邮箱", "公共账号",
|
||
"故障报修", "IT支持",
|
||
"自备电脑", "电脑补贴", "BYOD", "byod",
|
||
]
|
||
|
||
# 关键词到审批类型的映射(Dify 不可用时降级兜底用)
|
||
KEYWORD_TO_APPROVAL_TYPE: dict[str, str] = {
|
||
# 设备申请
|
||
"设备": "设备申请", "电脑": "设备申请", "笔记本": "设备申请",
|
||
"显示器": "设备申请", "资产领用": "设备申请", "资产借用": "设备申请", "资产升级": "设备申请",
|
||
# 账号权限申请
|
||
"VPN": "账号权限申请", "vpn": "账号权限申请", "账号": "账号权限申请",
|
||
"邮箱": "账号权限申请", "外联": "账号权限申请", "权限": "账号权限申请",
|
||
# 软件服务申请
|
||
"软件": "软件服务申请", "业务系统": "软件服务申请",
|
||
# 资产处置申请
|
||
"外修": "资产处置申请", "报废": "资产处置申请", "退还": "资产处置申请",
|
||
# 办公用品申请
|
||
"办公用品": "办公用品申请", "超额": "办公用品申请",
|
||
# 会议室故障报修
|
||
"会议室": "会议室故障报修", "故障报修": "会议室故障报修", "报修": "会议室故障报修",
|
||
# 企业应用管理
|
||
"企业应用": "企业应用管理", "应用管理": "企业应用管理", "应用开通": "企业应用管理",
|
||
# 资产变更确认
|
||
"资产变更": "资产变更确认", "变更确认": "资产变更确认", "变更": "资产变更确认",
|
||
# 终端设备网络准入
|
||
"网络准入": "终端设备网络准入", "终端准入": "终端设备网络准入", "准入申请": "终端设备网络准入",
|
||
# 活动与会议技术支持
|
||
"活动支持": "活动与会议技术支持", "会议支持": "活动与会议技术支持", "活动技术": "活动与会议技术支持",
|
||
# 员工IT支持与故障报修
|
||
"IT支持": "员工IT支持与故障报修",
|
||
# 公共邮箱账号申请
|
||
"公共邮箱": "公共邮箱账号申请", "公共账号": "公共邮箱账号申请", "共享邮箱": "公共邮箱账号申请",
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# Schema 定义
|
||
# =============================================================================
|
||
|
||
class ApprovalTemplateResponse(BaseModel):
|
||
"""审批模板响应"""
|
||
id: str
|
||
name: str
|
||
type: str
|
||
keywords: list[str]
|
||
|
||
|
||
class ApprovalJumpRequest(BaseModel):
|
||
"""跳转审批请求"""
|
||
template_id: str
|
||
employee_id: Optional[str] = None
|
||
|
||
|
||
class ApprovalJumpResponse(BaseModel):
|
||
"""跳转审批响应"""
|
||
url: str
|
||
template_name: str
|
||
|
||
|
||
class ApprovalContentItem(BaseModel):
|
||
"""审批表单控件内容"""
|
||
control: str # 控件类型: Text, Textarea, Number, Money, Date, Selector, Contact, etc.
|
||
id: str # 控件ID
|
||
value: dict # 控件值
|
||
|
||
|
||
class ApprovalSubmitRequest(BaseModel):
|
||
"""API提交审批请求"""
|
||
template_id: str
|
||
employee_id: str # 申请人userid
|
||
contents: list[ApprovalContentItem] # 表单内容
|
||
use_template_approver: int = 1 # 1-使用模板预设流程
|
||
|
||
|
||
class ApprovalSubmitResponse(BaseModel):
|
||
"""API提交审批响应"""
|
||
sp_no: str # 审批单号
|
||
template_name: str
|
||
|
||
|
||
class ApprovalCallbackRequest(BaseModel):
|
||
"""审批回调请求(XML解析后的模型)"""
|
||
sp_no: str
|
||
sp_name: str
|
||
template_id: str
|
||
apply_time: int
|
||
applyer_userid: str
|
||
sp_status: int # 1-审批中 2-已通过 3-已驳回 4-已撤销 6-通过后撤销 7-已删除 10-已支付
|
||
status_change_event: int # 1-提单 2-同意 3-驳回 4-转审 5-催办 6-撤销 8-通过后撤销 10-添加备注
|
||
|
||
|
||
class ApprovalUrgeRequest(BaseModel):
|
||
"""审批推送请求 — 手动触发资产年限核查+推送"""
|
||
sp_no: str
|
||
|
||
|
||
class ApprovalDetectIntentRequest(BaseModel):
|
||
"""审批意图检测请求"""
|
||
text: str
|
||
employee_id: Optional[str] = None
|
||
|
||
|
||
class ApprovalDetectIntentResponse(BaseModel):
|
||
"""审批意图检测响应
|
||
|
||
Attributes:
|
||
is_approval_request: 是否为审批请求(原字段,语义不变)
|
||
confidence: 置信度(0.0~1.0)(原字段,语义不变)
|
||
approval_type: 审批类型(原字段,语义不变)
|
||
source: 结果来源 — dify(Dify识别) / keyword_prefilter(关键词预过滤未命中) / fallback(降级兜底)
|
||
intent_type: 意图大类(新增)— approval/it_consult/non_it_routing/chitchat
|
||
business_category: 非IT业务类别(新增)— 行政/人力资源/财务/法务/行政-物业,仅 non_it_routing 时有值
|
||
routing_confidence: 路由置信度(新增)— 0.0~1.0,≥0.7 触发名片推荐
|
||
"""
|
||
is_approval_request: bool
|
||
confidence: float
|
||
approval_type: Optional[str] = None
|
||
source: str # "dify" | "keyword_prefilter" | "fallback"
|
||
# 以下为 v3 统一意图识别新增字段(向后兼容:原审批逻辑只读取前4个字段)
|
||
intent_type: str = "chitchat"
|
||
business_category: Optional[str] = None
|
||
routing_confidence: float = 0.0
|
||
|
||
|
||
# =============================================================================
|
||
# 企微API调用辅助函数
|
||
# =============================================================================
|
||
|
||
async def get_approval_token(redis: aioredis.Redis) -> str:
|
||
"""获取企微access_token(使用IT支持应用Secret,IP已在白名单中)。"""
|
||
manager = TokenManager(redis)
|
||
try:
|
||
return await manager.get_token()
|
||
finally:
|
||
await manager.close()
|
||
|
||
|
||
async def get_template_detail(access_token: str, template_id: str) -> dict:
|
||
"""获取审批模板详情
|
||
|
||
对应企微API:
|
||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/gettemplatedetail
|
||
|
||
返回模板内的控件构成及控件ID
|
||
"""
|
||
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/gettemplatedetail"
|
||
params = {"access_token": access_token}
|
||
|
||
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout=30.0, connect=10.0, read=30.0)) as client:
|
||
response = await client.post(url, params=params, json={"template_id": template_id})
|
||
result = response.json()
|
||
|
||
if result.get("errcode") != 0:
|
||
logger.error(f"获取模板详情失败: {result.get('errmsg')}")
|
||
raise Exception(f"获取模板详情失败: {result.get('errmsg')}")
|
||
|
||
return result
|
||
|
||
|
||
async def submit_approval_api(
|
||
access_token: str,
|
||
template_id: str,
|
||
creator_userid: str,
|
||
contents: list[dict],
|
||
use_template_approver: int = 1
|
||
) -> dict:
|
||
"""提交审批申请
|
||
|
||
对应企微API:
|
||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent
|
||
|
||
Args:
|
||
access_token: 审批应用access_token
|
||
template_id: 模板ID
|
||
creator_userid: 申请人userid
|
||
contents: 表单控件内容列表
|
||
use_template_approver: 1-使用模板预设流程
|
||
|
||
Returns:
|
||
{"sp_no": "审批单号"}
|
||
"""
|
||
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent"
|
||
params = {"access_token": access_token}
|
||
|
||
payload = {
|
||
"creator_userid": creator_userid,
|
||
"template_id": template_id,
|
||
"use_template_approver": use_template_approver,
|
||
"apply_data": {
|
||
"contents": contents
|
||
}
|
||
}
|
||
|
||
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout=30.0, connect=10.0, read=30.0)) as client:
|
||
response = await client.post(url, params=params, json=payload)
|
||
result = response.json()
|
||
|
||
if result.get("errcode") != 0:
|
||
logger.error(f"提交审批失败: {result.get('errmsg')}")
|
||
raise Exception(f"提交审批失败: {result.get('errmsg')}")
|
||
|
||
return {"sp_no": result.get("sp_no")}
|
||
|
||
|
||
# =============================================================================
|
||
# 资产升级审批推送 — 辅助函数
|
||
# =============================================================================
|
||
|
||
async def get_approval_detail(access_token: str, sp_no: str) -> dict:
|
||
"""获取审批申请详情。
|
||
|
||
对应企微API:
|
||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail
|
||
|
||
返回审批单的表单数据、审批流程节点、当前审批人等信息。
|
||
|
||
Args:
|
||
access_token: 审批应用 access_token
|
||
sp_no: 审批单号
|
||
|
||
Returns:
|
||
dict: 企微API返回的完整审批详情
|
||
|
||
Raises:
|
||
Exception: API调用失败或返回错误
|
||
"""
|
||
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail"
|
||
params = {"access_token": access_token}
|
||
payload = {"sp_no": sp_no}
|
||
|
||
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout=30.0, connect=10.0, read=30.0)) as client:
|
||
response = await client.post(url, params=params, json=payload)
|
||
result = response.json()
|
||
|
||
if result.get("errcode") != 0:
|
||
logger.error(f"获取审批详情失败: sp_no={sp_no}, errmsg={result.get('errmsg')}")
|
||
raise Exception(f"获取审批详情失败: {result.get('errmsg')}")
|
||
|
||
return result
|
||
|
||
|
||
async def get_approval_data(
|
||
access_token: str,
|
||
starttime: int,
|
||
endtime: int,
|
||
filters: Optional[list] = None,
|
||
cursor: int = 0,
|
||
size: int = 100,
|
||
) -> dict:
|
||
"""获取审批申请数据列表。
|
||
|
||
对应企微API:
|
||
POST https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldata
|
||
|
||
按时间范围和过滤条件查询审批单列表,返回审批单号及基本信息。
|
||
支持 cursor 分页。
|
||
|
||
Args:
|
||
access_token: 审批应用 access_token
|
||
starttime: 查询开始时间(秒级 Unix 时间戳)
|
||
endtime: 查询结束时间(秒级 Unix 时间戳)
|
||
filters: 过滤条件列表,格式 [{"key": "template_id", "value": "xxx"}, ...]
|
||
cursor: 分页游标(从 0 开始)
|
||
size: 单页返回数量(最大 100)
|
||
|
||
Returns:
|
||
dict: 企微API返回的完整响应,包含 data(审批列表)、total、next_cursor
|
||
|
||
Raises:
|
||
Exception: API调用失败或返回错误
|
||
"""
|
||
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldata"
|
||
params = {"access_token": access_token}
|
||
payload = {
|
||
"starttime": starttime,
|
||
"endtime": endtime,
|
||
"cursor": cursor,
|
||
"size": size,
|
||
}
|
||
if filters:
|
||
payload["filters"] = filters
|
||
|
||
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout=30.0, connect=10.0, read=30.0)) as client:
|
||
response = await client.post(url, params=params, json=payload)
|
||
result = response.json()
|
||
|
||
if result.get("errcode") != 0:
|
||
logger.error(
|
||
f"getapprovaldata 调用失败: errcode={result.get('errcode')}, "
|
||
f"errmsg={result.get('errmsg')}"
|
||
)
|
||
raise Exception(f"获取审批数据列表失败: {result.get('errmsg')}")
|
||
|
||
return result
|
||
|
||
|
||
def _extract_asset_code(approval_detail: dict) -> Optional[str]:
|
||
"""从审批单表单数据中提取资产编号。
|
||
|
||
遍历 apply_data.contents,查找标题包含"资产编号"/"固定资产编号"等关键字的控件,
|
||
提取其值。支持 Text/Textarea(value.text)和 Selector(value.value)两种控件类型。
|
||
|
||
Args:
|
||
approval_detail: get_approval_detail 返回的完整审批详情
|
||
|
||
Returns:
|
||
str: 资产编号,提取失败返回 None
|
||
"""
|
||
try:
|
||
contents = (
|
||
approval_detail
|
||
.get("info", {})
|
||
.get("apply_data", {})
|
||
.get("contents", [])
|
||
)
|
||
# 资产编号可能的标题名
|
||
asset_code_titles = {"资产编号", "固定资产编号", "资产编码", "设备编号"}
|
||
|
||
for item in contents:
|
||
# 检查控件标题
|
||
title_list = item.get("title", [])
|
||
for title_obj in title_list:
|
||
title_text = title_obj.get("text", "")
|
||
if title_text in asset_code_titles:
|
||
# 提取控件值
|
||
value = item.get("value", {})
|
||
# Text / Textarea 类型:value.text
|
||
text = value.get("text", "")
|
||
if text:
|
||
return text.strip()
|
||
# Selector 类型:value.value
|
||
sel_value = value.get("value", "")
|
||
if sel_value:
|
||
return sel_value.strip()
|
||
except Exception as e:
|
||
logger.error(f"提取资产编号失败: {e}")
|
||
return None
|
||
|
||
|
||
def _extract_current_approver(approval_detail: dict) -> Optional[str]:
|
||
"""从审批单详情中提取当前审批人 userid。
|
||
|
||
遍历 sp_record,查找 sp_status=1(审批中)的节点,
|
||
返回其 details 中第一个 sp_status=1(待处理)的 approver.userid。
|
||
|
||
注意:企微 API 实际返回的字段名:
|
||
- sp_record[].sp_status(非 status)
|
||
- sp_record[].approverattr(非 approverAttr)
|
||
- sp_record[].details[].approver.userid(非 sp_record[].approver[].userid)
|
||
|
||
Args:
|
||
approval_detail: get_approval_detail 返回的完整审批详情
|
||
|
||
Returns:
|
||
str: 当前审批人 userid,提取失败返回 None
|
||
"""
|
||
try:
|
||
sp_record = approval_detail.get("info", {}).get("sp_record", [])
|
||
for record in sp_record:
|
||
# 企微 API 字段名为 sp_status(非 status),1=审批中
|
||
if record.get("sp_status") != 1:
|
||
continue
|
||
|
||
# 审批人信息在 details[].approver 中(非 record.approver)
|
||
details = record.get("details", [])
|
||
for detail in details:
|
||
# detail.sp_status=1 表示该审批人尚未处理
|
||
if detail.get("sp_status") == 1:
|
||
approver = detail.get("approver", {})
|
||
if isinstance(approver, dict):
|
||
userid = approver.get("userid", "")
|
||
if userid:
|
||
return userid
|
||
elif isinstance(approver, list) and len(approver) > 0:
|
||
userid = approver[0].get("userid", "")
|
||
if userid:
|
||
return userid
|
||
except Exception as e:
|
||
logger.error(f"提取当前审批人失败: {e}")
|
||
return None
|
||
|
||
|
||
def _build_urge_description(check_result: dict, applyer_userid: str) -> str:
|
||
"""构建推送卡片的描述文本。
|
||
|
||
根据资产核查结果拼接多行描述,包含资产编号、设备名称、启用日期、
|
||
已使用年限、核查结论、申请人等信息。
|
||
|
||
Args:
|
||
check_result: AssetService.check_device_age 返回的核查结果
|
||
applyer_userid: 申请人 userid
|
||
|
||
Returns:
|
||
str: 多行描述文本
|
||
"""
|
||
lines = []
|
||
|
||
# 资产编号
|
||
lines.append(f"资产编号:{check_result.get('asset_code', '未知')}")
|
||
|
||
# 设备名称
|
||
if check_result.get("asset_name"):
|
||
lines.append(f"设备名称:{check_result['asset_name']}")
|
||
|
||
# 启用日期
|
||
if check_result.get("start_date"):
|
||
lines.append(f"启用日期:{check_result['start_date']}")
|
||
|
||
# 已使用年限
|
||
if check_result.get("years_display"):
|
||
lines.append(f"已使用:{check_result['years_display']}")
|
||
|
||
# 核查结论
|
||
threshold = settings.asset_replacement_threshold_years
|
||
if check_result.get("found") and check_result.get("meets_threshold") is not None:
|
||
if check_result["meets_threshold"]:
|
||
lines.append(f"核查结论:✅ 已满{threshold}年,符合更换条件")
|
||
else:
|
||
lines.append(f"核查结论:❌ 未满{threshold}年,不符合更换条件")
|
||
elif not check_result.get("found"):
|
||
lines.append("核查结论:⚠️ 未找到资产记录,请人工核查")
|
||
else:
|
||
lines.append("核查结论:⚠️ 无法计算年限,请人工核查")
|
||
|
||
# 申请人
|
||
lines.append(f"申请人:{applyer_userid}")
|
||
|
||
# 提示
|
||
lines.append("请点击查看审批详情")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
async def _do_asset_urge(sp_no: str, redis: aioredis.Redis) -> dict:
|
||
"""执行资产年限核查 + 向审批人推送卡片消息的完整逻辑。
|
||
|
||
流程:
|
||
1. 获取审批应用 access_token
|
||
2. 调用 oa/getapprovaldetail 获取审批单详情
|
||
3. 从表单数据提取资产编号
|
||
4. 从审批流程提取当前审批人 userid
|
||
5. 用 AssetService 核查设备使用年限
|
||
6. 向当前审批人推送企微文本卡片消息
|
||
|
||
所有步骤均有 try/except 保护,失败时记日志但不抛出异常。
|
||
|
||
Args:
|
||
sp_no: 审批单号
|
||
redis: Redis 客户端(用于获取 access_token)
|
||
|
||
Returns:
|
||
dict: 执行结果,包含 success/message 以及核查详情
|
||
"""
|
||
try:
|
||
# 1. 获取审批应用 access_token
|
||
token = await get_approval_token(redis)
|
||
|
||
# 2. 获取审批单详情
|
||
detail = await get_approval_detail(token, sp_no)
|
||
|
||
# 3. 提取资产编号
|
||
asset_code = _extract_asset_code(detail)
|
||
if not asset_code:
|
||
logger.warning(f"审批单 {sp_no} 未能提取资产编号")
|
||
return {"success": False, "message": "未能从审批表单提取资产编号", "sp_no": sp_no}
|
||
|
||
# 4. 提取当前审批人
|
||
approver = _extract_current_approver(detail)
|
||
if not approver:
|
||
logger.warning(f"审批单 {sp_no} 未能提取当前审批人")
|
||
return {"success": False, "message": "未能提取当前审批人 userid", "sp_no": sp_no}
|
||
|
||
# 5. 提取申请人 userid
|
||
applyer_userid = (
|
||
detail.get("info", {}).get("applyer", {}).get("userid", "")
|
||
)
|
||
|
||
# 6. 核查资产年限
|
||
asset_service = AssetService()
|
||
threshold = settings.asset_replacement_threshold_years
|
||
check_result = asset_service.check_device_age(asset_code, threshold)
|
||
|
||
# 7. 构建并推送卡片消息
|
||
description = _build_urge_description(check_result, applyer_userid)
|
||
card_url = (
|
||
f"https://app.work.weixin.qq.com/wework_admin/approval_v3"
|
||
f"#/?template_id={ASSET_UPGRADE_TEMPLATE_ID}&sp_id={sp_no}"
|
||
)
|
||
|
||
# 使用 IT 支持应用的 access_token 发送消息(非审批应用 token)
|
||
wecom_service = WecomService(redis_client=redis)
|
||
await wecom_service.send_card_message(
|
||
user_id=approver,
|
||
title="IT资产升级审批 — 设备年限核查",
|
||
description=description,
|
||
url=card_url,
|
||
btntxt="去审批",
|
||
)
|
||
|
||
logger.info(f"资产年限核查推送成功: sp_no={sp_no}, approver={approver}")
|
||
return {
|
||
"success": True,
|
||
"message": "推送成功",
|
||
"sp_no": sp_no,
|
||
"approver": approver,
|
||
"asset_code": asset_code,
|
||
"check_result": check_result,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"资产年限核查推送失败: sp_no={sp_no}, error={e}")
|
||
return {"success": False, "message": f"推送失败: {e}", "sp_no": sp_no}
|
||
|
||
|
||
# =============================================================================
|
||
# API 端点
|
||
# =============================================================================
|
||
|
||
@router.get("/approval/templates", response_model=list[ApprovalTemplateResponse])
|
||
async def get_approval_templates():
|
||
"""获取所有审批模板列表"""
|
||
return list(APPROVAL_TEMPLATES.values())
|
||
|
||
|
||
@router.get("/approval/templates/{template_id}", response_model=ApprovalTemplateResponse)
|
||
async def get_approval_template(template_id: str):
|
||
"""获取指定审批模板详情"""
|
||
if template_id not in APPROVAL_TEMPLATES:
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=404, detail="模板不存在")
|
||
return APPROVAL_TEMPLATES[template_id]
|
||
|
||
|
||
@router.get("/approval/templates/{template_id}/detail")
|
||
async def get_template_full_detail(
|
||
template_id: str,
|
||
redis: aioredis.Redis = Depends(get_redis)
|
||
):
|
||
"""获取审批模板完整详情(控件结构)"""
|
||
if template_id not in APPROVAL_TEMPLATES:
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=404, detail="模板不存在")
|
||
|
||
try:
|
||
token = await get_approval_token(redis)
|
||
detail = await get_template_detail(token, template_id)
|
||
return detail
|
||
except Exception as e:
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/approval/jump")
|
||
async def create_approval_jump(request: ApprovalJumpRequest):
|
||
"""生成跳转审批链接"""
|
||
template = APPROVAL_TEMPLATES.get(request.template_id)
|
||
if not template:
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=404, detail="模板不存在")
|
||
|
||
if template["type"] != "jump":
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=400, detail="该模板不支持跳转方式")
|
||
|
||
# 生成跳转URL(企微审批链接格式)
|
||
jump_url = f"https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=TOKEN&template_id={request.template_id}"
|
||
|
||
return success_response(data=ApprovalJumpResponse(
|
||
url=jump_url,
|
||
template_name=template["name"],
|
||
))
|
||
|
||
|
||
@router.post("/approval/submit", response_model=ApprovalSubmitResponse)
|
||
async def submit_approval(
|
||
request: ApprovalSubmitRequest,
|
||
redis: aioredis.Redis = Depends(get_redis)
|
||
):
|
||
"""API提交审批申请"""
|
||
template = APPROVAL_TEMPLATES.get(request.template_id)
|
||
if not template:
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=404, detail="模板不存在")
|
||
|
||
if template["type"] != "api":
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=400, detail="该模板不支持API提交")
|
||
|
||
try:
|
||
# 1. 获取审批token
|
||
token = await get_approval_token(redis)
|
||
|
||
# 2. 转换contents格式
|
||
contents = [item.model_dump() for item in request.contents]
|
||
|
||
# 3. 提交审批
|
||
result = await submit_approval_api(
|
||
access_token=token,
|
||
template_id=request.template_id,
|
||
creator_userid=request.employee_id,
|
||
contents=contents,
|
||
use_template_approver=request.use_template_approver
|
||
)
|
||
|
||
return ApprovalSubmitResponse(
|
||
sp_no=result["sp_no"],
|
||
template_name=template["name"],
|
||
)
|
||
|
||
except Exception as e:
|
||
from fastapi import HTTPException
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/approval/callback")
|
||
async def approval_callback(
|
||
sp_no: str = Query(...),
|
||
sp_name: str = Query(...),
|
||
template_id: str = Query(...),
|
||
apply_time: int = Query(...),
|
||
applyer_userid: str = Query(...),
|
||
sp_status: int = Query(...),
|
||
status_change_event: int = Query(...),
|
||
redis: aioredis.Redis = Depends(get_redis),
|
||
):
|
||
"""审批状态变化回调处理
|
||
|
||
对应企微审批回调事件: sys_approval_change
|
||
|
||
状态变化类型 (status_change_event):
|
||
1 - 提单
|
||
2 - 同意
|
||
3 - 驳回
|
||
4 - 转审
|
||
5 - 催办
|
||
6 - 撤销
|
||
8 - 通过后撤销
|
||
10 - 添加备注
|
||
|
||
审批单状态 (sp_status):
|
||
1 - 审批中
|
||
2 - 已通过
|
||
3 - 已驳回
|
||
4 - 已撤销
|
||
6 - 通过后撤销
|
||
7 - 已删除
|
||
10 - 已支付
|
||
"""
|
||
logger.info(f"审批回调: sp_no={sp_no}, status={sp_status}, event={status_change_event}")
|
||
|
||
# TODO: 根据业务需求处理审批状态变化
|
||
# 例如:
|
||
# - 审批通过后,更新IT服务台待办状态
|
||
# - 审批驳回后,通知申请人
|
||
# - 审批撤销后,关闭相关工单
|
||
|
||
event_map = {
|
||
1: "submitted",
|
||
2: "approved",
|
||
3: "rejected",
|
||
4: "transferred",
|
||
5: "reminded",
|
||
6: "revoked",
|
||
8: "revoked_after_approved",
|
||
10: "commented"
|
||
}
|
||
|
||
event_type = event_map.get(status_change_event, f"unknown_{status_change_event}")
|
||
logger.info(f"审批事件类型: {event_type}")
|
||
|
||
# IT资产升级申请提单时,自动触发年限核查+推送(异步执行,不阻塞回调响应)
|
||
if status_change_event == 1 and template_id == ASSET_UPGRADE_TEMPLATE_ID:
|
||
logger.info(f"检测到IT资产升级申请提单: sp_no={sp_no}")
|
||
asyncio.create_task(_do_asset_urge(sp_no, redis))
|
||
|
||
return {"errcode": 0, "errmsg": "ok"}
|
||
|
||
|
||
@router.post("/approval/urge")
|
||
async def urge_approval(
|
||
request: ApprovalUrgeRequest,
|
||
redis: aioredis.Redis = Depends(get_redis),
|
||
):
|
||
"""手动触发审批推送 — 资产年限核查 + 向当前审批人推送卡片消息。
|
||
|
||
用于回调未触发或需重新推送的场景。内部调用 _do_asset_urge 执行完整逻辑。
|
||
|
||
Args:
|
||
request: 包含 sp_no(审批单号)
|
||
redis: Redis 客户端
|
||
|
||
Returns:
|
||
success_response: 包含推送结果和核查详情
|
||
"""
|
||
result = await _do_asset_urge(request.sp_no, redis)
|
||
return success_response(data=result)
|
||
|
||
|
||
@router.get("/approval/keywords")
|
||
async def get_approval_keywords():
|
||
"""获取所有审批关键词(用于前端关键词检测)"""
|
||
keywords = []
|
||
for template in APPROVAL_TEMPLATES.values():
|
||
for kw in template["keywords"]:
|
||
keywords.append({
|
||
"keyword": kw,
|
||
"template_id": template["id"],
|
||
"template_name": template["name"],
|
||
"type": template["type"],
|
||
})
|
||
return success_response(data=keywords)
|
||
|
||
|
||
# =============================================================================
|
||
# 审批意图识别(Dify + 关键词预过滤 + 降级兜底)
|
||
# =============================================================================
|
||
|
||
def _keyword_prefilter(text: str) -> bool:
|
||
"""关键词预过滤:检查文本是否包含审批相关关键词(v2.0 收窄版)。
|
||
|
||
v2.0 变更(2026-07-13):
|
||
- 不再合并 APPROVAL_TEMPLATES 的 keywords(包含"借用""升级""外联"等泛化词)
|
||
- 仅使用 APPROVAL_PREFILTER_KEYWORDS(强意图词 + 复合专有词)
|
||
- 模板 keywords 仍保留在 KEYWORD_TO_APPROVAL_TYPE 中,仅用于 Dify 不可用时的降级兜底
|
||
|
||
Args:
|
||
text: 用户消息文本
|
||
|
||
Returns:
|
||
bool: 是否包含审批关键词
|
||
"""
|
||
if not text:
|
||
return False
|
||
lower_text = text.lower()
|
||
# v2.0: 仅使用预过滤关键词列表,不合并模板 keywords
|
||
return any(kw.lower() in lower_text for kw in APPROVAL_PREFILTER_KEYWORDS)
|
||
|
||
|
||
def _fallback_detect(text: str) -> tuple[bool, float, Optional[str]]:
|
||
"""关键词兜底:Dify 不可用时通过关键词匹配判断审批意图。
|
||
|
||
遍历 KEYWORD_TO_APPROVAL_TYPE 映射,命中第一个关键词即返回对应审批类型。
|
||
置信度取 0.6(略低于阈值,但预过滤已通过说明有审批关键词)。
|
||
|
||
Args:
|
||
text: 用户消息文本
|
||
|
||
Returns:
|
||
tuple: (is_approval_request, confidence, approval_type)
|
||
"""
|
||
lower_text = (text or "").lower()
|
||
approval_type: Optional[str] = None
|
||
for kw, atype in KEYWORD_TO_APPROVAL_TYPE.items():
|
||
if kw.lower() in lower_text:
|
||
approval_type = atype
|
||
break
|
||
# 预过滤已通过(说明有审批关键词),兜底返回 is_approval_request=True
|
||
return True, 0.6, approval_type
|
||
|
||
|
||
async def _call_dify_approval_intent(text: str, employee_id: str = "") -> dict:
|
||
"""调用 Dify 审批意图识别应用(Dify 原生 API)。
|
||
|
||
直接调用 Dify 原生 /v1/chat-messages 接口,绕过 Dify2OpenAI 代理。
|
||
Dify2OpenAI 代理会将 JSON 响应序列化为 "[object Object]" 字符串,
|
||
导致后端无法解析。使用原生 API 可获得正确的 JSON 响应。
|
||
|
||
Dify 应用的 System Prompt 已在 Dify 后台配置好,后端只需把用户消息传过去。
|
||
返回 JSON: {"is_approval_request": bool, "confidence": float, "approval_type": str|null}
|
||
|
||
Args:
|
||
text: 用户消息文本
|
||
employee_id: 员工 ID(可选,传给 Dify 的 user 字段)
|
||
|
||
Returns:
|
||
dict: {"is_approval_request": bool, "confidence": float, "approval_type": str|None}
|
||
|
||
Raises:
|
||
Exception: Dify 调用失败或响应解析失败
|
||
"""
|
||
base_url = settings.approval_dify_base_url
|
||
api_key = settings.approval_dify_api_key
|
||
timeout = settings.approval_dify_timeout
|
||
|
||
if not base_url or not api_key:
|
||
raise ValueError("Dify 审批意图识别应用未配置(APPROVAL_DIFY_BASE_URL / APPROVAL_DIFY_API_KEY)")
|
||
|
||
# 构建请求 URL:base_url + /v1/chat-messages(Dify 原生 API)
|
||
url = f"{base_url.rstrip('/')}/v1/chat-messages"
|
||
|
||
body = {
|
||
"inputs": {}, # Dify 应用的输入变量(无自定义变量时为空)
|
||
"query": text, # 用户消息文本
|
||
"response_mode": "blocking", # 阻塞模式,等待完整响应
|
||
"user": employee_id or "approval_detection", # 用户标识
|
||
}
|
||
headers = {
|
||
"Authorization": f"Bearer {api_key}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
async with httpx.AsyncClient(timeout=httpx.Timeout(timeout)) as client:
|
||
response = await client.post(url, json=body, headers=headers)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
# 解析 Dify 原生响应:answer 字段包含 AI 返回的文本(JSON 字符串)
|
||
answer = data.get("answer", "")
|
||
parsed = json.loads(answer)
|
||
|
||
return {
|
||
"is_approval_request": bool(parsed.get("is_approval_request", False)),
|
||
"confidence": float(parsed.get("confidence", 0.0)),
|
||
"approval_type": parsed.get("approval_type"),
|
||
# v3 统一意图识别新增字段(向后兼容:旧 Prompt 无这些字段时取默认值)
|
||
"intent_type": str(parsed.get("intent_type", "chitchat")),
|
||
"business_category": parsed.get("business_category"),
|
||
"routing_confidence": float(parsed.get("routing_confidence", 0.0)),
|
||
}
|
||
|
||
|
||
@router.post("/approval/detect-intent")
|
||
async def detect_approval_intent(request: ApprovalDetectIntentRequest):
|
||
"""审批意图检测端点。
|
||
|
||
流程:
|
||
1. 关键词预过滤 — 未命中直接返回 false(避免每条消息都调 Dify)
|
||
2. 命中关键词 → 调用 Dify 审批意图识别应用
|
||
3. Dify 调用失败 → 降级为关键词匹配(兜底)
|
||
|
||
Args:
|
||
request: 包含 text(用户消息)和可选的 employee_id
|
||
|
||
Returns:
|
||
ApprovalDetectIntentResponse: 检测结果
|
||
"""
|
||
text = request.text or ""
|
||
|
||
# 1. 关键词预过滤
|
||
if not _keyword_prefilter(text):
|
||
return success_response(data=ApprovalDetectIntentResponse(
|
||
is_approval_request=False,
|
||
confidence=0.0,
|
||
approval_type=None,
|
||
source="keyword_prefilter",
|
||
intent_type="chitchat",
|
||
business_category=None,
|
||
routing_confidence=0.0,
|
||
))
|
||
|
||
# 2. 调用 Dify 意图识别
|
||
try:
|
||
result = await _call_dify_approval_intent(text, request.employee_id or "")
|
||
# 检查置信度阈值
|
||
threshold = settings.approval_confidence_threshold
|
||
is_approval = result["is_approval_request"] and result["confidence"] >= threshold
|
||
logger.info(
|
||
f"审批意图检测(Dify): is_approval={is_approval}, "
|
||
f"confidence={result['confidence']}, type={result.get('approval_type')}, "
|
||
f"intent_type={result.get('intent_type')}, "
|
||
f"business_category={result.get('business_category')}, "
|
||
f"routing_confidence={result.get('routing_confidence')}"
|
||
)
|
||
return success_response(data=ApprovalDetectIntentResponse(
|
||
is_approval_request=is_approval,
|
||
confidence=result["confidence"],
|
||
approval_type=result.get("approval_type"),
|
||
source="dify",
|
||
intent_type=result.get("intent_type", "chitchat"),
|
||
business_category=result.get("business_category"),
|
||
routing_confidence=result.get("routing_confidence", 0.0),
|
||
))
|
||
except Exception as e:
|
||
logger.warning(f"Dify 审批意图识别失败,降级为关键词匹配: {e}")
|
||
# 3. 降级为关键词匹配
|
||
is_approval, confidence, approval_type = _fallback_detect(text)
|
||
logger.info(
|
||
f"审批意图检测(兜底): is_approval={is_approval}, "
|
||
f"confidence={confidence}, type={approval_type}"
|
||
)
|
||
return success_response(data=ApprovalDetectIntentResponse(
|
||
is_approval_request=is_approval,
|
||
confidence=confidence,
|
||
approval_type=approval_type,
|
||
source="fallback",
|
||
intent_type="approval" if is_approval else "chitchat",
|
||
business_category=None,
|
||
routing_confidence=0.0,
|
||
))
|