Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
企业微信通讯录 API 客户端
|
||||
封装企业微信开放平台部门和成员相关接口
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WECOM_API_BASE = "https://qyapi.weixin.qq.com/cgi-bin"
|
||||
|
||||
|
||||
class WecomClient:
|
||||
"""企业微信通讯录 API 客户端"""
|
||||
|
||||
def __init__(self, corp_id: str, corp_secret: str):
|
||||
self.corp_id = corp_id
|
||||
self.corp_secret = corp_secret
|
||||
self._access_token: Optional[str] = None
|
||||
self._token_expires_at: float = 0
|
||||
|
||||
async def get_access_token(self) -> str:
|
||||
"""获取 access_token(带内存缓存,提前 5 分钟过期)"""
|
||||
now = time.time()
|
||||
if self._access_token and now < self._token_expires_at:
|
||||
return self._access_token
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
f"{WECOM_API_BASE}/gettoken",
|
||||
params={"corpid": self.corp_id, "corpsecret": self.corp_secret},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
|
||||
if result.get("errcode") != 0:
|
||||
raise Exception(f"获取企业微信 access_token 失败: {result.get('errmsg', result)}")
|
||||
|
||||
self._access_token = result["access_token"]
|
||||
self._token_expires_at = now + result.get("expires_in", 7200) - 300
|
||||
return self._access_token
|
||||
|
||||
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""带 access_token 的 GET 请求"""
|
||||
token = await self.get_access_token()
|
||||
all_params = {"access_token": token}
|
||||
if params:
|
||||
all_params.update(params)
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(f"{WECOM_API_BASE}{path}", params=all_params)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
if result.get("errcode") != 0:
|
||||
raise Exception(f"企业微信 API 调用失败 [{path}]: {result.get('errmsg', result)}")
|
||||
return result
|
||||
|
||||
async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""带 access_token 的 POST 请求"""
|
||||
token = await self.get_access_token()
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(
|
||||
f"{WECOM_API_BASE}{path}",
|
||||
params={"access_token": token},
|
||||
json=body,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
if result.get("errcode") != 0:
|
||||
raise Exception(f"企业微信 API 调用失败 [{path}]: {result.get('errmsg', result)}")
|
||||
return result
|
||||
|
||||
# ==================== 连接测试 ====================
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""测试连接:获取 token + 拉取根部门信息验证"""
|
||||
await self.get_access_token()
|
||||
result = await self._get("/department/list", {"id": 1})
|
||||
dept_list = result.get("department", [])
|
||||
root_name = ""
|
||||
for d in dept_list:
|
||||
if d.get("id") == 1:
|
||||
root_name = d.get("name", "")
|
||||
break
|
||||
return {
|
||||
"success": True,
|
||||
"corp_name": root_name,
|
||||
"dept_count": len(dept_list),
|
||||
}
|
||||
|
||||
# ==================== 部门 API ====================
|
||||
|
||||
async def get_dept_list(self, dept_id: int = 1) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取部门列表
|
||||
https://developer.work.weixin.qq.com/document/path/90208
|
||||
"""
|
||||
result = await self._get("/department/list", {"id": dept_id})
|
||||
return result.get("department", [])
|
||||
|
||||
async def get_dept_detail(self, dept_id: int) -> Dict[str, Any]:
|
||||
"""获取单个部门详情"""
|
||||
result = await self._get("/department/get", {"id": dept_id})
|
||||
return result.get("department", {})
|
||||
|
||||
async def get_all_depts(self, root_dept_id: int = 1) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取指定根部门下的全量部门列表
|
||||
企业微信 /department/list 返回指定部门及所有子部门(递归),结果扁平
|
||||
"""
|
||||
all_depts = await self.get_dept_list(root_dept_id)
|
||||
return [d for d in all_depts if d.get("id") != root_dept_id]
|
||||
|
||||
async def get_dept_tree(self, root_dept_id: int = 1) -> List[Dict[str, Any]]:
|
||||
"""获取部门树形结构(供前端选择同步范围)"""
|
||||
all_depts = await self.get_dept_list(root_dept_id)
|
||||
|
||||
dept_map: Dict[int, Dict[str, Any]] = {}
|
||||
for d in all_depts:
|
||||
dept_map[d["id"]] = {
|
||||
"dept_id": d["id"],
|
||||
"name": d.get("name", ""),
|
||||
"parentid": d.get("parentid", 0),
|
||||
"children": [],
|
||||
}
|
||||
|
||||
tree: List[Dict[str, Any]] = []
|
||||
for d in dept_map.values():
|
||||
parent_id = d["parentid"]
|
||||
if parent_id in dept_map and parent_id != d["dept_id"]:
|
||||
dept_map[parent_id]["children"].append(d)
|
||||
elif d["dept_id"] == root_dept_id:
|
||||
tree.append(d)
|
||||
else:
|
||||
tree.append(d)
|
||||
|
||||
return tree
|
||||
|
||||
# ==================== 用户/成员 API ====================
|
||||
|
||||
async def get_user_list(self, dept_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取部门成员详情列表
|
||||
https://developer.work.weixin.qq.com/document/path/90201
|
||||
"""
|
||||
result = await self._get("/user/list", {"department_id": dept_id})
|
||||
return result.get("userlist", [])
|
||||
|
||||
async def get_user_detail(self, userid: str) -> Dict[str, Any]:
|
||||
"""获取成员详情"""
|
||||
return await self._get("/user/get", {"userid": userid})
|
||||
|
||||
# ==================== 回调注册 API ====================
|
||||
|
||||
async def create_callback(
|
||||
self,
|
||||
callback_url: str,
|
||||
callback_tag: List[str],
|
||||
token: str,
|
||||
aes_key: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
注册事件回调
|
||||
https://developer.work.weixin.qq.com/document/path/90930
|
||||
"""
|
||||
# 企业微信的回调设置不是通过API注册的,而是在管理后台配置
|
||||
# 这里提供一个验证URL有效性的接口(回调模式验证由 callback 端点的 GET 处理)
|
||||
return {"errcode": 0, "errmsg": "ok"}
|
||||
|
||||
async def delete_callback(self) -> Dict[str, Any]:
|
||||
"""企业微信回调通过管理后台管理,这里仅做标记"""
|
||||
return {"errcode": 0, "errmsg": "ok"}
|
||||
|
||||
async def get_callback(self) -> Dict[str, Any]:
|
||||
"""查询回调设置状态"""
|
||||
return {"errcode": 0, "errmsg": "ok"}
|
||||
Reference in New Issue
Block a user