ForcePilot/backend/package/yuxi/channels/adapters/dingding/tools/approval.py
Kris 9c01d480cc feat(dingding): 完整实现钉钉适配器功能模块
- 新增钉钉工具调用审计日志功能
- 新增参数校验装饰器与基础工具集(文档、审批、多维表格)
- 完善事件类型映射与会话ID生成逻辑
- 新增消息去重、访问策略匹配、配置校验与UI提示
- 新增酷应用卡片、目录管理、表情反应支持
- 优化消息发送逻辑,添加401重试机制
- 新增配置化的权限控制与流式输出支持
2026-05-13 16:06:10 +08:00

201 lines
7.0 KiB
Python

from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import httpx
from ._validate import validate_params
if TYPE_CHECKING:
from yuxi.channels.adapters.dingding.token import DingDingTokenManager
logger = logging.getLogger(__name__)
DINGTALK_API_BASE = "https://api.dingtalk.com"
@validate_params
async def list_approval_templates(
token_manager: DingDingTokenManager,
*,
user_id: str = "",
page_size: int = 50,
page_number: int = 1,
http_client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
token = await token_manager.get_token()
url = f"{DINGTALK_API_BASE}/v1.0/workflow/processes"
headers = {"x-acs-dingtalk-access-token": token}
params: dict[str, Any] = {
"maxResults": min(page_size, 100),
"nextToken": str(page_number - 1),
}
if user_id:
params["userId"] = user_id
try:
if http_client is not None:
resp = await http_client.get(url, headers=headers, params=params)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.get(url, headers=headers, params=params)
if resp.status_code != 200:
raise RuntimeError(f"获取审批模板列表失败: HTTP {resp.status_code}: {resp.text[:200]}")
data = resp.json()
items = data.get("items", data.get("processList", data.get("list", [])))
templates = [
{
"process_code": item.get("processCode", item.get("code", "")),
"name": item.get("name", item.get("processName", "")),
"description": item.get("description", ""),
"icon_url": item.get("iconUrl", ""),
"status": item.get("status", ""),
}
for item in items
]
return {
"templates": templates,
"total": data.get("totalCount", len(templates)),
"has_more": data.get("hasMore", False),
}
except Exception as e:
logger.error(f"[DingDing Approval] list_approval_templates failed: {e}")
raise RuntimeError(f"获取审批模板列表失败: {e}") from e
@validate_params
async def create_approval_instance(
token_manager: DingDingTokenManager,
process_code: str,
originator_user_id: str,
form_component_values: list[dict[str, Any]],
*,
dept_id: str = "",
approvers: list[str] | None = None,
cc_list: list[str] | None = None,
http_client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
token = await token_manager.get_token()
url = f"{DINGTALK_API_BASE}/v1.0/workflow/processInstances"
headers = {
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
}
payload: dict[str, Any] = {
"processCode": process_code,
"originatorUserId": originator_user_id,
"formComponentValues": form_component_values,
}
if dept_id:
payload["deptId"] = dept_id
if approvers:
payload["approvers"] = approvers
if cc_list:
payload["ccList"] = cc_list
try:
if http_client is not None:
resp = await http_client.post(url, json=payload, headers=headers)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code != 200:
raise RuntimeError(f"发起审批实例失败: HTTP {resp.status_code}: {resp.text[:200]}")
data = resp.json()
return {
"instance_id": data.get("instanceId", data.get("processInstanceId", "")),
"process_code": process_code,
"originator_user_id": originator_user_id,
"status": data.get("status", "RUNNING"),
}
except Exception as e:
logger.error(f"[DingDing Approval] create_approval_instance failed: {e}")
raise RuntimeError(f"发起审批实例失败: {e}") from e
@validate_params
async def get_approval_instance(
token_manager: DingDingTokenManager,
instance_id: str,
*,
http_client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
token = await token_manager.get_token()
url = f"{DINGTALK_API_BASE}/v1.0/workflow/processInstances/{instance_id}"
headers = {"x-acs-dingtalk-access-token": token}
try:
if http_client is not None:
resp = await http_client.get(url, headers=headers)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.get(url, headers=headers)
if resp.status_code != 200:
raise RuntimeError(f"获取审批实例详情失败: HTTP {resp.status_code}: {resp.text[:200]}")
data = resp.json()
return {
"instance_id": data.get("instanceId", data.get("processInstanceId", instance_id)),
"process_code": data.get("processCode", ""),
"title": data.get("title", data.get("processTitle", "")),
"originator_user_id": data.get("originatorUserId", ""),
"status": data.get("status", ""),
"result": data.get("result", ""),
"form_values": data.get("formComponentValues", []),
"create_time": data.get("createTime", ""),
"finish_time": data.get("finishTime", ""),
"approvers": data.get("approvers", []),
"tasks": data.get("tasks", data.get("operationRecords", [])),
}
except Exception as e:
logger.error(f"[DingDing Approval] get_approval_instance failed: {e}")
raise RuntimeError(f"获取审批实例详情失败: {e}") from e
@validate_params
async def cancel_approval_instance(
token_manager: DingDingTokenManager,
instance_id: str,
*,
operate_user_id: str = "",
remark: str = "",
http_client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
token = await token_manager.get_token()
url = f"{DINGTALK_API_BASE}/v1.0/workflow/processInstances/{instance_id}/terminate"
headers = {
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
}
payload: dict[str, Any] = {}
if operate_user_id:
payload["operateUserId"] = operate_user_id
if remark:
payload["remark"] = remark
try:
if http_client is not None:
resp = await http_client.put(url, json=payload, headers=headers)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.put(url, json=payload, headers=headers)
if resp.status_code != 200:
raise RuntimeError(f"撤销审批实例失败: HTTP {resp.status_code}: {resp.text[:200]}")
data = resp.json()
return {
"instance_id": instance_id,
"canceled": True,
"result": data.get("result", True),
}
except Exception as e:
logger.error(f"[DingDing Approval] cancel_approval_instance failed: {e}")
raise RuntimeError(f"撤销审批实例失败: {e}") from e