Merge commit '43274d577ab49b8e0e5cdac3ee00c1ab989cae56' into dev-wecom
This commit is contained in:
@@ -4,6 +4,7 @@ from pydantic_settings import BaseSettings,SettingsConfigDict
|
|||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env" , env_prefix="WNZS_")
|
model_config = SettingsConfigDict(env_file=".env" , env_prefix="WNZS_")
|
||||||
PGSQL: str = ""
|
PGSQL: str = ""
|
||||||
|
WECOM_PROXY: str = ""
|
||||||
WECOM_CORPID: str = ""
|
WECOM_CORPID: str = ""
|
||||||
WECOM_CORPSECRET: str = ""
|
WECOM_CORPSECRET: str = ""
|
||||||
WECOM_APP_TOKEN: str = ""
|
WECOM_APP_TOKEN: str = ""
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ def import_router(app: FastAPI):
|
|||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
logger.info(f"[导入路由] 路由导入完成 ✅")
|
logger.info(f"[导入路由] 路由导入完成 ✅")
|
||||||
|
|
||||||
|
async def import_mcp_server(app: FastAPI):
|
||||||
|
logger.info(f"[导入MCP] 开始导入MCP 🛣️")
|
||||||
|
from mcps import create_mcp_app
|
||||||
|
app.mount("/app" , await create_mcp_app())
|
||||||
|
logger.info(f"[导入MCP] MCP导入完成 ✅")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
@@ -38,5 +42,6 @@ async def lifespan(app: FastAPI):
|
|||||||
init_database()
|
init_database()
|
||||||
import_router(app)
|
import_router(app)
|
||||||
init_scheduler(app)
|
init_scheduler(app)
|
||||||
|
await import_mcp_server(app)
|
||||||
yield
|
yield
|
||||||
logger.info(f"[生命周期] 应用关闭 🔧✅")
|
logger.info(f"[生命周期] 应用关闭 🔧✅")
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -2,9 +2,11 @@ from fastapi import FastAPI
|
|||||||
|
|
||||||
from handler.exception import install as exception_install
|
from handler.exception import install as exception_install
|
||||||
from lifespan import lifespan
|
from lifespan import lifespan
|
||||||
|
from mcps import create_mcp_app
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
exception_install(app)
|
exception_install(app)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
8
mcps/__init__.py
Normal file
8
mcps/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from fastmcp import FastMCP
|
||||||
|
from mcps.test.test import weather_mcp
|
||||||
|
|
||||||
|
|
||||||
|
async def create_mcp_app():
|
||||||
|
main_mcp = FastMCP("MCP 主服务")
|
||||||
|
await main_mcp.import_server(weather_mcp , prefix="test")
|
||||||
|
return main_mcp.http_app()
|
||||||
3
mcps/test/test.py
Normal file
3
mcps/test/test.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
weather_mcp = FastMCP(name="WeatherService")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from sqlmodel import SQLModel, Field, Column, JSON
|
||||||
|
|
||||||
|
|
||||||
|
class Department(SQLModel, table = True):
|
||||||
|
did: int = Field(default=None, primary_key=True)
|
||||||
|
dname: str = Field(max_length=100)
|
||||||
|
name_en: str = Field(max_length=100)
|
||||||
|
department_leader: list[int] = Field(default=[], sa_column=Column(JSON))
|
||||||
|
parent_id: int = Field(default=0)
|
||||||
|
order: int = Field(default=0)
|
||||||
|
|
||||||
|
class Employee(SQLModel, table = True):
|
||||||
|
userid: int = Field(default=None, primary_key=True)
|
||||||
|
ename: str = Field(max_length=100)
|
||||||
|
dept_id: int = Field(foreign_key='Department.did')
|
||||||
|
open_userid: str = Field(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "wecom-wnzs-adapter"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "企业微信万能助手适配器"
|
description = "企业微信万能助手适配器"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"casbin>=1.43.0",
|
"casbin>=1.43.0",
|
||||||
"fastapi[standard]>=0.116.1",
|
"fastapi[standard]>=0.116.1",
|
||||||
@@ -16,13 +16,12 @@ dependencies = [
|
|||||||
"sqlmodel>=0.0.24",
|
"sqlmodel>=0.0.24",
|
||||||
"uvicorn>=0.35.0",
|
"uvicorn>=0.35.0",
|
||||||
"apscheduler>=3.11.0",
|
"apscheduler>=3.11.0",
|
||||||
"pydantic<2.10",
|
|
||||||
"pickledb>=1.3.2",
|
"pickledb>=1.3.2",
|
||||||
"wecom-sdk>=1.0.0",
|
|
||||||
"xmltodict>=1.0.2",
|
"xmltodict>=1.0.2",
|
||||||
"psycopg2-binary>=2.9.11",
|
"psycopg2-binary>=2.9.11",
|
||||||
"fastscheduler[fastapi]>=0.1.2",
|
"fastscheduler[fastapi]>=0.1.2",
|
||||||
"pydantic-settings>=2.11.0",
|
"pydantic-settings>=2.11.0",
|
||||||
|
"fastmcp>=2.14.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ from config import Settings
|
|||||||
from utils.sing import SingletonProvider
|
from utils.sing import SingletonProvider
|
||||||
|
|
||||||
# 获取单例函数
|
# 获取单例函数
|
||||||
|
def get_wecom_single() -> Wecom:
|
||||||
get_wecom = SingletonProvider(lambda: Wecom(
|
wecom = Wecom(
|
||||||
Settings().WECOM_CORPID,Settings().WECOM_CORPSECRET
|
Settings().WECOM_CORPID,Settings().WECOM_CORPSECRET
|
||||||
))
|
)
|
||||||
|
WECOM_PROXY = Settings().WECOM_PROXY
|
||||||
|
if WECOM_PROXY and WECOM_PROXY != "":
|
||||||
|
wecom.BASE_URL = WECOM_PROXY
|
||||||
|
return wecom
|
||||||
|
|
||||||
|
get_wecom = SingletonProvider(get_wecom_single)
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from wecom_sdk import Wecom
|
from .modules.mixin import Wecom
|
||||||
|
|
||||||
__all__ = [
|
__VERSION__ = "1.0.0"
|
||||||
"Wecom"
|
__AUTHOR__ = "Jasar Ayiken"
|
||||||
]
|
|
||||||
|
|||||||
3
service/wecom/etc/constants.py
Normal file
3
service/wecom/etc/constants.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
MESSAGE_TYPES: Literal["text", "image", "voice", "video", "textcard", "news", "mpnews"]
|
||||||
14
service/wecom/exceptions/general.py
Normal file
14
service/wecom/exceptions/general.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class SDKException(Exception):
|
||||||
|
def __init__(self, errcode: int, message: str):
|
||||||
|
"""
|
||||||
|
通用错误返回类,用于抛出请求错误时的异常
|
||||||
|
- 若请求返回的errcode不为0,则抛出此异常
|
||||||
|
|
||||||
|
@param errcode: 错误码
|
||||||
|
@param message: 错误信息
|
||||||
|
"""
|
||||||
|
self.errcode = str(errcode)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Error Occured: {self.errcode} - {self.message}"
|
||||||
78
service/wecom/modules/base.py
Normal file
78
service/wecom/modules/base.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from wecom.exceptions.general import SDKException
|
||||||
|
from wecom.schemas.token import (
|
||||||
|
AccessTokenInfo,
|
||||||
|
AccessTokenParams,
|
||||||
|
)
|
||||||
|
from wecom.utils.requests import HttpxRequest
|
||||||
|
|
||||||
|
BASE_URL: str = "https://qyapi.weixin.qq.com/cgi-bin"
|
||||||
|
|
||||||
|
|
||||||
|
class WecomBaseClient:
|
||||||
|
BASE_URL: str = BASE_URL
|
||||||
|
|
||||||
|
def __init__(self, corpid: str, corpsecret: str):
|
||||||
|
"""
|
||||||
|
企业微信SDK
|
||||||
|
@param corpid: 企业ID
|
||||||
|
@param corpsecret: 应用的凭证密钥
|
||||||
|
|
||||||
|
每个应用有独立的secret,获取到的access_token只能本应用使用,所以每个应用的access_token应该分开来获取
|
||||||
|
"""
|
||||||
|
self.corpid = corpid
|
||||||
|
self.corpsecret = corpsecret
|
||||||
|
self._access_token = None
|
||||||
|
self.access_token_valid_time = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
async def access_token(self) -> str:
|
||||||
|
"""企业微信SDK的access_token"""
|
||||||
|
if (
|
||||||
|
self.access_token_valid_time
|
||||||
|
and datetime.now() < self.access_token_valid_time
|
||||||
|
):
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
await self.__get_access_token()
|
||||||
|
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
@access_token.setter
|
||||||
|
def access_token(self, value: str):
|
||||||
|
self._access_token = value
|
||||||
|
|
||||||
|
async def __get_access_token(self, refresh: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
获取access_token
|
||||||
|
|
||||||
|
access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
|
||||||
|
由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
|
||||||
|
|
||||||
|
详细说明:https://work.weixin.qq.com/api/doc/90000/90135/91039
|
||||||
|
|
||||||
|
@return: access_token: str 或 None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if (
|
||||||
|
not refresh
|
||||||
|
and self.access_token_valid_time
|
||||||
|
and datetime.now() < self.access_token_valid_time
|
||||||
|
):
|
||||||
|
return self.access_token
|
||||||
|
|
||||||
|
url = self.BASE_URL + "/gettoken"
|
||||||
|
params = AccessTokenParams(
|
||||||
|
corpid=self.corpid, corpsecret=self.corpsecret
|
||||||
|
).model_dump()
|
||||||
|
resp = AccessTokenInfo(**await HttpxRequest.get(url=url, params=params))
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
self.access_token_valid_time = datetime.now() + timedelta(
|
||||||
|
seconds=resp.expires_in
|
||||||
|
)
|
||||||
|
self.access_token = resp.access_token
|
||||||
|
return resp.access_token
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
82
service/wecom/modules/department.py
Normal file
82
service/wecom/modules/department.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from wecom.exceptions.general import SDKException
|
||||||
|
from wecom.modules.base import WecomBaseClient
|
||||||
|
from wecom.schemas.departments import (
|
||||||
|
CreateDepartmentInfo,
|
||||||
|
CreateDepartmentParams,
|
||||||
|
DepartmentInfo,
|
||||||
|
UpdateDepartmentInfo,
|
||||||
|
UpdateDepartmentParams,
|
||||||
|
)
|
||||||
|
from wecom.utils.requests import HttpxRequest
|
||||||
|
|
||||||
|
|
||||||
|
class WecomDepartmentClient(WecomBaseClient):
|
||||||
|
|
||||||
|
async def create_departments(self, data: CreateDepartmentParams) -> int:
|
||||||
|
"""
|
||||||
|
创建部门
|
||||||
|
@param data: 创建部门的参数
|
||||||
|
|
||||||
|
@return: 部门id
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/department/create"
|
||||||
|
params = {"access_token": await self.access_token}
|
||||||
|
resp = CreateDepartmentInfo(
|
||||||
|
**await HttpxRequest.post(url=url, params=params, json=data)
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return resp.id
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
async def delete_departments(self, id: int) -> bool:
|
||||||
|
"""
|
||||||
|
删除部门
|
||||||
|
@param id: 部门id
|
||||||
|
|
||||||
|
@return: 删除状态(Boolean)
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/department/delete"
|
||||||
|
params = {"access_token": await self.access_token, "id": id}
|
||||||
|
resp = await HttpxRequest.get(url=url, params=params)
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
async def update_departments(self, data: UpdateDepartmentParams) -> bool:
|
||||||
|
"""
|
||||||
|
更新部门
|
||||||
|
@param data: 更新部门的参数
|
||||||
|
|
||||||
|
@return: 更新状态(Boolean)
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/department/update"
|
||||||
|
params = {"access_token": await self.access_token}
|
||||||
|
resp = UpdateDepartmentInfo(
|
||||||
|
**await HttpxRequest.post(url=url, params=params, json=data)
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
async def get_departments(self, id: int = None) -> list[DepartmentInfo]:
|
||||||
|
"""
|
||||||
|
获取部门列表
|
||||||
|
@param id: 部门id。获取指定部门及其下的子部门。
|
||||||
|
如果不填,默认获取全量组织架构
|
||||||
|
|
||||||
|
@return: 部门列表
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/department/list"
|
||||||
|
params = {"access_token": await self.access_token, "id": id}
|
||||||
|
resp = DepartmentInfo(**await HttpxRequest.get(url=url, params=params))
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return resp.department
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
60
service/wecom/modules/message.py
Normal file
60
service/wecom/modules/message.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from wecom.exceptions.general import SDKException
|
||||||
|
from wecom.modules.base import WecomBaseClient
|
||||||
|
from wecom.schemas.message import (
|
||||||
|
MessageParams,
|
||||||
|
RecallMessageInfo,
|
||||||
|
RecallMessageParams,
|
||||||
|
SendMessageInfo,
|
||||||
|
)
|
||||||
|
from wecom.utils.requests import HttpxRequest
|
||||||
|
|
||||||
|
|
||||||
|
class WecomMessageClient(WecomBaseClient):
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
self,
|
||||||
|
data: MessageParams,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
企业微信发送消息
|
||||||
|
@param data: 发送消息的参数
|
||||||
|
各类消息的参数详情 https://developer.work.weixin.qq.com/document/path/90236
|
||||||
|
|
||||||
|
@return: 消息ID
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/message/send"
|
||||||
|
params = {"access_token": await self.access_token}
|
||||||
|
|
||||||
|
data = data.model_dump()
|
||||||
|
|
||||||
|
resp = SendMessageInfo(
|
||||||
|
**await HttpxRequest.post(url=url, params=params, json=data)
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return resp.msgid
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
async def recall_message(self, data: RecallMessageParams) -> bool:
|
||||||
|
"""
|
||||||
|
企业微信撤回消息
|
||||||
|
@param msgid: 消息ID
|
||||||
|
|
||||||
|
@return: 撤回状态(Boolean)
|
||||||
|
"""
|
||||||
|
data = data.model_dump()
|
||||||
|
|
||||||
|
url = self.BASE_URL + "/message/recall"
|
||||||
|
params = {"access_token": await self.access_token}
|
||||||
|
|
||||||
|
resp = RecallMessageInfo(
|
||||||
|
**await HttpxRequest.post(url=url, params=params, json=data)
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
10
service/wecom/modules/mixin.py
Normal file
10
service/wecom/modules/mixin.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from wecom.modules.base import WecomBaseClient
|
||||||
|
from wecom.modules.department import WecomDepartmentClient
|
||||||
|
from wecom.modules.message import WecomMessageClient
|
||||||
|
from wecom.modules.users import WecomUsersClient
|
||||||
|
|
||||||
|
|
||||||
|
class Wecom(
|
||||||
|
WecomDepartmentClient, WecomUsersClient, WecomMessageClient, WecomBaseClient
|
||||||
|
):
|
||||||
|
pass
|
||||||
95
service/wecom/modules/users.py
Normal file
95
service/wecom/modules/users.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from wecom.exceptions.general import SDKException
|
||||||
|
from wecom.modules.base import WecomBaseClient
|
||||||
|
from wecom.schemas.departments import DepartmentInfo
|
||||||
|
from wecom.schemas.users import (
|
||||||
|
DepartmentUserDetailInfo,
|
||||||
|
DepartmentUserInfo,
|
||||||
|
UserInfo,
|
||||||
|
)
|
||||||
|
from wecom.utils.requests import HttpxRequest
|
||||||
|
|
||||||
|
|
||||||
|
class WecomUsersClient(WecomBaseClient):
|
||||||
|
async def get_user(self, userid: str) -> dict:
|
||||||
|
"""
|
||||||
|
读取成员
|
||||||
|
@param userid: 成员UserID。对应管理端的账号,企业内必须唯一。不区分大小写,长度为1~64个字节
|
||||||
|
|
||||||
|
|
||||||
|
@return: 成员信息
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/user/get"
|
||||||
|
params = {"access_token": await self.access_token, "userid": userid}
|
||||||
|
resp = UserInfo(**await HttpxRequest.get(url=url, params=params))
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return resp.model_dump(exclude={"errcode", "errmsg"})
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
async def get_user_in_department_detail(self, department_id: str) -> dict:
|
||||||
|
"""
|
||||||
|
读取部门成员完整信息
|
||||||
|
@param department_id: 获取的部门id
|
||||||
|
|
||||||
|
|
||||||
|
@return: 部门成员信息
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/user/list"
|
||||||
|
params = {
|
||||||
|
"access_token": await self.access_token,
|
||||||
|
"department_id": department_id,
|
||||||
|
}
|
||||||
|
resp = DepartmentUserDetailInfo(
|
||||||
|
**await HttpxRequest.get(url=url, params=params)
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return resp.model_dump(exclude={"errcode", "errmsg"})
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
async def get_user_in_department(self, department_id: int) -> dict:
|
||||||
|
"""
|
||||||
|
读取部门成员简要信息
|
||||||
|
@param department_id: 获取的部门id
|
||||||
|
|
||||||
|
|
||||||
|
@return: 部门成员信息
|
||||||
|
"""
|
||||||
|
url = self.BASE_URL + "/user/simplelist"
|
||||||
|
params = {
|
||||||
|
"access_token": await self.access_token,
|
||||||
|
"department_id": department_id,
|
||||||
|
}
|
||||||
|
resp = DepartmentUserInfo(**await HttpxRequest.get(url=url, params=params))
|
||||||
|
|
||||||
|
if resp.errcode == 0:
|
||||||
|
return resp.model_dump(exclude={"errcode", "errmsg"})
|
||||||
|
else:
|
||||||
|
raise SDKException(resp.errcode, resp.errmsg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_userid(userid: str, decrypt: bool = False):
|
||||||
|
"""
|
||||||
|
学工号/企业微信ID转换方法
|
||||||
|
|
||||||
|
@param userid: 学工号/企业微信ID
|
||||||
|
@param decrypt: 是否解密
|
||||||
|
|
||||||
|
@return: 转换后的学工号/企业微信ID
|
||||||
|
"""
|
||||||
|
if decrypt:
|
||||||
|
year = str(int(userid[10:12]) + 1945)
|
||||||
|
no = str(int(userid[2:9]) - 115342)
|
||||||
|
no = no[1:7]
|
||||||
|
userid = year + no
|
||||||
|
else:
|
||||||
|
userid = (
|
||||||
|
"8"
|
||||||
|
+ userid[2:3]
|
||||||
|
+ str(int(userid[-6:]) + 1115342)
|
||||||
|
+ userid[8:9]
|
||||||
|
+ str(int(userid[0:4]) - 1945)
|
||||||
|
)
|
||||||
|
return userid
|
||||||
13
service/wecom/schemas/base.py
Normal file
13
service/wecom/schemas/base.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSchema(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra="ignore",
|
||||||
|
use_enum_values=True,
|
||||||
|
from_attributes=True,
|
||||||
|
validate_assignment=True,
|
||||||
|
populate_by_name=True,
|
||||||
|
coerce_numbers_to_str=True,
|
||||||
|
arbitrary_types_allowed=True,
|
||||||
|
)
|
||||||
58
service/wecom/schemas/departments.py
Normal file
58
service/wecom/schemas/departments.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from typing import AnyStr, List
|
||||||
|
|
||||||
|
from wecom.schemas.base import BaseSchema
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDepartmentParams(BaseSchema):
|
||||||
|
"""
|
||||||
|
创建部门
|
||||||
|
|
||||||
|
@param name: 部门名称。长度限制为1~32个字节,字符不能包括\:?”<>
|
||||||
|
@param name_en: 英文名称
|
||||||
|
@param parentid: 父部门id。根部门id为1
|
||||||
|
@param order: 在父部门中的次序值。order值小的排序靠前。
|
||||||
|
@param id: 部门id,整型。指定时必须大于1,不指定时则自动生成
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
name_en: str | None = None
|
||||||
|
parentid: int
|
||||||
|
order: int | None = None
|
||||||
|
id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDepartmentParams(CreateDepartmentParams): ...
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDepartmentInfo(BaseSchema):
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDepartmentInfo(BaseSchema):
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentInfo(BaseSchema):
|
||||||
|
"""
|
||||||
|
部门单体响应数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: AnyStr
|
||||||
|
name_en: AnyStr | None = None
|
||||||
|
department_leader: List[str] | None = None
|
||||||
|
parentid: int | None = None
|
||||||
|
order: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentInfo(BaseSchema):
|
||||||
|
"""
|
||||||
|
部门整体响应数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
department: List[DepartmentInfo]
|
||||||
94
service/wecom/schemas/message.py
Normal file
94
service/wecom/schemas/message.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from typing import AnyStr, Literal
|
||||||
|
|
||||||
|
from wecom.schemas.base import BaseSchema
|
||||||
|
|
||||||
|
|
||||||
|
class MessageParams(BaseSchema):
|
||||||
|
"""
|
||||||
|
发送消息参数
|
||||||
|
各类消息的参数详情 https://developer.work.weixin.qq.com/document/path/90236
|
||||||
|
|
||||||
|
根据msgtype的不同,选择对应的消息内容填充即可
|
||||||
|
|
||||||
|
@param touser: 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。
|
||||||
|
@param toparty: 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||||
|
@param totag: 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||||
|
@param msgtype: 消息类型,此时固定为:text
|
||||||
|
@param agentid: 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
||||||
|
|
||||||
|
@param safe: 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
||||||
|
@param enable_id_trans: 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
||||||
|
@param enable_duplicate_check: 表示是否开启重复消息检查,0表示否,1表示是,默认0
|
||||||
|
@param duplicate_check_interval: 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时
|
||||||
|
|
||||||
|
touser、toparty、totag不能同时为空,后面不再强调
|
||||||
|
"""
|
||||||
|
|
||||||
|
touser: AnyStr | None = None
|
||||||
|
toparty: AnyStr | None = None
|
||||||
|
totag: AnyStr | None = None
|
||||||
|
msgtype: Literal[
|
||||||
|
"text", "image", "voice", "video", "textcard", "news", "mpnews", "markdown"
|
||||||
|
]
|
||||||
|
agentid: int
|
||||||
|
|
||||||
|
# 各种类型的消息内容
|
||||||
|
text: dict | None = None
|
||||||
|
voice: dict | None = None
|
||||||
|
video: dict | None = None
|
||||||
|
file: dict | None = None
|
||||||
|
textcard: dict | None = None
|
||||||
|
news: dict | None = None
|
||||||
|
mpnews: dict | None = None
|
||||||
|
markdown: dict | None = None
|
||||||
|
|
||||||
|
safe: int = 0
|
||||||
|
enable_id_trans: int = 0
|
||||||
|
enable_duplicate_check: int = 0
|
||||||
|
duplicate_check_interval: int = 1800
|
||||||
|
|
||||||
|
|
||||||
|
class SendMessageInfo(BaseSchema):
|
||||||
|
"""
|
||||||
|
发送消息响应数据
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
invaliduser: AnyStr | None = None
|
||||||
|
invalidparty: AnyStr | None = None
|
||||||
|
invalidtag: AnyStr | None = None
|
||||||
|
unlicenseduser: AnyStr | None = None
|
||||||
|
msgid: AnyStr | None = None
|
||||||
|
response_code: AnyStr | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SendMessageInvalid(BaseSchema):
|
||||||
|
"""
|
||||||
|
发送消息失败响应数据
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
errmsg: AnyStr
|
||||||
|
invaliduser: AnyStr | None = None
|
||||||
|
invalidparty: AnyStr | None = None
|
||||||
|
invalidtag: AnyStr | None = None
|
||||||
|
unlicenseduser: AnyStr | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RecallMessageParams(BaseSchema):
|
||||||
|
"""
|
||||||
|
撤回消息请求参数
|
||||||
|
"""
|
||||||
|
|
||||||
|
msgid: AnyStr
|
||||||
|
|
||||||
|
|
||||||
|
class RecallMessageInfo(BaseSchema):
|
||||||
|
"""
|
||||||
|
撤回消息响应数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
36
service/wecom/schemas/token.py
Normal file
36
service/wecom/schemas/token.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from wecom.schemas.base import BaseSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenParams(BaseSchema):
|
||||||
|
"""
|
||||||
|
获取access_token的参数
|
||||||
|
@param corpid: 企业ID
|
||||||
|
@param corpsecret: 应用的凭证密钥
|
||||||
|
"""
|
||||||
|
|
||||||
|
corpid: str
|
||||||
|
corpsecret: str
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenInfo(BaseSchema):
|
||||||
|
"""
|
||||||
|
获取access_token的返回数据
|
||||||
|
@param errcode: 返回码 出错返回码,为0表示成功,非0表示调用失败
|
||||||
|
@param errmsg: 对返回码的文本描述内容
|
||||||
|
@param access_token: 获取到的凭证 最长为512字节
|
||||||
|
@param expires_in: 凭证的有效时间(秒)
|
||||||
|
"""
|
||||||
|
|
||||||
|
errcode: int
|
||||||
|
errmsg: str
|
||||||
|
access_token: str | None = None
|
||||||
|
expires_in: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenInvalid(BaseSchema):
|
||||||
|
"""
|
||||||
|
获取access_token失败时的返回数据
|
||||||
|
@param errmsg: 错误信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
errmsg: str
|
||||||
39
service/wecom/schemas/users.py
Normal file
39
service/wecom/schemas/users.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from typing import AnyStr, List
|
||||||
|
|
||||||
|
from wecom.schemas.base import BaseSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfo(BaseSchema):
|
||||||
|
"""
|
||||||
|
用户单体响应数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
userid: AnyStr | None = None
|
||||||
|
name: AnyStr | None = None
|
||||||
|
department: List[int] | None = None
|
||||||
|
position: AnyStr | None = None
|
||||||
|
moblie: AnyStr | None = None
|
||||||
|
gender: int | None = None
|
||||||
|
email: AnyStr | None = None
|
||||||
|
status: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserSimpleInfo(BaseSchema):
|
||||||
|
userid: AnyStr
|
||||||
|
name: AnyStr
|
||||||
|
department: List[int]
|
||||||
|
open_userid: AnyStr | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentUserInfo(BaseSchema):
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
userlist: List[UserSimpleInfo]
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentUserDetailInfo(BaseSchema):
|
||||||
|
errcode: int
|
||||||
|
errmsg: AnyStr
|
||||||
|
userlist: List[UserInfo]
|
||||||
0
service/wecom/utils/convert.py
Normal file
0
service/wecom/utils/convert.py
Normal file
42
service/wecom/utils/requests.py
Normal file
42
service/wecom/utils/requests.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class HttpxRequest:
|
||||||
|
@classmethod
|
||||||
|
async def get(
|
||||||
|
cls, url: str, params: dict | None = None, headers: dict | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
发送GET请求
|
||||||
|
@param url: 请求URL
|
||||||
|
@param params: 请求参数
|
||||||
|
@param headers: 请求头
|
||||||
|
@return: 响应内容
|
||||||
|
"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url, params=params, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def post(
|
||||||
|
cls,
|
||||||
|
url: str,
|
||||||
|
params: dict | None = None,
|
||||||
|
data: dict | None = None,
|
||||||
|
json: dict | None = None,
|
||||||
|
headers: dict | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
发送POST请求
|
||||||
|
@param url: 请求URL
|
||||||
|
@param params: 请求参数
|
||||||
|
@param headers: 请求头
|
||||||
|
@return: 响应内容
|
||||||
|
"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
url, params=params, data=data, json=json, headers=headers
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
Reference in New Issue
Block a user