This commit is contained in:
2026-01-15 18:08:08 +08:00
parent 5586e98e51
commit 4a51ec89cc
21 changed files with 184 additions and 72 deletions

View File

@@ -1,12 +1,11 @@
from pydantic_settings import BaseSettings,SettingsConfigDict
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env" , env_prefix="WNZS_")
model_config = SettingsConfigDict(env_file=".env", env_prefix="WNZS_")
PGSQL: str = ""
WECOM_PROXY: str = ""
WECOM_CORPID: str = ""
WECOM_CORPSECRET: str = ""
WECOM_APP_TOKEN: str = ""
WECOM_APP_ENCODING_AES_KEY: str = ""

View File

@@ -1,7 +1,8 @@
import http
from fastapi import FastAPI, HTTPException, Request
from pydantic import ValidationError
from fastapi.responses import JSONResponse
from pydantic import ValidationError
from uvicorn.server import logger
exceptions = [Exception, HTTPException, ValidationError]

View File

@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from uvicorn.server import logger
def init_database():
from model import create_db_and_tables
@@ -10,12 +11,14 @@ def init_database():
create_db_and_tables()
logger.info("[数据库] 数据库初始化完成 ✅")
def init_scheduler(app : FastAPI):
def init_scheduler(app: FastAPI):
from scheduler import init_scheduler_router
logger.info("[定时任务] 初始化定时任务 📦")
init_scheduler_router(app)
logger.info("[定时任务] 定时任务初始化完成 ✅")
def active_config():
logger.info(f"[激活配置] 加载配置 ⚙️")
@@ -29,12 +32,15 @@ def import_router(app: FastAPI):
app.include_router(router)
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())
app.mount("/app", await create_mcp_app())
logger.info(f"[导入MCP] MCP导入完成 ✅")
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f"[生命周期] 应用启动 🚀")

View File

@@ -4,7 +4,6 @@ from handler.exception import install as exception_install
from lifespan import lifespan
from mcps import create_mcp_app
app = FastAPI(lifespan=lifespan)
exception_install(app)
@@ -12,4 +11,4 @@ exception_install(app)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app=app, port=8000)
uvicorn.run(app=app, port=8000)

View File

@@ -1,8 +1,9 @@
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()
await main_mcp.import_server(weather_mcp, prefix="test")
return main_mcp.http_app()

View File

@@ -1,3 +1,3 @@
from fastmcp import FastMCP
weather_mcp = FastMCP(name="WeatherService")
weather_mcp = FastMCP(name="WeatherService")

View File

@@ -1,6 +1,7 @@
from sqlmodel import Session, SQLModel, create_engine
from config import Settings
from config import Settings
from model.model import Department, Employee, Tenant
PGSQL = Settings().PGSQL
@@ -17,4 +18,3 @@ def get_engine():
def get_session():
return Session(get_engine())

View File

@@ -1,22 +1,85 @@
from sqlmodel import SQLModel, Field, Column, JSON
from datetime import datetime
from sqlalchemy import JSON, Column, DateTime, func
from sqlmodel import JSON, Field, SQLModel
class Department(SQLModel, table = True):
did: int = Field(default=None, primary_key=True)
class TenantTimeMixin(SQLModel):
tenant_id: int = Field(index=True, description="租户ID")
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
)
class Department(TenantTimeMixin, SQLModel, table=True):
id: int | None = 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)
department_leader: list[int] = Field(default_factory=list, sa_column=Column(JSON))
parent_id: int = Field(default=0, index=True)
order: int = Field(default=0)
class Employee(SQLModel, table = True):
userid: int = Field(default=None, primary_key=True)
class Employee(TenantTimeMixin, SQLModel, table=True):
id: int | None = 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)
dept_id: int = Field(foreign_key="department.id", index=True)
open_userid: str = Field(max_length=100, index=True)
class Tenant(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
# ========== 基础信息 ==========
name: str = Field(max_length=100, index=True, description="租户名称 / 企业名称")
# ========== 企业微信配置 ==========
wecom_corp_id: str = Field(max_length=64, index=True, description="企业微信 CorpID")
wecom_corp_secret: str = Field(max_length=128, description="企业微信应用 Secret")
wecom_agent_id: int = Field(description="企业微信应用 AgentId")
wecom_token: str = Field(max_length=64, description="企业微信回调 Token")
wecom_encoding_aes_key: str = Field(
max_length=64, description="企业微信回调 EncodingAESKey"
)
# ========== 时间字段 ==========
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
)

View File

@@ -25,4 +25,7 @@ dependencies = [
]
[dependency-groups]
dev = []
dev = [
"isort>=7.0.0",
"ruff>=0.14.11",
]

View File

@@ -1,15 +1,14 @@
import json
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import PlainTextResponse
from uvicorn.server import logger
from utils.wxcom import wxcpt
from utils.wxcom import (
decrypt_message,
get_request_params
)
from utils.wxcom import decrypt_message, get_request_params, wxcpt
router = APIRouter()
@router.get("/callback")
async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: str):
"""验证URL有效性"""
@@ -29,20 +28,21 @@ async def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: st
logger.error(f"验证过程发生错误: {str(e)}")
raise HTTPException(status_code=500, detail="服务器内部错误")
@router.post("/callback")
async def receive_message(request: Request):
"""接收并处理企业微信消息"""
try:
# 获取请求参数并验证,返回请求体、消息签名、时间戳和随机数
body, msg_signature, timestamp, nonce = await get_request_params(request)
# 对请求体进行解密,得到解密后的消息字典
xml_dict : dict= decrypt_message(body, msg_signature, timestamp, nonce)
logger.info(f"解密后的消息字典: \n {json.dumps(xml_dict.get("xml") , ensure_ascii=False , indent=2)}")
xml_dict: dict = decrypt_message(body, msg_signature, timestamp, nonce)
logger.info(
f"解密后的消息字典: \n {json.dumps(xml_dict.get('xml'), ensure_ascii=False, indent=2)}"
)
# 处理消息
# subscription(xml_dict)
except Exception as e:
logger.error(f"处理消息时发生错误: {str(e)}")
raise HTTPException(status_code=500, detail="服务器内部错误")
raise HTTPException(status_code=500, detail="服务器内部错误")

View File

@@ -1,7 +1,9 @@
from fastapi import FastAPI
from fastscheduler.fastapi_integration import create_scheduler_routes
from scheduler.scheduler import scheduler
def init_scheduler_router(app : FastAPI):
def init_scheduler_router(app: FastAPI):
app.include_router(create_scheduler_routes(scheduler))
scheduler.start()
scheduler.start()

View File

@@ -1,9 +1,12 @@
from fastscheduler import FastScheduler
from service import get_wecom
scheduler = FastScheduler(quiet=True)
@scheduler.every(10).seconds
def background_task():
print("Background work")
@scheduler.every(4).hours
async def background_task():
wecom = get_wecom()
await wecom.get_departments()

View File

@@ -1,15 +1,15 @@
from service.wecom import Wecom
from config import Settings
from service.wecom import Wecom
from utils.sing import SingletonProvider
# 获取单例函数
def get_wecom_single() -> Wecom:
wecom = Wecom(
Settings().WECOM_CORPID,Settings().WECOM_CORPSECRET
)
wecom = Wecom(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)
get_wecom = SingletonProvider(get_wecom_single)

View File

@@ -1,10 +1,7 @@
from datetime import datetime, timedelta
from wecom.exceptions.general import SDKException
from wecom.schemas.token import (
AccessTokenInfo,
AccessTokenParams,
)
from wecom.schemas.token import AccessTokenInfo, AccessTokenParams
from wecom.utils.requests import HttpxRequest
BASE_URL: str = "https://qyapi.weixin.qq.com/cgi-bin"

View File

@@ -11,7 +11,6 @@ from wecom.utils.requests import HttpxRequest
class WecomDepartmentClient(WecomBaseClient):
async def create_departments(self, data: CreateDepartmentParams) -> int:
"""
创建部门

View File

@@ -12,7 +12,6 @@ from wecom.utils.requests import HttpxRequest
class WecomMessageClient(WecomBaseClient):
async def send_message(
self,
data: MessageParams,

View File

@@ -1,11 +1,7 @@
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.schemas.users import DepartmentUserDetailInfo, DepartmentUserInfo, UserInfo
from wecom.utils.requests import HttpxRequest

View File

@@ -1,9 +1,4 @@
from .wx_com import wxcpt
from .wx_utils import get_request_params,decrypt_message,extract_message_content
from .wx_utils import decrypt_message, extract_message_content, get_request_params
__all__ = [
"wxcpt",
"get_request_params",
"decrypt_message",
"extract_message_content"
]
__all__ = ["wxcpt", "get_request_params", "decrypt_message", "extract_message_content"]

View File

@@ -1,6 +1,7 @@
from uvicorn.server import logger
from config import Settings
from utils.wxcom.WXBizMsgCrypt3 import WXBizMsgCrypt
from utils.wxcom.WXBizMsgCrypt3 import WXBizMsgCrypt
def get_wxcpt():
@@ -15,20 +16,20 @@ def get_wxcpt():
required_configs = [
Settings().WECOM_APP_TOKEN,
Settings().WECOM_APP_ENCODING_AES_KEY,
Settings().WECOM_CORPID
Settings().WECOM_CORPID,
]
if not all(required_configs):
raise ValueError("企业微信配置不完整")
return WXBizMsgCrypt(
Settings().WECOM_APP_TOKEN, # 设置的Token
Settings().WECOM_APP_ENCODING_AES_KEY, # 设置密钥
Settings().WECOM_CORPID # 企业ID
Settings().WECOM_CORPID, # 企业ID
)
except Exception as e:
logger.error(f"初始化WXBizMsgCrypt失败: {str(e)}")
raise
wxcpt = get_wxcpt()

View File

@@ -4,6 +4,7 @@ from typing import Dict, Tuple, Union
import xmltodict
from fastapi import HTTPException, Request
from uvicorn.server import logger
from .wx_com import wxcpt
@@ -23,7 +24,10 @@ async def get_request_params(request: Request) -> Tuple[bytes, str, str, str]:
return body, msg_signature, timestamp, nonce
def decrypt_message(body: bytes, msg_signature: str, timestamp: str, nonce: str) -> dict:
def decrypt_message(
body: bytes, msg_signature: str, timestamp: str, nonce: str
) -> dict:
"""解密消息"""
ret, sMsg = wxcpt.DecryptMsg(body, msg_signature, timestamp, nonce)
if ret != 0:
@@ -35,6 +39,7 @@ def decrypt_message(body: bytes, msg_signature: str, timestamp: str, nonce: str)
return xml_dict
def extract_message_content(
xml_dict: Dict,
) -> Tuple[str, str, str, str, Union[Dict[str, Union[str, None]], str, None], str, str]:
@@ -82,7 +87,6 @@ def extract_message_content(
message_data = xml_content.get("Content")
logger.info(f"收到未知类型消息: {message_data}")
return {
"ToUserName": to_user_name,
"FromUserName": from_user_name,
@@ -91,4 +95,4 @@ def extract_message_content(
"MsgId": msg_id,
"AgentID": agent_id,
**message_data,
}
}

46
uv.lock generated
View File

@@ -651,6 +651,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "isort"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" },
]
[[package]]
name = "jaraco-classes"
version = "3.4.0"
@@ -1659,6 +1668,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
]
[[package]]
name = "ruff"
version = "0.14.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
{ url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
{ url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
]
[[package]]
name = "secretstorage"
version = "3.5.0"
@@ -1981,6 +2016,12 @@ dependencies = [
{ name = "xmltodict" },
]
[package.dev-dependencies]
dev = [
{ name = "isort" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "apscheduler", specifier = ">=3.11.0" },
@@ -2003,7 +2044,10 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = []
dev = [
{ name = "isort", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.14.11" },
]
[[package]]
name = "wrapt"