v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化

This commit is contained in:
Simon
2026-07-17 23:08:59 +08:00
parent 5a77a89ab1
commit 3ed86d5fb3
181 changed files with 19738 additions and 2655 deletions
+36 -63
View File
@@ -24,7 +24,6 @@ import json
import logging
import re
import secrets
from collections import OrderedDict
from datetime import datetime
from typing import Any, Dict, List, Optional
from urllib.parse import quote
@@ -65,7 +64,11 @@ from app.tasks.h5_ai_task import process_h5_ai_reply
from app.services.funny_phrase_service import FunnyPhraseService
from app.services.ws_manager import manager as ws_manager
from app.services.wecom_service import WecomService
from app.services.employee_directory import get_org_directory
from app.services.employee_directory import (
count_tree_employees,
get_org_directory,
get_org_tree_cached,
)
from app.utils.response import AppException, ERR_UNAUTHORIZED, success_response
from app.services.closing_service import ClosingService
from pydantic import BaseModel, Field
@@ -919,7 +922,12 @@ async def h5_send_message(
# WS 广播失败不阻塞消息存储,只记录 warning
logger.warning(f"WS 广播用户消息失败(消息已存储): {ws_err}")
# 4. 启动后台 AI 任务(异步,不阻塞 HTTP 返回)
# 4. 提交当前事务,确保后台任务能读到刚创建的 conversation/message
# 为什么:asyncio.create_task 立即运行,若 HTTP 事务未提交,
# 后台 DB session 会报"会话不存在"race condition
await db.commit()
# 5. 启动后台 AI 任务(异步,不阻塞 HTTP 返回)
# 为什么:AI 推理(Dify)慢(3~15s),放后台经 WS 流式推回,
# 发送接口瞬时返回,前端不再卡"发送中"
# 约束:后台任务使用独立 DB session,且需单 worker(见 h5_ai_task.py
@@ -1734,7 +1742,7 @@ async def h5_search_employees(
return success_response(data=[])
try:
# 获取组织目录(含 10 分钟 Redis 缓存 + 本地降级)
# 获取组织目录(含 30 分钟 Redis 缓存 + 本地降级)
directory, _ = await get_org_directory(db, redis)
kw_lower = kw.lower()
@@ -1774,24 +1782,34 @@ async def h5_get_org_tree(
):
"""H5 员工端获取组织架构树(部门层级 + 每个部门下的员工列表)。
复用 get_org_directory() 获取员工列表(已含 department 字段
在服务端按 department 分组构建树结构。排除当前登录员工自己。
利用企微 department/list 返回的 parentid 字段构建真正的层级树
不再按部门名扁平分组。部门ID加 ``dept_`` 前缀作为唯一 key
避免同名部门合并。员工可以出现在其所属的所有部门下。
树结构示例:
树结构示例(多层级)
[
{
"id": "研发一部",
"label": "研发一部",
"id": "dept_1",
"label": "公司",
"dept_id": 1,
"parentid": 0,
"children": [
{"id": "zhangsan", "label": "张三", "isLeaf": true, "department": "研发一部"}
{
"id": "dept_2",
"label": "研发一部",
"dept_id": 2,
"parentid": 1,
"children": [
{"id": "zhangsan", "label": "张三", "isLeaf": true, "department": "研发一部"}
]
}
]
}
]
规则
- department 为空的员工归到"未分配部门"分组
- 企微返回多部门(逗号分隔)时,取第一个作为主部门
- 部门按名称排序,部门内员工按姓名排序
性能优化
- 树构建结果独立缓存(key: wecom:org_tree:h5TTL 30 分钟)
- 缓存中包含所有员工,读取后过滤掉当前登录员工自己
Args:
employee_id: 当前登录员工ID
@@ -1802,56 +1820,11 @@ async def h5_get_org_tree(
Dict: 统一响应格式,data 为树节点列表
"""
try:
# 获取组织目录(含 Redis 缓存 + 本地降级
directory, _ = await get_org_directory(db, redis)
# 获取组织架构树(含独立缓存 + 排除当前员工
tree = await get_org_tree_cached(db, redis, "h5", employee_id)
# 按部门分组(OrderedDict 保持稳定插入顺序,后续再排序)
dept_groups: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
for emp in directory:
# 排除当前员工自己
if emp.get("employee_id") == employee_id:
continue
# 取部门名:为空则归"未分配部门";多部门(逗号分隔)取第一个
dept = (emp.get("department") or "").strip()
if not dept:
dept = "未分配部门"
else:
dept = dept.split(",")[0].strip()
if not dept:
dept = "未分配部门"
if dept not in dept_groups:
dept_groups[dept] = []
dept_groups[dept].append(emp)
# 构建树节点(部门按名称排序,员工按姓名排序)
tree: List[Dict[str, Any]] = []
for dept_name in sorted(dept_groups.keys()):
employees = dept_groups[dept_name]
if not employees:
continue
# 部门内员工按姓名排序
employees.sort(key=lambda e: e.get("name", ""))
tree.append({
"id": dept_name,
"label": dept_name,
"children": [
{
"id": emp.get("employee_id", ""),
"label": emp.get("name", ""),
"isLeaf": True,
"department": dept_name,
}
for emp in employees
],
})
total_employees = sum(len(node["children"]) for node in tree)
logger.info(f"H5组织架构树: {len(tree)} 个部门, 共 {total_employees}")
total_employees = count_tree_employees(tree)
logger.info(f"H5组织架构树: {len(tree)} 个顶层节点, 共 {total_employees}")
return success_response(data=tree)
except AppException: