commit b2f23a933b1f974682844bd5f1e25928caa3ef92 Author: zhuyiyi <649091362@qq.com> Date: Thu Jun 25 17:41:06 2026 +0800 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..38daf51 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# 版本控制 +.git +.gitignore + +# 编辑器 +.vscode + +# 构建与配置文件(非运行时必需) +Dockerfile +docker-compose.yml +README.md +*.md +*.log +*.log.* + +# 开发依赖 +web/node_modules +node_modules + +# Python 缓存与临时文件 +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ +.coverage +htmlcov/ + +# 测试 +tests/ +test_*.py + +# 环境文件(通过 volume 挂载,不进镜像) +.env +.env.* +env/ + +# 部署脚本(只拷贝需要的 entrypoint.sh,其他 deploy/ 内容不进) +deploy/ +!deploy/entrypoint.sh +!deploy/web.conf + +# 其他 +*.swp +*.tmp +Thumbs.db \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..07ffcb8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.html linguist-language=python \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2f858b --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +__pycache__/ +.idea/ +venv/ +.mypy_cache/ +.vscode +.ruff_cache/ +.pytest_cache/ +migrations/ + +db*.sqlite3 +db.sqlite3 +db.sqlite3-journal +db.sqlite3-shm +db.sqlite3-wal + +.DS_Store +._.DS_Store + + +*.zip +*.tar.gz +tmp* +tmp/* +*.sqlite3-shm +*.sqlite3-wal +outputs/ +uploads/ + + +env/* +!env/*example.py +*.pkl +scripts/setup.sh +*.log +*.log.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e7e47f9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +FROM node:18.12.0-alpine3.16 AS web + +WORKDIR /opt/build +COPY /web ./web +RUN npm install pnpm -g --registry=https://registry.npmmirror.com && cd /opt/build/web && pnpm i --registry=https://registry.npmmirror.com && pnpm run build + +# === Python 依赖构建阶段(带编译工具)=== +FROM python:3.11-slim-bullseye AS builder + +# 切换为中科大源 +RUN sed -i "s@http://.*.debian.org@http://mirrors.ustc.edu.cn@g" /etc/apt/sources.list \ + && rm -f /etc/apt/apt.conf.d/docker-clean \ + && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && apt-get update \ + && apt-get install -y --no-install-recommends gcc python3-dev \ + && rm -rf /var/lib/apt/lists/* + +# 创建虚拟环境 +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# 安装 Python 依赖(使用阿里云源,避免清华限流) +ADD app ./app +COPY requirements.txt ./app +RUN find app -name "requirement*.txt" | xargs -n 1 pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ -r + +FROM python:3.11-slim-bullseye +WORKDIR /opt/backend-server + +# 设置时区 & 安装最小运行依赖 +RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nginx tzdata bash curl \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone + +# 从 builder 阶段拷贝已安装的 Python 依赖 +COPY --from=builder /opt/venv /opt/venv + +# 激活虚拟环境(通过 PATH) +ENV PATH="/opt/venv/bin:$PATH" +ENV LANG=zh_CN.UTF-8 + +# 拷贝应用代码(只拷贝必需目录和文件) +COPY app ./app +COPY static ./static +COPY templates ./templates +COPY run.py . +COPY deploy/entrypoint.sh . + +# 拷贝前端产物 +COPY --from=web /opt/build/web/dist ./web/dist + +# Nginx 配置 +COPY deploy/web.conf /etc/nginx/sites-available/web.conf +RUN rm -f /etc/nginx/sites-enabled/default \ + && ln -s /etc/nginx/sites-available/web.conf /etc/nginx/sites-enabled/web.conf; \ + python -c "import sys; print(sys.path)" + +EXPOSE 80 + +ENTRYPOINT ["sh", "entrypoint.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3947bd9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 mizhexiaoxiao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3e32674 --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +# Build configuration +# ------------------- + +APP_NAME := `sed -n 's/^ *name.*=.*"\([^"]*\)".*/\1/p' pyproject.toml` +APP_VERSION := `sed -n 's/^ *version.*=.*"\([^"]*\)".*/\1/p' pyproject.toml` +GIT_REVISION = `git rev-parse HEAD` + +# Introspection targets +# --------------------- + +.PHONY: help +help: header targets + +.PHONY: header +header: + @echo "\033[34mEnvironment\033[0m" + @echo "\033[34m---------------------------------------------------------------\033[0m" + @printf "\033[33m%-23s\033[0m" "APP_NAME" + @printf "\033[35m%s\033[0m" $(APP_NAME) + @echo "" + @printf "\033[33m%-23s\033[0m" "APP_VERSION" + @printf "\033[35m%s\033[0m" $(APP_VERSION) + @echo "" + @printf "\033[33m%-23s\033[0m" "GIT_REVISION" + @printf "\033[35m%s\033[0m" $(GIT_REVISION) + @echo "\n" + +.PHONY: targets +targets: + @echo "\033[34mDevelopment Targets\033[0m" + @echo "\033[34m---------------------------------------------------------------\033[0m" + @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' + +# Development targets +# ------------- + +.PHONY: install +install: ## Install dependencies + uv add pyproject.toml + + +.PHONY: run +run: start + +.PHONY: start +start: ## Starts the server + python run.py + +# Check, lint and format targets +# ------------------------------ + +.PHONY: check +check: check-format lint + +.PHONY: check-format +check-format: ## Dry-run code formatter + black ./ --check + isort ./ --profile black --check + +.PHONY: lint +lint: ## Run ruff + ruff check ./app + +.PHONY: format +format: ## Run code formatter + black ./ + isort ./ --profile black + + +.PHONY: test +test: ## Run the test suite + $(eval include .env) + $(eval export $(sh sed 's/=.*//' .env)) + pytest -vv -s --cache-clear ./ + +.PHONY: clean-db +clean-db: ## 删除migrations文件夹和db.sqlite3 + find . -type d -name "migrations" -exec rm -rf {} + + rm -f db.sqlite3 db.sqlite3-shm db.sqlite3-wal + +.PHONY: migrate +migrate: ## 运行aerich migrate命令生成迁移文件 + aerich migrate + +.PHONY: upgrade +upgrade: ## 运行aerich upgrade命令应用迁移 + aerich upgrade \ No newline at end of file diff --git a/README-en.md b/README-en.md new file mode 100644 index 0000000..20a0c84 --- /dev/null +++ b/README-en.md @@ -0,0 +1,219 @@ +

+ + Vue FastAPI Admin Logo + +

+ +

vue-fastapi-admin

+ +English | [简体中文](./README.md) + +vue-fastapi-admin is a modern front-end and back-end separation development platform that combines FastAPI, Vue3, and Naive UI. It incorporates RBAC (Role-Based Access Control) management, dynamic routing, and JWT (JSON Web Token) authentication, making it ideal for rapid development of small to medium-sized applications and also serves as a valuable learning resource. + +### Features +- **Popular Tech Stack**: The backend is developed with the high-performance asynchronous framework FastAPI using Python 3.11, while the front-end is powered by cutting-edge technologies such as Vue3 and Vite, complemented by the efficient package manager, pnpm. +- **Code Standards**: The project is equipped with various plugins for code standardization and quality control, ensuring consistency and enhancing team collaboration efficiency. +- **Dynamic Routing**: Backend dynamic routing combined with the RBAC model allows for fine-grained control of menus and routing. +- **JWT Authentication**: User identity verification and authorization are handled through JWT, enhancing the application's security. +- **Granular Permission Control**: Implements detailed permission management including button and interface level controls, ensuring different roles and users have appropriate permissions. + +### Live Demo +- URL: http://139.9.100.77:9999 +- Username: admin +- Password: 123456 + +### Screenshots + +#### Login Page +![Login Page](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/login.jpg) + +#### Workbench +![Workbench](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/workbench.jpg) + +#### User Management +![User Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/user.jpg) + +#### Role Management +![Role Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/role.jpg) + +#### Menu Management +![Menu Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/menu.jpg) + +#### API Management +![API Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/api.jpg) + +### Quick Start +Please follow the instructions below for installation and configuration: + +#### Method 1:dockerhub pull image + +```sh +docker pull mizhexiaoxiao/vue-fastapi-admin:latest +docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 mizhexiaoxiao/vue-fastapi-admin +``` + +#### Method 2: Build Image Using Dockerfile +##### Install Docker + +```sh +yum install -y docker-ce +systemctl start docker +``` + +##### Build the Image + +```sh +git clone https://github.com/mizhexiaoxiao/vue-fastapi-admin.git +cd vue-fastapi-admin +docker build --no-cache . -t vue-fastapi-admin +``` + +##### Start the Container + +```sh +docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 vue-fastapi-admin +``` + +##### Access the Service + +http://localhost:9999 + +username:admin + +password:123456 + +### Local Setup +#### Backend +The backend service requires the following environment: +- Python 3.11 + +#### Method 1 (Recommended): Install Dependencies with uv +1. Install uv +```sh +pip install uv +``` + +2. Create and activate virtual environment +```sh +uv venv +source .venv/bin/activate # Linux/Mac +# or +.\.venv\Scripts\activate # Windows +``` + +3. Install dependencies +```sh +uv add pyproject.toml +``` + +4. Start the backend service +```sh +python run.py +``` + +#### Method 2: Install Dependencies with Pip +1. Create a Python virtual environment: +```sh +python3 -m venv venv +source venv/bin/activate # Linux/Mac +# or +.\venv\Scripts\activate # Windows +``` + +2. Install project dependencies: +```sh +pip install -r requirements.txt +``` + +3. Start the backend service: +```sh +python run.py +``` +The backend service is now running, and you can visit http://localhost:9999/docs to view the API documentation. + +#### Frontend +The frontend project requires a Node.js environment (recommended version 18.8.0 or higher). +- node v18.8.0+ + +1. Navigate to the frontend project directory: +```sh +cd web +``` + +2. Install project dependencies (pnpm is recommended: https://pnpm.io/zh/installation) +```sh +npm i -g pnpm # If pnpm is already installed, skip this step +pnpm i # Or use npm i +``` + +3. Start the frontend development server: +```sh +pnpm dev +``` + +### Directory Structure Explanation + +``` +├── app // Application directory +│ ├── api // API interface directory +│ │ └── v1 // Version 1 of the API interfaces +│ │ ├── apis // API-related interfaces +│ │ ├── base // Base information interfaces +│ │ ├── menus // Menu related interfaces +│ │ ├── roles // Role related interfaces +│ │ └── users // User related interfaces +│ ├── controllers // Controllers directory +│ ├── core // Core functionality module +│ ├── log // Log directory +│ ├── models // Data models directory +│ ├── schemas // Data schema/structure definitions +│ ├── settings // Configuration settings directory +│ └── utils // Utilities directory +├── deploy // Deployment related directory +│ └── sample-picture // Sample picture directory +└── web // Front-end web directory + ├── build // Build scripts and configuration directory + │ ├── config // Build configurations + │ ├── plugin // Build plugins + │ └── script // Build scripts + ├── public // Public resources directory + │ └── resource // Public resource files + ├── settings // Front-end project settings + └── src // Source code directory + ├── api // API interface definitions + ├── assets // Static resources directory + │ ├── images // Image resources + │ ├── js // JavaScript files + │ └── svg // SVG vector files + ├── components // Components directory + │ ├── common // Common components + │ ├── icon // Icon components + │ ├── page // Page components + │ ├── query-bar // Query bar components + │ └── table // Table components + ├── composables // Composable functionalities + ├── directives // Directives directory + ├── layout // Layout directory + │ └── components // Layout components + ├── router // Routing directory + │ ├── guard // Route guards + │ └── routes // Route definitions + ├── store // State management (pinia) + │ └── modules // State modules + ├── styles // Style files directory + ├── utils // Utilities directory + │ ├── auth // Authentication related utilities + │ ├── common // Common utilities + │ ├── http // Encapsulated axios + │ └── storage // Encapsulated localStorage and sessionStorage + └── views // Views/Pages directory + ├── error-page // Error pages + ├── login // Login page + ├── profile // Profile page + ├── system // System management page + └── workbench // Workbench page +``` + +### Visitors Count + +Loading \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1bff29 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +

+ + Vue FastAPI Admin Logo + +

+ +

vue-fastapi-admin

+ +[English](./README-en.md) | 简体中文 + +基于 FastAPI + Vue3 + Naive UI 的现代化前后端分离开发平台,融合了 RBAC 权限管理、动态路由和 JWT 鉴权,助力中小型应用快速搭建,也可用于学习参考。 + +### 特性 +- **最流行技术栈**:基于 Python 3.11 和 FastAPI 高性能异步框架,结合 Vue3 和 Vite 等前沿技术进行开发,同时使用高效的 npm 包管理器 pnpm。 +- **代码规范**:项目内置丰富的规范插件,确保代码质量和一致性,有效提高团队协作效率。 +- **动态路由**:后端动态路由,结合 RBAC(Role-Based Access Control)权限模型,提供精细的菜单路由控制。 +- **JWT鉴权**:使用 JSON Web Token(JWT)进行身份验证和授权,增强应用的安全性。 +- **细粒度权限控制**:实现按钮和接口级别的权限控制,确保不同用户或角色在界面操作和接口访问时具有不同的权限限制。 + +### 在线预览 +- http://47.111.145.81:3000 +- username: admin +- password: 123456 + +### 登录页 + +![image](https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/login.jpg) +### 工作台 + +![image](https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/workbench.jpg) + +### 用户管理 + +![image](https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/user.jpg) +### 角色管理 + +![image](https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/role.jpg) + +### 菜单管理 + +![image](https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/menu.jpg) + +### API管理 + +![image](https://gitee.com/mizhexiaoxiao/vue-fastapi-admin/raw/main/deploy/sample-picture/api.jpg) + +### 快速开始 +#### 方法一:dockerhub拉取镜像 + +```sh +docker pull mizhexiaoxiao/vue-fastapi-admin:latest +docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 mizhexiaoxiao/vue-fastapi-admin +``` + +#### 方法二:dockerfile构建镜像 +##### docker安装(版本17.05+) + +```sh +yum install -y docker-ce +systemctl start docker +``` + +##### 构建镜像 + +```sh +git clone https://gitee.com/mizhexiaoxiao/vue-fastapi-admin.git +cd vue-fastapi-admin +docker build --no-cache . -t vue-fastapi-admin +``` + +##### 启动容器 + +```sh +docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 vue-fastapi-admin +``` + +##### 访问 + +http://localhost:9999 + +username:admin + +password:123456 + +### 本地启动 +#### 后端 +启动项目需要以下环境: +- Python 3.11 + +#### 方法一(推荐):使用 uv 安装依赖 +1. 安装 uv +```sh +pip install uv +``` + +2. 创建并激活虚拟环境 +```sh +uv venv +source .venv/bin/activate # Linux/Mac +# 或 +.\.venv\Scripts\activate # Windows +``` + +3. 安装依赖 +```sh +uv add pyproject.toml +``` + +4. 启动服务 +```sh +python run.py +``` + +#### 方法二:使用 Pip 安装依赖 +1. 创建虚拟环境 +```sh +python3 -m venv venv +``` + +2. 激活虚拟环境 +```sh +source venv/bin/activate # Linux/Mac +# 或 +.\venv\Scripts\activate # Windows +``` + +3. 安装依赖 +```sh +pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +4. 启动服务 +```sh +python run.py +``` + +服务现在应该正在运行,访问 http://localhost:9999/docs 查看API文档 + +#### 前端 +启动项目需要以下环境: +- node v18.8.0+ + +1. 进入前端目录 +```sh +cd web +``` + +2. 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation) +```sh +npm i -g pnpm # 已安装可忽略 +pnpm i # 或者 npm i +``` + +3. 启动 +```sh +pnpm dev +``` + +### 目录说明 + +``` +├── app // 应用程序目录 +│ ├── api // API接口目录 +│ │ └── v1 // 版本1的API接口 +│ │ ├── apis // API相关接口 +│ │ ├── base // 基础信息接口 +│ │ ├── menus // 菜单相关接口 +│ │ ├── roles // 角色相关接口 +│ │ └── users // 用户相关接口 +│ ├── controllers // 控制器目录 +│ ├── core // 核心功能模块 +│ ├── log // 日志目录 +│ ├── models // 数据模型目录 +│ ├── schemas // 数据模式/结构定义 +│ ├── settings // 配置设置目录 +│ └── utils // 工具类目录 +├── deploy // 部署相关目录 +│ └── sample-picture // 示例图片目录 +└── web // 前端网页目录 + ├── build // 构建脚本和配置目录 + │ ├── config // 构建配置 + │ ├── plugin // 构建插件 + │ └── script // 构建脚本 + ├── public // 公共资源目录 + │ └── resource // 公共资源文件 + ├── settings // 前端项目配置 + └── src // 源代码目录 + ├── api // API接口定义 + ├── assets // 静态资源目录 + │ ├── images // 图片资源 + │ ├── js // JavaScript文件 + │ └── svg // SVG矢量图文件 + ├── components // 组件目录 + │ ├── common // 通用组件 + │ ├── icon // 图标组件 + │ ├── page // 页面组件 + │ ├── query-bar // 查询栏组件 + │ └── table // 表格组件 + ├── composables // 可组合式功能块 + ├── directives // 指令目录 + ├── layout // 布局目录 + │ └── components // 布局组件 + ├── router // 路由目录 + │ ├── guard // 路由守卫 + │ └── routes // 路由定义 + ├── store // 状态管理(pinia) + │ └── modules // 状态模块 + ├── styles // 样式文件目录 + ├── utils // 工具类目录 + │ ├── auth // 认证相关工具 + │ ├── common // 通用工具 + │ ├── http // 封装axios + │ └── storage // 封装localStorage和sessionStorage + └── views // 视图/页面目录 + ├── error-page // 错误页面 + ├── login // 登录页面 + ├── profile // 个人资料页面 + ├── system // 系统管理页面 + └── workbench // 工作台页面 +``` + +### 进群交流 +进群的条件是给项目一个star,小小的star是作者维护下去的动力。 + +你可以在群里提出任何疑问,我会尽快回复答疑。 + + + +## 打赏 +如果项目有帮助到你,可以请作者喝杯咖啡~ + +
+ + +
+ +## 定制开发 +如果有基于该项目的定制需求或其他合作,请添加下方微信,备注来意 + + + +### Visitors Count + +Loading diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..78f9b04 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,49 @@ +from pathlib import Path +import sys; sys.path.extend([str(Path(__file__).parent / 'utils' / 'some_sdk')]) + +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from tortoise import Tortoise + +from app.core.exceptions import SettingNotFound +from app.core.init_app import ( + init_data, + tear_down, + make_middlewares, + register_exceptions, + register_routers, + mount_static_and_config_swagger +) + +try: + from app.settings.config import settings +except ImportError: + raise SettingNotFound("Can not import settings") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_data() + yield + await Tortoise.close_connections() + await tear_down() + + +def create_app() -> FastAPI: + app = FastAPI( + title=settings.APP_TITLE, + description=settings.APP_DESCRIPTION, + version=settings.VERSION, + openapi_url="/api/openapi.json", + docs_url="/api/api-docs", + middleware=make_middlewares(), + lifespan=lifespan, + ) + register_exceptions(app) + register_routers(app, prefix="/api") + mount_static_and_config_swagger(app) + return app + + +app = create_app() diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..8ed6454 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter + +from .v1 import v1_router + +api_router = APIRouter() +api_router.include_router(v1_router, prefix="/v1") + + +__all__ = ["api_router"] diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..4b395f1 --- /dev/null +++ b/app/api/v1/__init__.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter + +from app.core.dependency import DependPermisson, DependWeixinUser + +from .apis import apis_router +from .auditlog import auditlog_router +from .base import base_router +from .depts import depts_router +from .menus import menus_router +from .roles import roles_router +from .users import users_router +from .weixin import weixin_router +from .finance_parse import finance_parse_router +from .msg import msg_route_router +from .automation import automation_router + +v1_router = APIRouter() + +v1_router.include_router(base_router, prefix="/base") +v1_router.include_router(users_router, prefix="/user", dependencies=[DependPermisson]) +v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermisson]) +v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermisson]) +v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermisson]) +v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermisson]) +v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermisson]) +v1_router.include_router(finance_parse_router, prefix="/parse_finance_data", dependencies=[DependPermisson]) +v1_router.include_router(weixin_router, prefix='/weixin') +v1_router.include_router(automation_router, prefix='/auto') +v1_router.include_router(msg_route_router, dependencies=[DependWeixinUser]) diff --git a/app/api/v1/apis/__init__.py b/app/api/v1/apis/__init__.py new file mode 100644 index 0000000..e5435dc --- /dev/null +++ b/app/api/v1/apis/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .apis import router + +apis_router = APIRouter() +apis_router.include_router(router, tags=["API模块"]) + +__all__ = ["apis_router"] diff --git a/app/api/v1/apis/apis.py b/app/api/v1/apis/apis.py new file mode 100644 index 0000000..9cdca28 --- /dev/null +++ b/app/api/v1/apis/apis.py @@ -0,0 +1,67 @@ +from fastapi import APIRouter, Query +from tortoise.expressions import Q + +from app.controllers.api import api_controller +from app.schemas import Success, SuccessExtra +from app.schemas.apis import * + +router = APIRouter() + + +@router.get("/list", summary="查看API列表") +async def list_api( + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + path: str = Query(None, description="API路径"), + summary: str = Query(None, description="API简介"), + tags: str = Query(None, description="API模块"), +): + q = Q() + if path: + q &= Q(path__contains=path) + if summary: + q &= Q(summary__contains=summary) + if tags: + q &= Q(tags__contains=tags) + total, api_objs = await api_controller.list(page=page, page_size=page_size, search=q, order=["tags", "id"]) + data = [await obj.to_dict() for obj in api_objs] + return SuccessExtra(data=data, total=total, page=page, page_size=page_size) + + +@router.get("/get", summary="查看Api") +async def get_api( + id: int = Query(..., description="Api"), +): + api_obj = await api_controller.get(id=id) + data = await api_obj.to_dict() + return Success(data=data) + + +@router.post("/create", summary="创建Api") +async def create_api( + api_in: ApiCreate, +): + await api_controller.create(obj_in=api_in) + return Success(msg="Created Successfully") + + +@router.post("/update", summary="更新Api") +async def update_api( + api_in: ApiUpdate, +): + await api_controller.update(id=api_in.id, obj_in=api_in) + return Success(msg="Update Successfully") + + +@router.delete("/delete", summary="删除Api") +async def delete_api( + api_id: int = Query(..., description="ApiID"), +): + await api_controller.remove(id=api_id) + return Success(msg="Deleted Success") + + +@router.post("/refresh", summary="刷新API列表") +async def refresh_api(): + await api_controller.refresh_api() + return Success(msg="OK") diff --git a/app/api/v1/auditlog/__init__.py b/app/api/v1/auditlog/__init__.py new file mode 100644 index 0000000..d90f07c --- /dev/null +++ b/app/api/v1/auditlog/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .auditlog import router + +auditlog_router = APIRouter() +auditlog_router.include_router(router, tags=["审计日志模块"]) + +__all__ = ["auditlog_router"] diff --git a/app/api/v1/auditlog/auditlog.py b/app/api/v1/auditlog/auditlog.py new file mode 100644 index 0000000..ffc7fc6 --- /dev/null +++ b/app/api/v1/auditlog/auditlog.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Query +from tortoise.expressions import Q + +from app.models.admin import AuditLog +from app.schemas import SuccessExtra +from app.schemas.apis import * + +router = APIRouter() + + +@router.get("/list", summary="查看操作日志") +async def get_audit_log_list( + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + username: str = Query("", description="操作人名称"), + module: str = Query("", description="功能模块"), + method: str = Query("", description="请求方法"), + summary: str = Query("", description="接口描述"), + status: int = Query(None, description="状态码"), + start_time: str = Query("", description="开始时间"), + end_time: str = Query("", description="结束时间"), +): + q = Q() + if username: + q &= Q(username__icontains=username) + if module: + q &= Q(module__icontains=module) + if method: + q &= Q(method__icontains=method) + if summary: + q &= Q(summary__icontains=summary) + if status: + q &= Q(status=status) + if start_time and end_time: + q &= Q(created_at__range=[start_time, end_time]) + elif start_time: + q &= Q(created_at__gte=start_time) + elif end_time: + q &= Q(created_at__lte=end_time) + + audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at") + total = await AuditLog.filter(q).count() + data = [await audit_log.to_dict() for audit_log in audit_log_objs] + return SuccessExtra(data=data, total=total, page=page, page_size=page_size) diff --git a/app/api/v1/automation/__init__.py b/app/api/v1/automation/__init__.py new file mode 100644 index 0000000..f7174f8 --- /dev/null +++ b/app/api/v1/automation/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .automation import router + +automation_router = APIRouter() +automation_router.include_router(router, tags=["自动化模块"]) + +__all__ = ["automation_router"] diff --git a/app/api/v1/automation/automation.py b/app/api/v1/automation/automation.py new file mode 100644 index 0000000..f883a79 --- /dev/null +++ b/app/api/v1/automation/automation.py @@ -0,0 +1,188 @@ +# app/api/v1/automation.py + +import logging +from typing import Dict, Any +from app.http_base import unified_resp +from fastapi import APIRouter, Query, Depends +from tortoise.expressions import Q +from app.controllers.automation.scenario import automation_scenario_controller +from app.controllers.automation.task import task_controller +from app.controllers.automation.action import action_controller +from app.schemas.automation import ( + ScenarioCreate, + ScenarioUpdate, + ScenarioCopy, + TaskUpdate, + ActionUpdate, +) +from app.schemas.base import Success, SuccessExtra, Fail +from app.schemas.apis import Paginate +from ..weixin.base import get_weixin_user + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +# ============================ +# Scenario 路由 +# ============================ + +@router.get("/scenario/list", summary="查看场景列表") +@unified_resp +async def list_automation_scenario( + pagination: Paginate = Depends(), + title: str = Query("", description="场景标题,用于搜索"), + is_global: bool = Query(None, description="是否全局场景"), + enabled: bool = Query(None, description="是否启用"), + owner_user_id: str = Query("", description="归属用户ID(非全局时)"), +): + q = Q(visible=True) + if title: + q &= Q(title__contains=title) + if is_global is not None: + q &= Q(is_global=is_global) + if enabled is not None: + q &= Q(enabled=enabled) + if owner_user_id: + q &= Q(owner_user_id=owner_user_id) + + total, scenario_objs = await automation_scenario_controller.list( + page=pagination.page, page_size=pagination.page_size, search=q, order=["-created_at"] + ) + data = [await obj.to_dict() for obj in scenario_objs] + return {"count": total, "lists": data, "page": pagination.page, "page_size": pagination.page_size} + + +@router.get("/scenario/event_and_action", summary="查看场景事件和动作") +@unified_resp +async def list_event_and_action(): + actions = await action_controller.list_automation_actions() + events = await automation_scenario_controller.list_automation_events() + return {"events": events, "actions": actions} + + +@router.get("/scenario/get/{id}", summary="查看场景详情") +@unified_resp +async def get_automation_scenario(id: int): + obj = await automation_scenario_controller.get(id=id) + return await obj.to_dict() + + +@router.post("/scenario/create", summary="创建场景") +@unified_resp +async def create_automation_scenario( + scenario_in: ScenarioCreate, + weixin_user: dict = Depends(get_weixin_user) +): + obj = await automation_scenario_controller.new_scenario(scenario_in, weixin_user.userid) + return await obj.to_dict() + + +@router.post("/scenario/copy", summary="复制场景") +@unified_resp +async def copy_automation_scenario( + scenario_in: ScenarioCopy, +): + def handler(obj_dict: Dict[str, Any]) -> Dict[str, Any]: + obj_dict["title"] = f"复制:{obj_dict['title']}" + obj_dict["enabled"] = False + return obj_dict + + obj = await automation_scenario_controller.copy(id=scenario_in.id, handler=handler) + return await obj.to_dict() + + +@router.post("/scenario/update", summary="更新场景") +@unified_resp +async def update_automation_scenario( + scenario_in: ScenarioUpdate, + weixin_user: dict = Depends(get_weixin_user) +): + obj = await automation_scenario_controller.update_scenario(scenario_in=scenario_in, user_id=weixin_user.userid) + return await obj.to_dict() + + +@router.delete("/scenario/delete", summary="删除场景") +@unified_resp +async def delete_automation_scenario( + id: int = Query(..., description="场景ID"), +): + await automation_scenario_controller.remove_scenario(scenario_id=id) + return Success(msg="Deleted Successfully") + + +# ============================ +# Task 路由 +# ============================ + +@router.get("/task/list", summary="查看待办列表") +@unified_resp +async def list_task( + pagination: Paginate = Depends(), + assignee_user_id: str = Query("", description="指派人用户ID"), + status: str = Query("", description="任务状态"), + related_customer_id: str = Query("", description="关联客户ID"), +): + q = Q() + if assignee_user_id: + q &= Q(assignee_user_id=assignee_user_id) + if status: + q &= Q(status=status) + if related_customer_id: + q &= Q(related_customer_id=related_customer_id) + + total, task_objs = await task_controller.list( + page=pagination.page, page_size=pagination.page_size, search=q + # , order=["-created_at"] + ) + data = [await obj.to_dict() for obj in task_objs] + return {"count": total, "lists": data, "page": pagination.page, "page_size": pagination.page_size} + + +@router.get("/task/get", summary="查看待办详情") +@unified_resp +async def get_task( + id: int = Query(..., description="待办ID"), +): + obj = await task_controller.get(id=id) + return await obj.to_dict() + + +@router.post("/task/update", summary="更新待办") +@unified_resp +async def update_task( + task_in: TaskUpdate, +): + obj = await task_controller.update(id=task_in.id, obj_in=task_in) + return await obj.to_dict() + + +# ============================ +# Action 路由 +# ============================ + +@router.get("/action/list", summary="查看动作列表(按任务)") +@unified_resp +async def list_action( + task_id: int = Query(..., description="所属任务ID"), +): + actions = await action_controller.model.filter(task_id=task_id).all() + data = [await act.to_dict() for act in actions] + return data + + +# @router.get("/action/type", summary="查看动作类型列表(按任务)") +# @unified_resp +# async def list_action_type(): +# actions = await action_controller.list_automation_actions() +# return actions + + +@router.post("/action/update", summary="更新动作(如标记完成)") +@unified_resp +async def update_action( + action_in: ActionUpdate, +): + obj = await action_controller.update(id=action_in.id, obj_in=action_in) + return await obj.to_dict() \ No newline at end of file diff --git a/app/api/v1/base/__init__.py b/app/api/v1/base/__init__.py new file mode 100644 index 0000000..bc9b157 --- /dev/null +++ b/app/api/v1/base/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .base import router + +base_router = APIRouter() +base_router.include_router(router, tags=["基础模块"]) + +__all__ = ["base_router"] diff --git a/app/api/v1/base/base.py b/app/api/v1/base/base.py new file mode 100644 index 0000000..3ba5655 --- /dev/null +++ b/app/api/v1/base/base.py @@ -0,0 +1,103 @@ +from datetime import datetime, timedelta, timezone + +from fastapi import APIRouter + +from app.controllers.user import user_controller +from app.core.ctx import CTX_USER_ID +from app.core.dependency import DependAuth +from app.models.admin import Api, Menu, Role, User +from app.schemas.base import Fail, Success +from app.schemas.login import * +from app.schemas.users import UpdatePassword +from app.settings import settings +from app.utils.jwt import create_access_token +from app.utils.password import get_password_hash, verify_password + +router = APIRouter() + + +@router.post("/access_token", summary="获取token") +async def login_access_token(credentials: CredentialsSchema): + user: User = await user_controller.authenticate(credentials) + await user_controller.update_last_login(user.id) + access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES) + expire = datetime.now(timezone.utc) + access_token_expires + + data = JWTOut( + access_token=create_access_token( + data=JWTPayload( + user_id=user.id, + username=user.username, + is_superuser=user.is_superuser, + exp=expire, + ) + ), + username=user.username, + ) + return Success(data=data.model_dump()) + + +@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth]) +async def get_userinfo(): + user_id = CTX_USER_ID.get() + user_obj = await user_controller.get(id=user_id) + data = await user_obj.to_dict(exclude_fields=["password"]) + data["avatar"] = "https://avatars.githubusercontent.com/u/54677442?v=4" + return Success(data=data) + + +@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth]) +async def get_user_menu(): + user_id = CTX_USER_ID.get() + user_obj = await User.filter(id=user_id).first() + menus: list[Menu] = [] + if user_obj.is_superuser: + menus = await Menu.all() + else: + role_objs: list[Role] = await user_obj.roles + for role_obj in role_objs: + menu = await role_obj.menus + menus.extend(menu) + menus = list(set(menus)) + parent_menus: list[Menu] = [] + for menu in menus: + if menu.parent_id == 0: + parent_menus.append(menu) + res = [] + for parent_menu in parent_menus: + parent_menu_dict = await parent_menu.to_dict() + parent_menu_dict["children"] = [] + for menu in menus: + if menu.parent_id == parent_menu.id: + parent_menu_dict["children"].append(await menu.to_dict()) + res.append(parent_menu_dict) + return Success(data=res) + + +@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth]) +async def get_user_api(): + user_id = CTX_USER_ID.get() + user_obj = await User.filter(id=user_id).first() + if user_obj.is_superuser: + api_objs: list[Api] = await Api.all() + apis = [api.method.lower() + api.path for api in api_objs] + return Success(data=apis) + role_objs: list[Role] = await user_obj.roles + apis = [] + for role_obj in role_objs: + api_objs: list[Api] = await role_obj.apis + apis.extend([api.method.lower() + api.path for api in api_objs]) + apis = list(set(apis)) + return Success(data=apis) + + +@router.post("/update_password", summary="修改密码", dependencies=[DependAuth]) +async def update_user_password(req_in: UpdatePassword): + user_id = CTX_USER_ID.get() + user = await user_controller.get(user_id) + verified = verify_password(req_in.old_password, user.password) + if not verified: + return Fail(msg="旧密码验证错误!") + user.password = get_password_hash(req_in.new_password) + await user.save() + return Success(msg="修改成功") diff --git a/app/api/v1/codegen/__init__.py b/app/api/v1/codegen/__init__.py new file mode 100644 index 0000000..bb23c9a --- /dev/null +++ b/app/api/v1/codegen/__init__.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +from .table import router +from .datasource import router as datasource_router + +codegen_router = APIRouter() +codegen_router.include_router(router, tags=["代码生成模块"]) +codegen_router.include_router(datasource_router, tags=["数据源管理"]) + +__all__ = ["codegen_router"] diff --git a/app/api/v1/codegen/datasource.py b/app/api/v1/codegen/datasource.py new file mode 100644 index 0000000..910bde3 --- /dev/null +++ b/app/api/v1/codegen/datasource.py @@ -0,0 +1,73 @@ +import logging + +from fastapi import APIRouter, Query +from fastapi.exceptions import HTTPException +from tortoise.expressions import Q + +from app.controllers.datasource import datasource_controller +from app.schemas.base import Success, SuccessExtra +from app.schemas.roles import * + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/datasource") + + +@router.get("/list", summary="查看模板列表") +async def list_role( + page: int = Query(1, description="页码"), + limit: int = Query(10, description="每页数量"), + table_name: str = Query("", description="名称,用于查询"), +): + # data = await datasource_controller.load_tables() + # return SuccessExtra(data=data) + q = Q() + if table_name: + q = Q(name__contains=table_name) + total, role_objs = await datasource_controller.list(page=page, page_size=limit, search=q) + data = [await obj.to_dict() for obj in role_objs] + return SuccessExtra(data=data, total=total, page=page, page_size=limit) + + +@router.get("/get/{table_name}", summary="查看") +async def get_role(table_name): + data = await datasource_controller.load_tables(name=table_name) + return SuccessExtra(data=data) + + +@router.post("/create", summary="创建") +async def create_role(role_in: RoleCreate): + if await datasource_controller.is_exist(name=role_in.name): + raise HTTPException( + status_code=400, + detail="The role with this rolename already exists in the system.", + ) + await datasource_controller.create(obj_in=role_in) + return Success(msg="Created Successfully") + + +@router.post("/update", summary="更新") +async def update_role(role_in: RoleUpdate): + await datasource_controller.update(id=role_in.id, obj_in=role_in) + return Success(msg="Updated Successfully") + + +@router.delete("/delete", summary="删除") +async def delete_role( + role_id: int = Query(..., description="ID"), +): + await datasource_controller.remove(id=role_id) + return Success(msg="Deleted Success") + + +@router.get("/authorized", summary="查看权限") +async def get_role_authorized(id: int = Query(..., description="ID")): + role_obj = await datasource_controller.get(id=id) + data = await role_obj.to_dict(m2m=True) + return Success(data=data) + + +@router.post("/authorized", summary="更新权限") +async def update_role_authorized(role_in: RoleUpdateMenusApis): + role_obj = await datasource_controller.get(id=role_in.id) + await datasource_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos) + return Success(msg="Updated Successfully") diff --git a/app/api/v1/codegen/table.py b/app/api/v1/codegen/table.py new file mode 100644 index 0000000..2501e4c --- /dev/null +++ b/app/api/v1/codegen/table.py @@ -0,0 +1,81 @@ +import logging + +from fastapi import APIRouter, Query +from fastapi.exceptions import HTTPException +from tortoise.expressions import Q + +from app.controllers.table import codegen_controller +from app.schemas.base import Success, SuccessExtra +from app.schemas.codegen import * + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/table") + + +@router.get("/page", summary="查看列表") +async def list_role( + page: int = Query(1, description="页码"), + limit: int = Query(10, description="每页数量"), + table_name: str = Query("", description="名称,用于查询"), +): + q = Q() + if table_name: + q = Q(name__contains=table_name) + total, role_objs = await codegen_controller.list(page=page, page_size=limit, search=q) + data = [await obj.to_dict() for obj in role_objs] + return SuccessExtra(data=dict(list=data, total=total, page=page, page_size=limit)) + + +@router.post("/import/{db_connection}", summary="导入表") +async def get_role(db_connection, table: dict = None): + print('db_connection, importTables', db_connection, table) + data = await codegen_controller.import_table(db_connection, importTables=table['table']) + # role_obj = await codegen_controller.get(id=role_id) + return Success(data=data) + + +import json +@router.put("/field/{id}", summary="更新字段") +async def update_fields(id: int, fields_in: dict): + fields_in['fields'] = json.dumps(fields_in['fields']) + await codegen_controller.update(id=id, obj_in=fields_in) + return Success(msg="Updated Successfully") + +@router.get("/preview/{id}", summary="代码预览") +async def code_preview(id: int): + data = await codegen_controller.preview(id) + return Success(data=data) + + +@router.get("/{id}", summary="查看详细的表信息") +async def get_table_detail(id): + table_obj = await codegen_controller.get(id=id) + return Success(data=await table_obj.to_dict()) + +@router.post("/update", summary="更新") +async def update_role(role_in): + await codegen_controller.update(id=role_in.id, obj_in=role_in) + return Success(msg="Updated Successfully") + + +@router.delete("/", summary="删除") +async def delete_role( + batch_ids: dict +): + for id in batch_ids.get('data'): + await codegen_controller.remove(id=id) + return Success(msg="Deleted Success") + + +@router.get("/authorized", summary="查看权限") +async def get_role_authorized(id: int = Query(..., description="ID")): + role_obj = await codegen_controller.get(id=id) + data = await role_obj.to_dict(m2m=True) + return Success(data=data) + + +@router.post("/authorized", summary="更新权限") +async def update_role_authorized(role_in): + role_obj = await codegen_controller.get(id=role_in.id) + await codegen_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos) + return Success(msg="Updated Successfully") diff --git a/app/api/v1/depts/__init__.py b/app/api/v1/depts/__init__.py new file mode 100644 index 0000000..1455b12 --- /dev/null +++ b/app/api/v1/depts/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .depts import router + +depts_router = APIRouter() +depts_router.include_router(router, tags=["部门模块"]) + +__all__ = ["depts_router"] diff --git a/app/api/v1/depts/depts.py b/app/api/v1/depts/depts.py new file mode 100644 index 0000000..a22ef0a --- /dev/null +++ b/app/api/v1/depts/depts.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter, Query + +from app.controllers.dept import dept_controller +from app.schemas import Success +from app.schemas.depts import * + +router = APIRouter() + + +@router.get("/list", summary="查看部门列表") +async def list_dept( + name: str = Query(None, description="部门名称"), +): + dept_tree = await dept_controller.get_dept_tree(name) + return Success(data=dept_tree) + + +@router.get("/get", summary="查看部门") +async def get_dept( + id: int = Query(..., description="部门ID"), +): + dept_obj = await dept_controller.get(id=id) + data = await dept_obj.to_dict() + return Success(data=data) + + +@router.post("/create", summary="创建部门") +async def create_dept( + dept_in: DeptCreate, +): + await dept_controller.create_dept(obj_in=dept_in) + return Success(msg="Created Successfully") + + +@router.post("/update", summary="更新部门") +async def update_dept( + dept_in: DeptUpdate, +): + await dept_controller.update_dept(obj_in=dept_in) + return Success(msg="Update Successfully") + + +@router.delete("/delete", summary="删除部门") +async def delete_dept( + dept_id: int = Query(..., description="部门ID"), +): + await dept_controller.delete_dept(dept_id=dept_id) + return Success(msg="Deleted Success") diff --git a/app/api/v1/finance_parse/__init__.py b/app/api/v1/finance_parse/__init__.py new file mode 100644 index 0000000..d46affe --- /dev/null +++ b/app/api/v1/finance_parse/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .finance_parse import router + +finance_parse_router = APIRouter() +finance_parse_router.include_router(router, tags=["订单备注解析模块"]) + +__all__ = ["finance_parse_router"] diff --git a/app/api/v1/finance_parse/finance_parse.py b/app/api/v1/finance_parse/finance_parse.py new file mode 100644 index 0000000..c191998 --- /dev/null +++ b/app/api/v1/finance_parse/finance_parse.py @@ -0,0 +1,95 @@ +from fastapi import APIRouter, File, UploadFile, Query +from fastapi.responses import FileResponse +from app.schemas import Success, SuccessExtra, Fail +from app.controllers.finance_parse import task_controller, parse_finance_data +import os, time +from app.schemas.task import DecodeTaskParams, DecodeTaskResult, TaskResponse +from app.models.automation import TaskStatus, TaskType +from datetime import datetime +router = APIRouter() + +import shutil +from pathlib import Path + +from app.utils.excel_utils import get_sheets_and_headers + + +@router.post("/upload", summary="上传文件") +async def upload_file(file: UploadFile = File(...)): + # file.filename: 原始文件名 + # file.content_type: MIME 类型 + # file.file: 类文件对象(SpooledTemporaryFile) + + # 保存文件到本地(示例:保存到 ./uploads/) + upload_dir = Path("uploads") + upload_dir.mkdir(exist_ok=True) + filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{file.filename}" + + file_path = upload_dir / filename + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # 获取所有 sheet 名称及其表头 + sheets_headers = get_sheets_and_headers(file_path) + + # # 打印结果 + # for sheet_name, headers in sheets_headers.items(): + # print(f"Sheet: {sheet_name}") + # print(f"Headers: {headers}\n") + + data = { + "filename": filename, + "data": sheets_headers, + } + return Success(data=data) + + +@router.post("/parse", summary="解析文件") +async def parse_file(data: dict): + upload_dir = Path("uploads") + filename = upload_dir / data["filename"] + sheet = data["sheet"] + header = data["header"] + parse_type = data["parse_type"] + taskname = data["filename"] + "_" + sheet + + print(filename, sheet, header, parse_type) + task, task_obj = await task_controller.create_task(name=taskname, obj_in=DecodeTaskParams(filename=str(filename), sheet_name=sheet, header=header, decode_type=parse_type)) + total_amount = 0 + start = time.time() + + try: + result_file_path, total_amount = parse_finance_data( + filename, + target_index=header, + is_horizontal=(parse_type == "horizontal"), + sheet_name=sheet + ) + except Exception as e: + return Fail(msg=f"解析失败: {str(e)}", code=400) + + if not os.path.exists(result_file_path): + return Fail(msg=f"解析结果文件未生成", code=404) + + # 提取原始文件名(不含路径),用于下载时的默认文件名 + download_filename = os.path.basename(result_file_path) + await task_controller.update_task(task_obj, task.id, TaskStatus.SUCCESS, DecodeTaskResult( + filename=str(result_file_path), + spend=time.time() - start, + rows=total_amount, + )) + + return FileResponse( + path=result_file_path, + filename=download_filename, # 浏览器下载时显示的文件名 + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' # .xlsx + ) + +@router.get("/list", summary="获取任务列表") +async def get_tasks(page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + name: str = Query("", description="任务名称,用于查询"), + type: TaskType = Query(None, description="任务类型,用于查询")): + total, tasks = await task_controller.list(name, type, page, page_size) + data = [TaskResponse.from_orm(task).model_dump() for task in tasks] + return SuccessExtra(data=data, total=total, page=page, page_size=page_size) diff --git a/app/api/v1/menus/__init__.py b/app/api/v1/menus/__init__.py new file mode 100644 index 0000000..3e27a24 --- /dev/null +++ b/app/api/v1/menus/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .menus import router + +menus_router = APIRouter() +menus_router.include_router(router, tags=["菜单模块"]) + +__all__ = ["menus_router"] diff --git a/app/api/v1/menus/menus.py b/app/api/v1/menus/menus.py new file mode 100644 index 0000000..308e8ca --- /dev/null +++ b/app/api/v1/menus/menus.py @@ -0,0 +1,63 @@ +import logging + +from fastapi import APIRouter, Query + +from app.controllers.menu import menu_controller +from app.schemas.base import Fail, Success, SuccessExtra +from app.schemas.menus import * + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get("/list", summary="查看菜单列表") +async def list_menu( + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), +): + async def get_menu_with_children(menu_id: int): + menu = await menu_controller.model.get(id=menu_id) + menu_dict = await menu.to_dict() + child_menus = await menu_controller.model.filter(parent_id=menu_id).order_by("order") + menu_dict["children"] = [await get_menu_with_children(child.id) for child in child_menus] + return menu_dict + + parent_menus = await menu_controller.model.filter(parent_id=0).order_by("order") + res_menu = [await get_menu_with_children(menu.id) for menu in parent_menus] + return SuccessExtra(data=res_menu, total=len(res_menu), page=page, page_size=page_size) + + +@router.get("/get", summary="查看菜单") +async def get_menu( + menu_id: int = Query(..., description="菜单id"), +): + result = await menu_controller.get(id=menu_id) + return Success(data=result) + + +@router.post("/create", summary="创建菜单") +async def create_menu( + menu_in: MenuCreate, +): + await menu_controller.create(obj_in=menu_in) + return Success(msg="Created Success") + + +@router.post("/update", summary="更新菜单") +async def update_menu( + menu_in: MenuUpdate, +): + await menu_controller.update(id=menu_in.id, obj_in=menu_in) + return Success(msg="Updated Success") + + +@router.delete("/delete", summary="删除菜单") +async def delete_menu( + id: int = Query(..., description="菜单id"), +): + child_menu_count = await menu_controller.model.filter(parent_id=id).count() + if child_menu_count > 0: + return Fail(msg="Cannot delete a menu with child menus") + await menu_controller.remove(id=id) + return Success(msg="Deleted Success") diff --git a/app/api/v1/msg/__init__.py b/app/api/v1/msg/__init__.py new file mode 100644 index 0000000..e6f5d04 --- /dev/null +++ b/app/api/v1/msg/__init__.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter +from .msg import router as msg_router +from .event import router as event_router + +msg_route_router = APIRouter() +msg_route_router.include_router(msg_router, tags=["消息推送模块"]) +msg_route_router.include_router(event_router, tags=["事件模块"]) + +__all__ = ["msg_route_router"] diff --git a/app/api/v1/msg/event.py b/app/api/v1/msg/event.py new file mode 100644 index 0000000..4854be3 --- /dev/null +++ b/app/api/v1/msg/event.py @@ -0,0 +1,15 @@ +# app/api/wechat.py +from fastapi import APIRouter +from app.schemas.msg import FeishuEvent +from app.http_base import unified_resp + +import logging +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/event") + +# 群聊入口 +@router.post("/feishu", summary="通过群ID获取到群信息以及同步数据到本地") +async def feishu(feishuEvent: FeishuEvent): + return {"challenge": feishuEvent.challenge} + diff --git a/app/api/v1/msg/msg.py b/app/api/v1/msg/msg.py new file mode 100644 index 0000000..6168014 --- /dev/null +++ b/app/api/v1/msg/msg.py @@ -0,0 +1,80 @@ +# app/api/wechat.py +from fastapi import APIRouter, Depends +from app.controllers.msg import msg_controller +from app.controllers.weixin.customer import weixin_customer_controller +from app.schemas.msg import MsgNewOrder, MsgFilter, MsgUpdate +from app.http_base import unified_resp +from app.schemas.apis import Paginate +from tortoise.expressions import Q +from ..weixin.base import get_weixin_user + +import logging +logger = logging.getLogger(__name__) + +router = APIRouter(prefix='/msg') + +# 群聊入口 +@router.post("/new_order", summary="通过群ID获取到群信息以及同步数据到本地") +@unified_resp +async def create_new_order(msg: MsgNewOrder): + + if msg.buyer_id.startswith('*******'): + return {"msg": "不支持的客户类型"} + + if msg.buyer_nick: + weixin_customer = await weixin_customer_controller.model.filter(taobao_name=msg.buyer_nick).first() + elif msg.buyer_id: + weixin_customer = await weixin_customer_controller.model.filter(weixin_id=msg.buyer_id).first() + else: + return {"msg": "buyer_id or buyer_nick is required"} + + if not weixin_customer: + return {"msg": "当前客户未绑定微信账号"} + + logger.info(f'用户新订单: {msg}') + + order_id = msg.order_id + + await msg_controller.new_order(order_id, weixin_customer, is_refund=msg.is_refund) + await msg_controller.sync_msg() + return {"msg": "success"} + + +@router.get("/fill_customer_info", summary="填充客户信息") +@unified_resp +async def fill_customer_info(): + await msg_controller.fill_customer_info() + return {"msg": "success"} + +@router.get("/list", summary="通过群ID获取到群信息以及同步数据到本地") +@unified_resp +async def list_msg(paginate: Paginate = Depends(), order: str = "-id", msg_filter: MsgFilter = Depends(), weixin_user: dict = Depends(get_weixin_user)): + print('paginate', paginate.page, paginate.page_size) + print('msg_filter', msg_filter) + print('weixin_user', weixin_user.username, weixin_user.userid) + # q = Q(owner_id='11') + q = Q(owner_id=weixin_user.userid) + if msg_filter.type is not None: + q &= Q(type=msg_filter.type) + if msg_filter.is_read is not None: + q &= Q(is_read=msg_filter.is_read) + if msg_filter.is_read: + order = '-read_at' + + if msg_filter.is_refund is not None: + q &= Q(detail__contains={"is_refund": 1 if msg_filter.is_refund else 0}) + + total, msg_list = await msg_controller.list(paginate.page, paginate.page_size, order=[order], search=q) + return {"count": total, "lists": [ await msg.to_dict() for msg in msg_list]} + +@router.post("/set_read", summary="设置消息为已读") +@unified_resp +async def set_read(msg: MsgUpdate, weixin_user: dict = Depends(get_weixin_user)): + await msg_controller.set_read(msg.id) + return {"msg": "success"} + +@router.get("/get_order_user_days_before", summary="获取指定天数前的订单用户") +@unified_resp +async def get_order_user_days_before(): + return await msg_controller.get_order_user_days_before(2) + diff --git a/app/api/v1/roles/__init__.py b/app/api/v1/roles/__init__.py new file mode 100644 index 0000000..0b6957e --- /dev/null +++ b/app/api/v1/roles/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .roles import router + +roles_router = APIRouter() +roles_router.include_router(router, tags=["角色模块"]) + +__all__ = ["roles_router"] diff --git a/app/api/v1/roles/roles.py b/app/api/v1/roles/roles.py new file mode 100644 index 0000000..f43a304 --- /dev/null +++ b/app/api/v1/roles/roles.py @@ -0,0 +1,73 @@ +import logging + +from fastapi import APIRouter, Query +from fastapi.exceptions import HTTPException +from tortoise.expressions import Q + +from app.controllers import role_controller +from app.schemas.base import Success, SuccessExtra +from app.schemas.roles import * + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/list", summary="查看角色列表") +async def list_role( + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + role_name: str = Query("", description="角色名称,用于查询"), +): + q = Q() + if role_name: + q = Q(name__contains=role_name) + total, role_objs = await role_controller.list(page=page, page_size=page_size, search=q) + data = [await obj.to_dict() for obj in role_objs] + return SuccessExtra(data=data, total=total, page=page, page_size=page_size) + + +@router.get("/get", summary="查看角色") +async def get_role( + role_id: int = Query(..., description="角色ID"), +): + role_obj = await role_controller.get(id=role_id) + return Success(data=await role_obj.to_dict()) + + +@router.post("/create", summary="创建角色") +async def create_role(role_in: RoleCreate): + if await role_controller.is_exist(name=role_in.name): + raise HTTPException( + status_code=400, + detail="The role with this rolename already exists in the system.", + ) + await role_controller.create(obj_in=role_in) + return Success(msg="Created Successfully") + + +@router.post("/update", summary="更新角色") +async def update_role(role_in: RoleUpdate): + await role_controller.update(id=role_in.id, obj_in=role_in) + return Success(msg="Updated Successfully") + + +@router.delete("/delete", summary="删除角色") +async def delete_role( + role_id: int = Query(..., description="角色ID"), +): + await role_controller.remove(id=role_id) + return Success(msg="Deleted Success") + + +@router.get("/authorized", summary="查看角色权限") +async def get_role_authorized(id: int = Query(..., description="角色ID")): + role_obj = await role_controller.get(id=id) + data = await role_obj.to_dict(m2m=True) + return Success(data=data) + + +@router.post("/authorized", summary="更新角色权限") +async def update_role_authorized(role_in: RoleUpdateMenusApis): + role_obj = await role_controller.get(id=role_in.id) + await role_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos) + return Success(msg="Updated Successfully") diff --git a/app/api/v1/users/__init__.py b/app/api/v1/users/__init__.py new file mode 100644 index 0000000..c3b420e --- /dev/null +++ b/app/api/v1/users/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .users import router + +users_router = APIRouter() +users_router.include_router(router, tags=["用户模块"]) + +__all__ = ["users_router"] diff --git a/app/api/v1/users/users.py b/app/api/v1/users/users.py new file mode 100644 index 0000000..f1248c8 --- /dev/null +++ b/app/api/v1/users/users.py @@ -0,0 +1,87 @@ +import logging + +from fastapi import APIRouter, Body, Query +from tortoise.expressions import Q + +from app.controllers.dept import dept_controller +from app.controllers.user import user_controller +from app.schemas.base import Fail, Success, SuccessExtra +from app.schemas.users import * + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get("/list", summary="查看用户列表") +async def list_user( + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + username: str = Query("", description="用户名称,用于搜索"), + email: str = Query("", description="邮箱地址"), + dept_id: int = Query(None, description="部门ID"), +): + q = Q() + if username: + q &= Q(username__contains=username) + if email: + q &= Q(email__contains=email) + if dept_id is not None: + q &= Q(dept_id=dept_id) + total, user_objs = await user_controller.list(page=page, page_size=page_size, search=q) + data = [await obj.to_dict(m2m=True, exclude_fields=["password"]) for obj in user_objs] + for item in data: + dept_id = item.pop("dept_id", None) + item["dept"] = await (await dept_controller.get(id=dept_id)).to_dict() if dept_id else {} + + return SuccessExtra(data=data, total=total, page=page, page_size=page_size) + + +@router.get("/get", summary="查看用户") +async def get_user( + user_id: int = Query(..., description="用户ID"), +): + user_obj = await user_controller.get(id=user_id) + user_dict = await user_obj.to_dict(exclude_fields=["password"]) + return Success(data=user_dict) + + +@router.post("/create", summary="创建用户") +async def create_user( + user_in: UserCreate, +): + user = await user_controller.get_by_email(user_in.email) + if user: + return Fail(code=400, msg="The user with this email already exists in the system.") + new_user = await user_controller.create_user(obj_in=user_in) + await user_controller.update_roles(new_user, user_in.role_ids) + return Success(msg="Created Successfully") + + +@router.post("/update", summary="用户管理中更新用户") +async def update_user( + user_in: UserUpdate, +): + user = await user_controller.update(id=user_in.id, obj_in=user_in) + await user_controller.update_roles(user, user_in.role_ids) + return Success(msg="Updated Successfully") + +@router.post("/update_user_online", summary="更新用户") +async def update_user_online( + user_in: UserUpdateOnline, +): + user = await user_controller.update(id=user_in.id, obj_in=user_in) + return Success(msg="Updated Successfully") + +@router.delete("/delete", summary="删除用户") +async def delete_user( + user_id: int = Query(..., description="用户ID"), +): + await user_controller.remove(id=user_id) + return Success(msg="Deleted Successfully") + + +@router.post("/reset_password", summary="重置密码") +async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)): + await user_controller.reset_password(user_id) + return Success(msg="密码已重置为123456") diff --git a/app/api/v1/weixin/__init__.py b/app/api/v1/weixin/__init__.py new file mode 100644 index 0000000..c2804d4 --- /dev/null +++ b/app/api/v1/weixin/__init__.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter +from app.controllers.weixin import schedule as _schedule +from .weixin import router +from .weixin_user import router as weixin_user_router + + +weixin_router = APIRouter() +weixin_router.include_router(router, tags=["微信模块"]) +weixin_router.include_router(weixin_user_router, tags=["微信用户模块"]) + +__all__ = ["weixin_router"] diff --git a/app/api/v1/weixin/base.py b/app/api/v1/weixin/base.py new file mode 100644 index 0000000..0c61ca7 --- /dev/null +++ b/app/api/v1/weixin/base.py @@ -0,0 +1,16 @@ +# app/core/dependency.py +from typing import Annotated +from fastapi import Header, HTTPException, Depends +from app.core.ctx import set_ctx_weixin_user +from app.models import WeixinUser + +async def get_weixin_user(token: str = Header(..., description="token验证")) -> WeixinUser: + user = await WeixinUser.filter(userid=token).first() + if not user: + raise HTTPException(status_code=401, detail="Weixin User Authentication failed") + # CTX_USER_ID.set(int(user.id)) + set_ctx_weixin_user(int(user.id), f'{user.username}({user.english_name})') + return user + +DependWeixinUser = Depends(get_weixin_user) +WeixinUserDep = Annotated[WeixinUser, DependWeixinUser] diff --git a/app/api/v1/weixin/requirements.txt b/app/api/v1/weixin/requirements.txt new file mode 100644 index 0000000..bfc3a20 --- /dev/null +++ b/app/api/v1/weixin/requirements.txt @@ -0,0 +1,2 @@ +httpx +pypinyin diff --git a/app/api/v1/weixin/weixin.py b/app/api/v1/weixin/weixin.py new file mode 100644 index 0000000..8d28cf2 --- /dev/null +++ b/app/api/v1/weixin/weixin.py @@ -0,0 +1,95 @@ +# app/api/wechat.py +from fastapi import APIRouter, Query +# from app.controllers import wechat_sdk +# from app.controllers.weixin_script import WeixinUser_controller +from some_sdk.services.binder import xy_client +from some_sdk.wk_weixin_sdk import auth +from some_sdk.wk_weixin_sdk.apis import corp_group, extern_user +from app.http_base import unified_resp + + +import logging +logger = logging.getLogger(__name__) + +router = APIRouter() + +# @router.get("/app-config") +# @unified_resp +# def get_config(): +# return wechat_sdk.get_base_config() + +# 获取企业身份签名 +@router.get("/jssdk-config", summary="获取企业微信 企业身份 JS-SDK 配置") +@unified_resp +async def jssdk_config(type: str = Query(..., description="类型"), url: str = Query(..., description="前端当前页面 URL(不含 #)")): + return await auth.get_jssdk_config(type, url) + +@router.get("/get_access_user_info", summary="获取访问用户身份") +@unified_resp +async def get_access_user_info(code: str = Query(..., description="code为企业成员点击了构造链接之后附加在url中参数")): + return await auth.get_access_user_info(code) + +@router.get("/get_external_group_chat_info", summary="获取外部群聊详细信息") +@unified_resp +async def get_external_group_chat_info(chat_id: str = Query(..., description="群对话ID")): + return await corp_group.get_external_group_chat_info(xy_client, chat_id) + +@router.get("/get_external_user_chat_info", summary="获取外部客户详细信息") +@unified_resp +async def get_external_user_chat_info(user_id: str = Query(..., description="为外部客户的userid")): + return await extern_user.get_external_user_chat_info(user_id) + +# @router.get("/get_external_user_list", summary="获取指定用户的所有外部客户") +# @unified_resp +# async def get_external_user_list(user_id: str = Query(..., description="企业成员的userid")): +# return await wechat_sdk.get_external_user_list(user_id) + +# @router.get("/get_corp_user_id_list", summary="获取指公司所有员工") +# @unified_resp +# async def get_corp_user_id_list(): +# return await wechat_sdk.get_corp_user_id_list() + +# @router.get("/get_follow_user_list", summary="获取配置了客户联系功能的成员列表") +# @unified_resp +# async def get_follow_user_list(): +# return await wechat_sdk.get_follow_user_list() + +# @router.get("/get_follow_user_list", summary="获取配置了客户联系功能的成员列表") +# @unified_resp +# async def get_follow_user_list(): +# return await wechat_sdk.get_follow_user_list() + +# @router.get("/get_groupchat_list", summary="获取指定用户的客户群列表") +# @unified_resp +# async def get_groupchat_list(): +# return await wechat_sdk.get_groupchat_list(['XiYinShuo']) + +# @router.get("/update_all_user", summary="同步所有用户信息") +# @unified_resp +# async def update_all_user(): +# return await WeixinUser_controller.update_all_user() + +# @router.get("/update_all_user_by_group_chat", summary="通过群聊同步用户信息") +# @unified_resp +# async def update_all_user_by_group_chat(): +# return await WeixinUser_controller.update_all_user_by_group_chat() + +# @router.get("/convert_extenal_userid", summary="通过群聊同步用户信息") +# @unified_resp +# async def convert_extenal_userid(): +# return await wechat_sdk.convert_extenal_userid('wmKgOaDQAA37oxGfhBFCAFPqmICXNAZA') + +# @router.get("/update_all_user_by_xingyun", summary="通过外部系统同步用户信息") +# @unified_resp +# async def update_all_user_by_xingyun(): +# return await WeixinUser_controller.update_all_user_by_xingyun() + +# @router.get("/convert_extenal_userid", summary="转换三方应用的extenal_userid") +# @unified_resp +# async def convert_extenal_userid(user_id: str = Query(..., description="客户的userid")): +# return await wechat_sdk.convert_extenal_userid(user_id) + +# @router.get("/get_external_contact_list", summary="获取已服务的外部联系人") +# @unified_resp +# async def get_external_contact_list(cursor: Optional[str] = Query(None, description="客户的userid")): +# return await wechat_sdk.get_external_contact_list(cursor) diff --git a/app/api/v1/weixin/weixin_user.py b/app/api/v1/weixin/weixin_user.py new file mode 100644 index 0000000..5b9c5d4 --- /dev/null +++ b/app/api/v1/weixin/weixin_user.py @@ -0,0 +1,294 @@ +# app/api/wechat.py +from fastapi import APIRouter, Query, BackgroundTasks, Depends +from tortoise.expressions import Q +from app.controllers.weixin.utils import get_buyer_nick_from_group_name +from app.controllers.weixin.group import weixin_group_chat_controller +from app.controllers.weixin.customer import weixin_customer_controller +from app.controllers.weixin.user import weixin_user_controller +from app.controllers.action import action_controller +from app.schemas.weixin import WeixinUserBindInfo, WeixinOrderBindInfo, TriggerWeixinGroupChat, WeixinOrderRemark, WeixinCustomerFilter, WeixinCustomerCreate +from app.http_base import unified_resp +from app.schemas.apis import Paginate +from .base import get_weixin_user + +from .base import WeixinUserDep +from app.models.msg import ActionType + +from datetime import datetime, timedelta +from app.utils.event_task import event_manager, EventType +from app.core.cache import cache_if +# from app.schemas.crm import CrmBindInfoCreate, CrmBindInfoUpdate + +import logging +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="") + +# 废弃 +@router.post("/list_user_join_group", summary="加载用户加入的群聊列表") +@unified_resp +async def list_user_join_group(customer: WeixinCustomerCreate, weixin_user: dict = Depends(get_weixin_user)): + print(f'list_user_join_group, customer: {customer}') + # user_join_group = await weixin_group_chat_controller.list_user_join_group(customer.xingyun_id) + user_join_group = await weixin_customer_controller.get_user_detail(customer.weixin_id) + return user_join_group + +@router.get("/list_customer", summary="加载客户列表") +@unified_resp +async def list_msg(paginate: Paginate = Depends(), order: str = "-id", customer_filter: WeixinCustomerFilter = Depends(), weixin_user: dict = Depends(get_weixin_user)): + print('paginate', paginate.page, paginate.page_size) + print('customer_filter', customer_filter) + print('weixin_user', weixin_user.username, weixin_user.userid) + + q = Q() + data = {} + if customer_filter.order_id is not None: + q &= Q(order_id=customer_filter.order_id) + data['order_id'] = customer_filter.order_id + if customer_filter.shop_name is not None: + q &= Q(shop_name__contains=customer_filter.shop_name) + data['shop_name'] = customer_filter.shop_name + if customer_filter.union_id is not None: + # 是或者的关系 weixin_id\xingyun_id\taobao_id\union_id + q &= (Q(weixin_id=customer_filter.union_id) | Q(xingyun_id=customer_filter.union_id) | Q(taobao_id=customer_filter.union_id) | Q(union_id=customer_filter.union_id)) + data['union_id'] = customer_filter.union_id + if customer_filter.union_name is not None: + # 是或者的关系 weixin_username\xingyun_name\taobao_name + q &= (Q(weixin_name__contains=customer_filter.union_name) | Q(xingyun_name__contains=customer_filter.union_name) | Q(taobao_name__contains=customer_filter.union_name)) + data['union_name'] = customer_filter.union_name + if customer_filter.xingyun_tags is not None: + q &= Q(xingyun_tags__contains={"tagName": customer_filter.xingyun_tags}) + data['xingyun_tags'] = customer_filter.xingyun_tags + if customer_filter.has_order is not None: + q &= Q(order_id__isnull=not customer_filter.has_order) + data['has_order'] = customer_filter.has_order + + # 打印查询条件明文 + print('raw query', data) + + total, user_list = await weixin_customer_controller.list(paginate.page, paginate.page_size, order=[order], search=q) + + lists = [] + for user in user_list: + user_info = user.dump_dict() + lists.append(user_info) + user_info['user_join_group'] = await weixin_customer_controller.get_user_detail(user.weixin_id) + + return {"count": total, "lists": lists} + + +# 群聊入口 +@router.get("/get_user_info", summary="通过群ID获取到群信息以及同步数据到本地") +@unified_resp +async def get_external_group_chat_info(userid: str): + weixin_user = await weixin_user_controller.model.filter(userid=userid).first() + return weixin_user.to_dict() + +# 群聊入口 +@router.get("/group/get_external_group_chat_info", summary="通过群ID获取到群信息以及同步数据到本地") +@unified_resp +async def get_external_group_chat_info(chat_id: str, background_tasks: BackgroundTasks): + group_info = await weixin_group_chat_controller.get_external_group_chat_info(chat_id) + + if group_info['detect_update']: + await event_manager.publish_async(EventType.GROUP_CHAT_UPDATED, group_info, background_tasks) + + return group_info + + +@router.get("/group/get_order_relative_user_list_by_weixin_groupid", summary="通过群ID获取订单相关用户列表") +@unified_resp +async def get_order_relative_user_list_by_weixin_groupid(chat_id: str = Query(..., description="群ID"), background_tasks: BackgroundTasks = None): + async with cache_if(f'order:chat:{chat_id}', ttl=60*3) as cache: + if cache.hit: + order_list = cache.value + if order_list: + order_list[0] = await weixin_group_chat_controller.get_user_detail_by_order(order_id=order_list[0]['ctid']) + else: + order_list = await _get_order_relative_user_list_by_weixin_groupid(chat_id, background_tasks) + cache.set(order_list) + return order_list + +async def _get_order_relative_user_list_by_weixin_groupid(chat_id: str = Query(..., description="群ID"), background_tasks: BackgroundTasks = None): + group_info = await weixin_group_chat_controller.get_external_group_chat_info(chat_id) + logger.info(f'group_info: {group_info}') + + if group_info['detect_update'] or True: + await event_manager.publish_async(EventType.GROUP_CHAT_UPDATED, group_info, background_tasks) + + name = group_info.get('name') + logger.debug(f'群聊名称: {name} chat_id: {chat_id}') + + user_order_list = [] + + external_member_list = group_info['external_member_list'] + if external_member_list: + userid_list = [member['userid'] for member in external_member_list if member.get('userid')] + logger.debug(f'userid_list: {userid_list}') + customer_list = await weixin_customer_controller.model.filter(weixin_id__in=userid_list, taobao_id__isnull=False).all() + for customer in customer_list: + buyer = customer.taobao_name or customer.taobao_id + if not buyer: continue + + logger.debug(f'buyer: {buyer}') + result, order_list = await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_userid(customer.weixin_id) + if not result: continue + user_order_list = order_list + break + # return await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_group_name(buyer_nick) + # return await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_group_buyer_id(buyer_id) + + if not user_order_list: + logger.warning(f'群内成员没有订单,将根据群聊名称 {name} 来获取订单相关用户列表') + assert name, "当前群聊未命名" + buyer_nick = get_buyer_nick_from_group_name(name) + logger.debug(f'name: {name}; buyer_nick: {buyer_nick}') + assert buyer_nick, "当前群聊未命名" + user_order_list = await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_group_name(buyer_nick) + + if group_info['detect_update']: + try: + await action_controller.check_create_group_action_is_done(group_info, user_order_list[0]['shop_name'] if user_order_list else None) + except Exception as e: + logger.exception(f'检查创建群聊动作是否完成失败: {e}') + + return user_order_list + + + +# @router.get("/group/load_xingyun_group_info", summary="通过群ID获取订单相关用户列表") +# @unified_resp +# async def load_xingyun_group_info(): +# return await weixin_group_chat_controller.load_xingyun_group_info() + + +@router.get("/get_user_detail_by_order", summary="通过订单ID获取订单相关用户详情") +@unified_resp +async def get_user_detail_by_order(order_id: str): + return await weixin_group_chat_controller.get_user_detail_by_order(order_id) + + +@router.get("/get_erp_order_log", summary="通过订单ID获取订单日志") +@unified_resp +async def get_erp_order_log(order_id: str, login_user: WeixinUserDep): + result = await weixin_group_chat_controller.get_erp_order_log(order_id) + try: + await action_controller.new_action(ActionType.VIEW_ERP_LOG, login_user, order_id=order_id) + except Exception as e: + logger.exception(e) + logger.error(f'员工操作日志记录失败') + return result + + +# 通过私聊入口 +@router.get("/get_order_relative_user_list_by_weixin_userid", summary="通过用户ID获取订单相关用户列表") +@unified_resp +async def get_order_relative_user_list_by_weixin_userid(userid: str, background_tasks: BackgroundTasks): + result, order_list = await weixin_group_chat_controller.get_order_relative_user_list_by_weixin_userid(userid) + if not result: + await event_manager.publish_async(EventType.OPEN_PERSONAL_CHAT, {"userid": userid}, background_tasks) + + return order_list + + +@router.post("/bind_order", summary="绑定订单") +@unified_resp +async def bind_order(bind_in: WeixinOrderBindInfo, background_tasks: BackgroundTasks, login_user: WeixinUserDep): + bind_in.userid = bind_in.userid.strip() + bind_in.order_id = bind_in.order_id.strip() + logger.info(f'【绑定订单】userid: {bind_in.userid}; order_id: {bind_in.order_id}') + + old_info = await weixin_customer_controller.model.filter(weixin_id=bind_in.userid, order_id=bind_in.order_id).first() + assert not old_info, "不用为客户绑定相同订单" + + result, orders = await weixin_customer_controller.bind_order(bind_in) + try: + await action_controller.new_action(ActionType.BIND_ORDER, login_user, **bind_in.model_dump(exclude_unset=True)) + if orders and bind_in.remark: + orders = [{ + "title": order.get("title"), + "ctid": order.get("ctid"), + "trade_no": order.get("trade_no"), + "remark": order.get("remark", ""), + } for order in orders] + + need_monitor_order_list = await weixin_customer_controller.remark_order_by_order_id(remark=bind_in.remark, orders=orders) + await action_controller.new_action(ActionType.WRITE_REMARK, login_user, order_id=bind_in.order_id, remark=bind_in.remark, orders=need_monitor_order_list, done=len(need_monitor_order_list) == 0) + except Exception as e: + logger.exception(e) + logger.error(f'员工操作日志记录失败') + + # 用户触发绑定时,及时同步到星云有客中 + await event_manager.publish_async(EventType.BIND_ORDER_FOR_USER, orders[0] if orders else {}, background_tasks) + return result + + +@router.post("/remark_order", summary="为订单添加额外备注") +@unified_resp +async def remark_order(remark_in: WeixinOrderRemark, login_user: WeixinUserDep): + if not remark_in.remark: + logger.warning(f'订单备注为空,将不执行任何操作') + return False + + try: + need_monitor_order_list = await weixin_customer_controller.remark_order_by_order_id(remark=remark_in.remark, order_id=remark_in.order_id) + await action_controller.new_action(ActionType.WRITE_REMARK, login_user, order_id=remark_in.order_id, remark=remark_in.remark, orders=need_monitor_order_list, done=len(need_monitor_order_list) == 0) + except Exception as e: + logger.exception(e) + logger.error(f'员工操作日志记录失败') + return bool(need_monitor_order_list) + + +@router.post("/bind_user", summary="用户绑定客户") +@unified_resp +async def bind_user(bind_info: WeixinUserBindInfo, login_user: WeixinUserDep): + result = await weixin_customer_controller.bind_user(bind_info=bind_info) + try: + await action_controller.new_action(ActionType.BIND_USER, login_user, **bind_info.model_dump(exclude_unset=True)) + except Exception as e: + logger.exception(e) + logger.error(f'员工操作日志记录失败') + return result + + +@router.post("/bind_inner_user", summary="用户绑定内部公司成员(erp和企微架构不同)") +@unified_resp +async def bind_inner_user(bind_info: WeixinUserBindInfo, login_user: WeixinUserDep): + result = await weixin_user_controller.bind_user(bind_info) + try: + await action_controller.new_action(ActionType.BIND_USER, login_user, **bind_info.model_dump(exclude_unset=True)) + except Exception as e: + logger.exception(e) + logger.error(f'员工操作日志记录失败') + return result + +@router.get("/load_recent_user_from_xingyun", summary="加载最近N天的客户数据") +@unified_resp +async def load_user_from_xingyun(days_range: int = Query(3, description="加载最近N天的客户数据")): + + now_time = datetime.now() + start_time = (now_time - timedelta(days=days_range)).replace(hour=0, minute=0, second=0, microsecond=0) + end_time = now_time + timedelta(days=1) + return await weixin_customer_controller.load_user_from_xingyun(add_time_start=start_time, add_time_end=end_time) + +@router.get("/load_all_user_from_xingyun", summary="加载所有客户数据") +@unified_resp +async def load_all_user_from_xingyun(background_tasks: BackgroundTasks): + await event_manager.publish_async(EventType.SYNC_XINGYUN_CONTACT_INFO, {}, background_tasks) + return True + +@router.post("/trigger_create_group_chat", summary="触发创建群聊") +@unified_resp +async def trigger_create_group_chat(group_chat: TriggerWeixinGroupChat, login_user: WeixinUserDep): + try: + await action_controller.new_action(ActionType.CREATE_GROUP, login_user, **group_chat.model_dump(exclude_unset=True)) + if group_chat.order_id: + username = login_user.english_name or login_user.nickname + username = username.split('-印刷')[0] + remark = f'企微联系{username}拉群' + need_monitor_order_list = await weixin_customer_controller.remark_order_by_order_id(remark=remark, order_id=group_chat.order_id) + await action_controller.new_action(ActionType.WRITE_REMARK, login_user, order_id=group_chat.order_id, remark=remark, orders=need_monitor_order_list, done=len(need_monitor_order_list) == 0) + except Exception as e: + logger.exception(e) + logger.error(f'员工操作日志记录失败') + return True diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py new file mode 100644 index 0000000..f12bef9 --- /dev/null +++ b/app/controllers/__init__.py @@ -0,0 +1,2 @@ +from .role import role_controller as role_controller +from .user import user_controller as user_controller diff --git a/app/controllers/action.py b/app/controllers/action.py new file mode 100644 index 0000000..8e3636c --- /dev/null +++ b/app/controllers/action.py @@ -0,0 +1,109 @@ +from app.core.crud import CRUDBase +from app.schemas.msg import ( + ActionCreate, + ActionUpdate, +) +from app.models.automation import Action, ActionType +from app.models.weixin import WeixinCustomer, CustomerGroup, WeixinGroupChat, WeixinUser +from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user +from some_sdk.services.binder import lintao_client +from app.controllers.weixin.customer import weixin_customer_controller +from datetime import datetime + +import logging +logger = logging.getLogger(__name__) + +class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]): + def __init__(self): + super().__init__(model=Action) + + async def new_action(self, type: ActionType, user, done=False, **kwargs): + action = ActionCreate(userid=user.userid, username=f'{user.username}({user.english_name})', done=done, type=type, detail=kwargs) + return await self.create(action) + + async def check_create_group_action_is_done(self, group_info, shop_name: str = None): + name = group_info.get('name') + chat_id = group_info.get('chat_id') + + # 完成建群操作 + create_group_action = await self.model.filter(type=ActionType.CREATE_GROUP, done=False, detail__contains={"group_name": name}).order_by('-id').first() + if not create_group_action: return + + create_group_action_detail = create_group_action.detail + create_group_action_detail.update({ + "chat_id": chat_id, + }) + + create_group_action.done = True + create_group_action.done_at = datetime.now() + create_group_action.result = group_info + create_group_action.detail = create_group_action_detail + await create_group_action.save() + + # 完成建群操作后,将群成员添加到客户群中 + weixin_group = await WeixinGroupChat.filter(chat_id=chat_id).first() + if not weixin_group: return + + staff = await WeixinUser.filter(userid=create_group_action.userid).first() + + external_user_ids = create_group_action.detail.get('external_user_ids') or [] + for userid in external_user_ids: + customer = await WeixinCustomer.filter(weixin_id=userid).first() + if not customer: continue + + membership = await CustomerGroup.create( + staff_userid=create_group_action.userid, + customer_userid=customer.weixin_id, + group_chatid=chat_id, + staff=staff, + customer=customer, + group=weixin_group, + shop_name=shop_name, + order_id=create_group_action.detail.get('order_id'), + join_time=create_group_action.created_at, + ) + logger.info(f'【建群成功】保存客户的群聊 {name}({chat_id}) 信息') + + async def check_remark_action_is_done(self): + actions = await self.model.filter(done=False, type=ActionType.WRITE_REMARK) + logger.info(f'检查备注操作是否完成,监控列表中:共有{len(actions)}条') + + update_list = [] + for action in actions: + action_info = action.detail + order_id = action_info.get("order_id") + remark = action_info.get("remark") + if not order_id: continue + + done = False + action_result = [] + + if remark: + async for order in get_order_relative_user(lintao_client, trade_no=order_id): + + try: + await weixin_customer_controller.remark_order(order, remark=remark) + if order.get('title'): + logger.info(f'【监控列表中,订单被领单】重新为订单 {order.get("trade_no")} 打上备注 {remark}') + # 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉 + done = True + action_result.append({ + "trade_no": order.get("trade_no"), + "title": order.get("title"), + "remark": remark, + }) + except Exception as e: + logger.exception(f'【监控列表中,订单被领单】重新为订单为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}') + + if done or not remark: + action.done = True + action.done_at = datetime.now() + action.result = action_result + update_list.append(action) + + if update_list: + await self.model.bulk_update(update_list, fields=['done', 'result', 'done_at']) + + +action_controller = ActionController() + diff --git a/app/controllers/api.py b/app/controllers/api.py new file mode 100644 index 0000000..33e5682 --- /dev/null +++ b/app/controllers/api.py @@ -0,0 +1,46 @@ +from fastapi.routing import APIRoute + +from app.core.crud import CRUDBase +import logging +from app.models.admin import Api +from app.schemas.apis import ApiCreate, ApiUpdate + +logger = logging.getLogger(__name__) + +class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]): + def __init__(self): + super().__init__(model=Api) + + async def refresh_api(self): + from app import app + + # 删除废弃API数据 + all_api_list = [] + for route in app.routes: + # 只更新有鉴权的API + if isinstance(route, APIRoute) and len(route.dependencies) > 0: + all_api_list.append((list(route.methods)[0], route.path_format)) + delete_api = [] + for api in await Api.all(): + if (api.method, api.path) not in all_api_list: + delete_api.append((api.method, api.path)) + for item in delete_api: + method, path = item + logger.debug(f"API Deleted {method} {path}") + await Api.filter(method=method, path=path).delete() + + for route in app.routes: + if isinstance(route, APIRoute) and len(route.dependencies) > 0: + method = list(route.methods)[0] + path = route.path_format + summary = route.summary + tags = list(route.tags)[0] + api_obj = await Api.filter(method=method, path=path).first() + if api_obj: + await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save() + else: + logger.debug(f"API Created {method} {path}") + await Api.create(**dict(method=method, path=path, summary=summary, tags=tags)) + + +api_controller = ApiController() diff --git a/app/controllers/automation/action.py b/app/controllers/automation/action.py new file mode 100644 index 0000000..c1263f4 --- /dev/null +++ b/app/controllers/automation/action.py @@ -0,0 +1,82 @@ +# app/controllers/automation.py (继续追加) +from app.core.crud import CRUDBase +from typing import List, Optional, Dict, Any +from app.models.automation import Action +from app.models.enums import ActionType +from app.schemas.automation import ActionCreate, ActionUpdate +from app.utils.common import transform_pydantic_to_list +from app.schemas.automation import NotifyAction + +class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]): + def __init__(self): + super().__init__(model=Action) + + async def get_pending_actions_by_task(self, task_id: int) -> List[Action]: + """获取某任务中未完成的动作""" + return await self.model.filter(task_id=task_id, done=False).all() + + async def list_automation_actions(self) -> List[Dict[str, Any]]: + """获取动作类型映射""" + + ActionTypeMap = [ + {"value": ActionType.CREATE_GROUP, "label": "创建群聊", "automation": True, "allowAutoExecute": False, "autoExecute": False, "enable": True}, + {"value": ActionType.VIEW_ERP_LOG, "label": "查看ERP日志", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True}, + {"value": ActionType.BIND_USER, "label": "绑定用户", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True}, + {"value": ActionType.BIND_ORDER, "label": "绑定订单", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True}, + {"value": ActionType.WRITE_REMARK, "label": "写ERP额外备注", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True}, + {"value": ActionType.SEND_WECHAT_NOTIFY, "label": "发送通知", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True, "schema": transform_pydantic_to_list(NotifyAction)}, + {"value": ActionType.CLEAN_GROUP, "label": "清理群聊", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True}, + {"value": ActionType.SET_GROUP_ADMIN, "label": "设置群管理员", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True}, + ] + + return [item for item in ActionTypeMap if item["automation"]] + + async def mark_action_done( + self, + action_id: int, + userid: str, + username: str, + result: Dict[str, Any], + notes: Optional[str] = None + ) -> Action: + """标记动作为已完成""" + update_data = ActionUpdate( + done=True, + done_at=self.model._meta.db_fields.get("done_at").to_db_value(None, None), # 实际用 datetime.now() + userid=userid, + username=username, + result=result, + notes=notes + ) + # Note: 建议在 service 层处理 done_at = datetime.utcnow() + return await self.update(action_id, update_data) + + async def create_actions_for_task(self, task_id: int, actions_def: List[Dict]) -> List[Action]: + """为任务批量创建动作实例""" + actions = [] + for act in actions_def: + action_in = ActionCreate( + task_id=task_id, + type=act.get("type"), + detail=act.get("detail", {}), + done=False + ) + action = await self.create(action_in) + actions.append(action) + return actions + + async def trigger_scenarios(self, scenario_list: List[Action], event_data: Dict[str, Any]) -> None: + """触发场景执行""" + for scenario in scenario_list: + if not scenario.enabled: + continue + + for action in scenario.actions: + if not action.enabled: + continue + + if action.type == ActionType.SEND_WECHAT_NOTIFY: + await self.trigger_scenarios_by_action(action, event_data) + + +action_controller = ActionController() \ No newline at end of file diff --git a/app/controllers/automation/scenario.py b/app/controllers/automation/scenario.py new file mode 100644 index 0000000..4911d23 --- /dev/null +++ b/app/controllers/automation/scenario.py @@ -0,0 +1,222 @@ +# app/controllers/automation.py + +from typing import List, Optional, Tuple, Dict, Any +from app.core.crud import CRUDBase +from app.models.automation import Scenario, ScenarioTriggerIndex, ScenarioScope +from app.schemas.automation import ScenarioCreate, ScenarioUpdate +from app.utils.event_task import event_manager + +import logging +logger = logging.getLogger(__name__) + +class ScenarioController(CRUDBase[Scenario, ScenarioCreate, ScenarioUpdate]): + def __init__(self): + super().__init__(model=Scenario) + + async def update_trigger_index(self, scenario: Scenario, user_id: str) -> Scenario: + """更新场景""" + add_list = [] + event_set = set() + for condition in scenario.trigger.get('conditions') or []: + event_name = condition.get('event_name', '') + if event_name and event_name in event_set: + continue + event_set.add(event_name) + + orm = ScenarioTriggerIndex( + owner_user_id=user_id, + scenario_id=scenario.id, + is_global=scenario.is_global, + event_name=condition.get('event_name', ''), + enabled=condition.get('enabled', True), + scope=condition.get('scope', ScenarioScope.PERSONAL), + ) + add_list.append(orm) + + if add_list: + await ScenarioTriggerIndex.bulk_create(add_list) + return True + + return False + + async def new_scenario(self, scenario_in: ScenarioCreate, user_id: str) -> Scenario: + """创建新场景""" + obj = await self.create(scenario_in) + + # 更新触发条件 + if obj.trigger: + await self.update_trigger_index(obj, user_id) + + return obj + + async def update_scenario(self, scenario_in: ScenarioUpdate, user_id: str) -> Scenario: + """更新场景""" + obj = await self.update(id=scenario_in.id, obj_in=scenario_in) + + # 更新触发条件 + if obj.trigger: + await ScenarioTriggerIndex.filter(scenario_id=obj.id).delete() + await self.update_trigger_index(obj, user_id) + + return obj + + async def remove_scenario(self, scenario_id: int) -> None: + """删除场景""" + await self.model.filter(id=scenario_id).delete() + await ScenarioTriggerIndex.filter(scenario_id=scenario_id).delete() + return True + + async def list_automation_events(self) -> List[Dict[str, Any]]: + """获取所有自动化事件""" + return [a for a in event_manager.automation_event_handlers if a.get("automation_event")] + + async def get_global_scenarios(self, enabled: bool = True) -> List[Scenario]: + """获取所有启用的全局场景""" + return await self.model.filter(is_global=True, enabled=enabled).all() + + async def get_user_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]: + """获取某用户的启用个人场景""" + return await self.model.filter(owner_user_id=user_id, is_global=False, enabled=enabled).all() + + async def get_applicable_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]: + """获取对某用户生效的所有场景(全局 + 个人)""" + global_scenarios = await self.get_global_scenarios(enabled=enabled) + personal_scenarios = await self.get_user_scenarios(user_id, enabled=enabled) + return global_scenarios + personal_scenarios + + +class AutomationScenarioController(ScenarioController): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + async def find_active_scenarios(self, event_name: str, event_data: Dict[str, Any]) -> List[Scenario]: + """查找所有实际激活的场景配置""" + scenario_index_list = await ScenarioTriggerIndex.filter(enabled=True, event_name=event_name).all() + + scenario_list = [] + if scenario_index_list: + scenario_list = await self.model.filter(id__in=[s.scenario_id for s in scenario_index_list]).all() + + logger.info(f'find_active_scenarios: {len(scenario_list)}') + + active_scenarios = [] + for scenario in scenario_list: + result, reason = await self.check_is_applicable(scenario, event_data) + if result: + active_scenarios.append((scenario, reason)) + + return active_scenarios + + async def check_is_applicable(self, scenario: Scenario, event_data: Dict[str, Any]) -> tuple[bool, str]: + """检查场景是否适用于当前事件""" + + trigger = scenario.trigger + # 如果没有条件,默认返回True,即只要事件发生就会激活场景 + if not trigger or not trigger.get('conditions'): + logger.info(f'scenario {scenario.id} trigger without conditions, always return True') + return True, '没有设置具体的触发条件,事件发生就激活场景' + + logic = trigger.get('logic', 'and') + conditions = trigger.get('conditions', []) + + if logic == 'or': + for condition in conditions: + condition = condition.get('condition', {}) + + result, reason = self.check_condition(condition, event_data) + if result: + logger.info(f'scenario {scenario.id} trigger condition {condition} return True, reason: {reason}') + return True, reason + return False, '所有触发条件都不满足' + elif logic == 'and': + + reason_list = [] + for condition in conditions: + condition = condition.get('condition', {}) + + result, reason = self.check_condition(condition, event_data) + if not result: + logger.info(f'scenario {scenario.id} trigger condition {condition} return False, reason: {reason}') + return False, f'触发条件 {condition} 不满足, 原因: {reason}' + else: + reason_list.append(reason) + + return True, f'{", ".join(reason_list)}' + + raise ValueError(f"Invalid logic operator: {logic}") + + def check_condition(self, condition: Dict[str, Any], event_data: Dict[str, Any]) -> tuple[bool, str]: + """检查单个条件是否满足""" + def find_field_val(field: str, event_data: Dict[str, Any]) -> Any: + """递归查找字段值""" + if '.' in field: + parts = field.split('.') + current = event_data + for part in parts: + if isinstance(current, list): + # current = current[int(part)] + result = [] + for item in current: + # find_field_val + field_val = find_field_val(part, item) + if field_val is not None: + result.append(field_val) + return result + elif isinstance(current, dict): + current = current.get(part, None) + else: + return None + return current + return event_data.get(field, None) + + if not condition or not condition.get('field', ''): + logger.error(f'scenario check_condition: field is empty, 当做无效条件处理') + return True, '没有设置具体的触发条件,事件发生就激活场景' + + field = condition.get('field', '') + + value = condition.get('value', '') + operator = condition.get('operator', '') + field_val = find_field_val(field, event_data) + if field_val is None: + logger.error(f'scenario check_condition: field {field} value is None, event_data: {event_data}') + return False, f'触发条件 {condition} 字段 {field} 不存在' + + # 字符串比较 + if operator in ['contains', 'not_contains']: + # if not isinstance(field_val, str): + # return False, f'字段 {field} 不是字符串类型' + if operator == 'contains' and value in field_val: + return True, f'字段 {field} 包含 {value}' + if operator == 'not_contains' and value not in field_val: + return True, f'字段 {field} 不包含 {value}' + return False, f'字段 {field} 不满足 {operator} {value}' + + # 数字、时间比较 + if operator in ['gt', 'gte', 'lt', 'lte', 'eq', 'ne']: + if not isinstance(field_val, (int, float, str)): + return False, f'字段 {field} 不是数字或时间类型' + try: + field_val = float(field_val) + value = float(value) + except ValueError: + return False, f'字段 {field} 不是数字或时间类型' + + if operator == 'gt' and field_val > value: + return True, f'字段 {field} 大于 {value}' + if operator == 'gte' and field_val >= value: + return True, f'字段 {field} 大于等于 {value}' + if operator == 'lt' and field_val < value: + return True, f'字段 {field} 小于 {value}' + if operator == 'lte' and field_val <= value: + return True, f'字段 {field} 小于等于 {value}' + if operator == 'eq' and field_val == value: + return True, f'字段 {field} 等于 {value}' + if operator == 'ne' and field_val != value: + return True, f'字段 {field} 不等于 {value}' + + return False, f'字段 {field} 不满足 {operator} {value}' + + + +automation_scenario_controller = AutomationScenarioController() \ No newline at end of file diff --git a/app/controllers/automation/task.py b/app/controllers/automation/task.py new file mode 100644 index 0000000..74992d0 --- /dev/null +++ b/app/controllers/automation/task.py @@ -0,0 +1,77 @@ +# app/controllers/automation.py (继续追加) +from app.core.crud import CRUDBase +from typing import List, Optional, Dict, Any, Tuple + +from tortoise.expressions import Q +from app.models.automation import Task, Scenario, ScenarioScope +from app.schemas.automation import TaskCreate, TaskUpdate +from datetime import datetime, timedelta + +class TaskController(CRUDBase[Task, TaskCreate, TaskUpdate]): + def __init__(self): + super().__init__(model=Task) + + async def get_user_tasks( + self, + user_id: str, + status: Optional[str] = None, + page: int = 1, + page_size: int = 20 + ) -> Tuple[int, List[Task]]: + """分页获取某用户的待办(支持按状态过滤)""" + query = Q(assignee_user_id=user_id) + if status: + query &= Q(status=status) + return await self.list(page=page, page_size=page_size, search=query, order=["-created_at"]) + + async def get_tasks_by_customer(self, customer_id: str) -> List[Task]: + """获取与某客户相关的所有待办(用于侧边栏上下文)""" + return await self.model.filter(related_customer_id=customer_id).order_by("-created_at").all() + + async def create_from_scenario( + self, + scenario: Scenario, + event_data: Optional[Dict[str, Any]] = None, + assignee_user_id: Optional[str] = None, + assignee_username: Optional[str] = None, + reason: Optional[str] = None, + ) -> Task: + """根据场景模板创建待办任务""" + # 计算截止时间(示例:场景中定义的 due_days 天后) + due_at = scenario.due_days and (datetime.now() + timedelta(days=scenario.due_days)) or None + + owner_user_id = None + if scenario.scope == ScenarioScope.ALL: + owner_user_id = ScenarioScope.ALL + elif scenario.scope == ScenarioScope.PERSONAL: + owner_user_id = scenario.owner_user_id + + task_in = TaskCreate( + title=scenario.title, + reason=reason, + notes=scenario.notes, + event_data=event_data or {}, + source_scenario=scenario, + source_scenario_id=scenario.id, + owner_user_id=owner_user_id, + assignee_user_id=assignee_user_id, + assignee_username=assignee_username, + due_at=due_at, + status="pending", + auto_closeable=True, + ) + return await self.create(task_in) + + async def auto_close_by_event(self, event_type: str, payload: Dict[str, Any]) -> List[Task]: + """ + 根据事件自动关闭匹配的待办(用于自动消除) + 例如:当 group_id 匹配且动作含 create_group 时,关闭待办 + """ + # 示例逻辑:后续可扩展为规则引擎 + closed_tasks = [] + # 这里可遍历 pending 任务,检查 content 中的动作是否已被事件满足 + # 为简化,此处留作扩展点 + return closed_tasks + + +task_controller = TaskController() \ No newline at end of file diff --git a/app/controllers/crm.py b/app/controllers/crm.py new file mode 100644 index 0000000..334b5cf --- /dev/null +++ b/app/controllers/crm.py @@ -0,0 +1,235 @@ +import asyncio +from typing import List, Dict, Any + +import concurrent # 添加这行 +import concurrent.futures + +from app.core.crud import CRUDBase +from app.schemas.crm import ( + CrmCustomerCreate, + CrmCustomerUpdate, + CrmCustomerCreate, + CrmBindInfoCreate, + CrmBindInfoUpdate, +) + +from some_sdk.services import binder as binder_service +from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user +from app.models.weixin import CrmCustomer, CrmBindInfo +from tortoise.expressions import Subquery +from tortoise.functions import Count # 导入 Count 函数 + +import logging +logger = logging.getLogger(__name__) + +def split_generator(iterable, chunk_size=50): + """ + 将可迭代对象拆分为多个子列表 + """ + chunk = [] + for item in iterable: + chunk.append(item) + if len(chunk) == chunk_size: + yield chunk + chunk = [] + if chunk: + yield chunk + +class CrmCustomerController(CRUDBase[CrmCustomer, CrmCustomerCreate, CrmCustomerUpdate]): + def __init__(self): + super().__init__(model=CrmCustomer) + + async def get_unbound_customers_method_four_alt(self, limit: int = 50): + # 创建子查询 + bound_ids_subquery = CrmBindInfo.all().values_list('customer_id', flat=True) + + # 使用正确的子查询语法 + unbound_customers = await self.model.filter( + cid__not_in=Subquery(bound_ids_subquery) + ).limit(limit) + return unbound_customers + + async def bind_crm_all_customer_by_db(self): + + # 定义一个函数来执行单个同步的 bind_shop_info 调用 + def sync_fetch_bind_info(user_data: Dict[str, Any]): + # 同步调用 binder_service + all_users = [] + for crm_user, user_list_from_bind in binder_service.bind_shop_info([user_data]): + all_users.extend(user_list_from_bind) + return all_users + + parsed_set = set() + while True: + # 获取一批未绑定的用户 ORM 对象 + unbound_orm_objects = await self.get_unbound_customers_method_four_alt(limit=50) + if not unbound_orm_objects: + break + print(f'获取到 {len(unbound_orm_objects)} 个待绑定的 ORM 对象', flush=True) + expected_cids = {obj.cid for obj in unbound_orm_objects if obj.cid is not None and obj.cid not in parsed_set} + if not expected_cids: break + user_data_list = [await obj.to_dict() for obj in unbound_orm_objects if obj.cid not in parsed_set] + [parsed_set.add(cid) for cid in expected_cids] + print(f'待绑定用户数(转换后):{len(user_data_list)}', flush=True) + await self._bind_crm_all_customer_by_db(user_data_list, expected_cids, sync_fetch_bind_info) + + async def _bind_crm_all_customer_by_db(self, not_in_db_list: List[Dict[str, Any]], expected_cids: set, sync_fetch_bind_info): + print(f'待绑定用户数(传入列表):{len(not_in_db_list)}', flush=True) + + # --- 并发执行所有同步的 bind_shop_info 调用 --- + # 使用 asyncio.to_thread (Python 3.9+) 或 run_in_executor 将同步函数移到线程池执行 + # 限制并发线程数很重要,避免创建过多线程 + max_workers = 10 # 限制线程池大小,根据系统性能调整 + loop = asyncio.get_event_loop() + + # 创建任务列表 + tasks = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + for user_data in not_in_db_list: + # 将同步函数提交到线程池执行,并返回一个 Future + # asyncio.run_in_executor 将 Future 包装成 awaitable 的协程 + task = loop.run_in_executor(executor, sync_fetch_bind_info, user_data) + tasks.append(task) + + # 等待所有线程池任务完成 + all_bind_lists_results = await asyncio.gather(*tasks, return_exceptions=True) + + # 处理结果,将异常和正常结果分开 + all_users_to_process = [] + for result in all_bind_lists_results: + if isinstance(result, Exception): + logger.exception(f"Thread pool task failed: {result}") + # 可以选择跳过或记录错误 + else: + all_users_to_process.extend(result) + + # --- 去重逻辑 --- + unique_users_to_process = {} + for user in all_users_to_process: + customer_id = user.get("customer_id", 0) + tenant_name = user.get("tenant_name", 0) + platform = user.get("platform", 0) + + key = f"{customer_id}_{tenant_name}_{platform}" + if key not in unique_users_to_process: + unique_users_to_process[key] = user + + print(f"去重后待处理用户数: {len(unique_users_to_process)}", flush=True) + + # --- 并发处理去重后的用户 --- + # 这部分可以保持原有逻辑,因为它已经是并发的了 + # 但要注意,如果同时有多个任务尝试插入相同的 customer_id,可能会有并发问题 + # 可以考虑使用数据库的 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 等特性 + + async def process_user(user_obj: Dict[str, Any]): + try: + user_type = user_obj.get("type", 0) + tenant_name = user_obj.get("tenant_name", 0) + customer_id = user_obj.get("customer_id", 0) + platform = user_obj.get("platform", 0) + + if user_type == 'staff': + print(f'绑定员工:{user_obj}', flush=True) + else: + print(f'绑定客户:{user_obj}, Customer ID: {customer_id}', flush=True) + # --- 检查数据库中是否已存在绑定 --- + existing_bind = await CrmBindInfo.filter(customer_id=customer_id, platform=platform, tenant_name=tenant_name).first() + if not existing_bind: + bind_instance = CrmBindInfo.create_bind(user_obj) + await bind_instance.save() + print(f"客户 {customer_id} 绑定成功", flush=True) + else: + print(f"客户 {customer_id} 已存在绑定", flush=True) + logger.warning(f"Attempted to bind customer_id {customer_id} which already exists in CrmBindInfo.") + except Exception as e: + logger.exception(e) + print(f"处理用户 {user_obj.get('customer_id', 'unknown')} 时出错: {e}", flush=True) + + max_concurrent_db_tasks = 10 # 可以独立控制数据库操作的并发数 + semaphore = asyncio.Semaphore(max_concurrent_db_tasks) + + async def process_user_with_semaphore(user_obj): + async with semaphore: + return await process_user(user_obj) + + tasks = [process_user_with_semaphore(user_obj) for user_obj in unique_users_to_process.values()] + await asyncio.gather(*tasks, return_exceptions=True) + + print("当前批次所有用户绑定任务完成", flush=True) + + async def bind_crm_all_customer(self): + """ + 绑定CRM客户和微信用户 + """ + + await self.bind_crm_all_customer_by_db() + + for shop in binder_service.iter_shop(): + shopId = shop.get("shopId", "") + shopName = shop.get("shopName", "") + print(f"开始处理店铺:{shopName}", flush=True) + + not_in_db_list = [] + + shop_info = { + 'shopId': shopId, + 'shopName': shopName, + } + + is_done = False + user_list_iter = binder_service.iter_list_trade_user(binder_service.xy_client, shopId=shopId) + for user_list in split_generator(user_list_iter): + if is_done: break + # 提取所有待检查的 cid + all_cids = [str(user.get("cid")) for user in user_list] + # print(user_list[0]) + # break + + # 只查询 user_list 中存在的 cid + in_objs = await self.model.filter(cid__in=all_cids) + + # 提取已存在的 cid + in_db_cid_set = {user.cid for user in in_objs} + + print(f'已处理用户数:{len(in_db_cid_set)}', flush=True) + + # 筛选待处理用户 + not_in_db_list = [user for user in user_list if user.get("cid") not in in_db_cid_set] + for user in not_in_db_list: + id = user.pop("id", None) + # print('user', user) + + # if not not_in_db_list: + # is_done = True + # break + + print(f'待处理用户数:{len(not_in_db_list)}', flush=True) + + if not_in_db_list: + model_list = [self.model(**CrmCustomerCreate(**user).model_dump(exclude_unset=True)) for user in not_in_db_list] + await self.model.bulk_create(model_list) + + if not not_in_db_list: + print(f'店铺:{shopName},无待处理用户', flush=True) + continue + + await self.bind_crm_all_customer_by_db() + + async def bind_user(self, bind_info: CrmBindInfoCreate): + """ + 绑定用户 + """ + bind_info_dict = bind_info.model_dump(exclude_unset=True) + platform = bind_info_dict.get("platform", None) + platform_id = bind_info_dict.get("platform_id", None) + bind_info_dict.pop("id", None) + + existing_bind = await CrmBindInfo.filter(platform=platform, platform_id=platform_id).first() + if existing_bind: + await CrmBindInfo.filter(id=existing_bind.id).update(**bind_info_dict) + else: + obj = CrmBindInfo(**bind_info_dict) + await obj.save() + return {"message": "用户绑定成功"} + +crm_customer_controller = CrmCustomerController() diff --git a/app/controllers/datasource.py b/app/controllers/datasource.py new file mode 100644 index 0000000..29b9dc9 --- /dev/null +++ b/app/controllers/datasource.py @@ -0,0 +1,41 @@ +from typing import List + +from app.core.crud import CRUDBase +from app.models.admin import Api, Menu, Datasource +from app.schemas.codegen import DatasourceCreate, DatasourceUpdate, DatasourceInfo +from app.utils.db import DatabaseInfo + +class DatasourceController(CRUDBase[Datasource, DatasourceCreate, DatasourceUpdate]): + def __init__(self): + super().__init__(model=Datasource) + + async def load_tables(self, name: str) -> list[DatasourceInfo]: + # datasource_obj = await self.model.filter(name=name).first() + # if not datasource_obj: + # raise HTTPException(status_code=400, detail="数据源不存在") + + with DatabaseInfo( + host="lt.330770.xyz", + port=3307, + user="root", + password="rap_sky", + database="rpa" + ) as db: + tables = db.get_all_tables() + + return tables + + + async def update_datasources(self, datasource: Datasource, menu_ids: List[int], api_infos: List[dict]) -> None: + await datasource.menus.clear() + for menu_id in menu_ids: + menu_obj = await Menu.filter(id=menu_id).first() + await datasource.menus.add(menu_obj) + + await datasource.apis.clear() + for item in api_infos: + api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first() + await datasource.apis.add(api_obj) + + +datasource_controller = DatasourceController() diff --git a/app/controllers/dept.py b/app/controllers/dept.py new file mode 100644 index 0000000..0dbb5fa --- /dev/null +++ b/app/controllers/dept.py @@ -0,0 +1,86 @@ +from tortoise.expressions import Q +from tortoise.transactions import atomic + +from app.core.crud import CRUDBase +from app.models.admin import Dept, DeptClosure +from app.schemas.depts import DeptCreate, DeptUpdate + + +class DeptController(CRUDBase[Dept, DeptCreate, DeptUpdate]): + def __init__(self): + super().__init__(model=Dept) + + async def get_dept_tree(self, name): + q = Q() + # 获取所有未被软删除的部门 + q &= Q(is_deleted=False) + if name: + q &= Q(name__contains=name) + all_depts = await self.model.filter(q).order_by("order") + + # 辅助函数,用于递归构建部门树 + def build_tree(parent_id): + return [ + { + "id": dept.id, + "name": dept.name, + "desc": dept.desc, + "order": dept.order, + "parent_id": dept.parent_id, + "children": build_tree(dept.id), # 递归构建子部门 + } + for dept in all_depts + if dept.parent_id == parent_id + ] + + # 从顶级部门(parent_id=0)开始构建部门树 + dept_tree = build_tree(0) + return dept_tree + + async def get_dept_info(self): + pass + + async def update_dept_closure(self, obj: Dept): + parent_depts = await DeptClosure.filter(descendant=obj.parent_id) + for i in parent_depts: + print(i.ancestor, i.descendant) + dept_closure_objs: list[DeptClosure] = [] + # 插入父级关系 + for item in parent_depts: + dept_closure_objs.append(DeptClosure(ancestor=item.ancestor, descendant=obj.id, level=item.level + 1)) + # 插入自身x + dept_closure_objs.append(DeptClosure(ancestor=obj.id, descendant=obj.id, level=0)) + # 创建关系 + await DeptClosure.bulk_create(dept_closure_objs) + + @atomic() + async def create_dept(self, obj_in: DeptCreate): + # 创建 + if obj_in.parent_id != 0: + await self.get(id=obj_in.parent_id) + new_obj = await self.create(obj_in=obj_in) + await self.update_dept_closure(new_obj) + + @atomic() + async def update_dept(self, obj_in: DeptUpdate): + dept_obj = await self.get(id=obj_in.id) + # 更新部门关系 + if dept_obj.parent_id != obj_in.parent_id: + await DeptClosure.filter(ancestor=dept_obj.id).delete() + await DeptClosure.filter(descendant=dept_obj.id).delete() + await self.update_dept_closure(dept_obj) + # 更新部门信息 + dept_obj.update_from_dict(obj_in.model_dump(exclude_unset=True)) + await dept_obj.save() + + @atomic() + async def delete_dept(self, dept_id: int): + # 删除部门 + obj = await self.get(id=dept_id) + obj.is_deleted = True + await obj.save() + # 删除关系 + await DeptClosure.filter(descendant=dept_id).delete() + + +dept_controller = DeptController() diff --git a/app/controllers/finance_parse/__init__.py b/app/controllers/finance_parse/__init__.py new file mode 100644 index 0000000..166621b --- /dev/null +++ b/app/controllers/finance_parse/__init__.py @@ -0,0 +1,43 @@ +from .finance_parse import parse_finance_data +from tortoise.expressions import Q +from tortoise.transactions import atomic + +from app.models.automation import Task, TaskStatus +from app.schemas.task import TaskModel, DecodeTaskParams, DecodeTaskResult + +class TaskController(object): + async def get_task(self, name, type): + q = Q() + # 获取所有未被软删除的任务 + q &= Q(is_deleted=False) + if name: q &= Q(name__contains=name) + if type: q &= Q(type=type) + all_tasks = await self.model.filter(q).order_by("id") + return all_tasks + + async def list(self, name, type, page: int = 1, page_size: int = 10, order: list[str] = ["id"]): + q = Q() + if name: q &= Q(name__contains=name) + if type: q &= Q(type=type) + query = Task.filter(q) + return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order) + + @atomic() + async def create_task(self, name: str, obj_in: DecodeTaskParams): + task_obj = TaskModel.create_decode_task(name, obj_in) + task_obj.result = task_obj.result or {} + obj = Task(**task_obj.model_dump()) + await obj.save() + return obj, task_obj + + @atomic() + async def update_task(self, obj_in: TaskModel, task_id: int, status: TaskStatus, result: DecodeTaskResult): + task_obj = await Task.get(id=task_id) + obj_in.status = status + obj_in.set_result(result) + + # 更新任务信息 + task_obj.update_from_dict(obj_in.model_dump(exclude_unset=True)) + await task_obj.save() + +task_controller = TaskController() diff --git a/app/controllers/finance_parse/finance_parse.py b/app/controllers/finance_parse/finance_parse.py new file mode 100644 index 0000000..de26b9b --- /dev/null +++ b/app/controllers/finance_parse/finance_parse.py @@ -0,0 +1,616 @@ +# spacy_training/scripts/predict.py +import re +import cn2an +import os.path +from tqdm import tqdm +from pathlib import Path + + +# 单位与连接符定义 +MAX_ITEM_OFFSET = 10 +UNIT_PATTERN = "个|张|片|块|条|幅|根" +SIZE_UNITS_PATTERN = r'mm|cm|dm|m' +SIZE_JOIN_PATTERN = "xX×" +NUMS_PATTERN = r'\d+(?:\.\d+)' # 修复:原缺少 ? 导致整数不匹配 +CNT_PATTERN = rf'{NUMS_PATTERN}?[^克+\s\d-]*' +EXCEPT_TEXT = "解析失败" +INDEX_COL = '序号' + +base_colume = ['解析备注', '尺寸备注', '描述', '异常信息'] +mapping_size = { + "width_mm": "长", + "height_mm": "宽", + # "original_size": "原始尺寸", + # "total_quantity": "总数量", + "style_count": "款数", + "quantity_per_style": "数量", + "unit": "单位", + "描述": "描述", + "解析备注": "解析备注", + "size_unit": "尺寸单位", + "exception_msg": '异常信息' + } + +import spacy +import pandas as pd + +# 将 doc 转换为字典 +def doc_to_dict(doc): + return { + "text": doc.text, + "entities": [ + { + "text": ent.text, + "label": ent.label_, + "start": ent.start_char, + "end": ent.end_char + } + for ent in doc.ents + ], + } + +def predict_file(df, model_path="./spacy_training/model", key='备注'): + print(model_path) + nlp = spacy.load(model_path) + if "sentencizer" not in nlp.pipe_names: + nlp.add_pipe("sentencizer") + + # 统一表头空格 + df.columns = df.columns.str.strip() + key = key.strip() + + for i, row in tqdm(df.iterrows(), total=len(df)): + order = str(row[key]).strip() + base_row = {key: val for key, val in row.items()} # 复制原行数据 + yield order, base_row, doc_to_dict(nlp(order)) + # doc = nlp(text) + # print(f"\n🔤 文本: {text}") + # import json; print(json.dumps(doc_to_dict(doc), indent=4, ensure_ascii=False)) + + # break + +from dataclasses import dataclass + +@dataclass +class StructuredOrder: + key: str + width_mm: float + height_mm: float + original_size: str + style_count: int + quantity_per_style: float + unit: str + total_quantity: float + original_qty: str + size_position: list + qty_position: list + exception_msg: str + calc_type: str + size_unit: str + meta: dict + +def expand_to_structured(raw_pairs, size_unit=''): + unit_map = { + '百': 100, '千': 1000, '万': 10000, + '百万': 1000000, '千万': 10000000, '亿': 100000000, + } + + UNIT_REGEX = re.compile(rf'({UNIT_PATTERN})$') + exception_msg = [] + + def extract_multiplier(text): + """ + 从字符串中提取数值乘数,支持: + - 阿拉伯数字 + 中文单位:3.5万 → 35000 + - 纯中文数字 + 单位:三万五千 → 35000 + - 纯中文:两千万 → 20000000 + """ + # 去除末尾单位(保留前面的部分) + clean = UNIT_REGEX.sub('', text.strip()) + + # 如果去单位后为空,原字符串可能是纯单位(如“万”),默认系数为1 + if not clean: + matched_unit = UNIT_REGEX.search(text) + if matched_unit: + unit = matched_unit.group(1) + return unit_map.get(unit, 1.0) + return 1.0 + + # 尝试直接转阿拉伯数字 + try: + return float(clean) + except ValueError: + pass + + # 尝试转中文数字(如“三万五千”虽然不合理,但“三万”可以) + try: + # 注意:cn2an 可以直接处理“三万五千”这种 + num = cn2an.cn2an(clean, "smart") # smart 模式支持混合写法 + if isinstance(num, (int, float)): + # 检查原字符串是否有单位后缀(比如“三万”中的“万”已被去除,需补回) + matched_unit = UNIT_REGEX.search(text) + if matched_unit: + unit = matched_unit.group(1) + num *= unit_map[unit] + return float(num) + except Exception: + exception_msg.append(f"尺寸解析异常:{text}") + pass + + return 1.0 + + def parse_size(size_str): + join_str = ''.join(re.findall(f"[{SIZE_JOIN_PATTERN}]+", size_str)) + if len(join_str) == 0 or re.search('比例|等高|等比|等宽', size_str): + result = f'异常值:{size_str}' + return result, result, size_unit + size_str = size_str.replace(join_str, "x") + match = re.match(rf'({NUMS_PATTERN}?)([a-z]+)?[{SIZE_JOIN_PATTERN}]({NUMS_PATTERN}?)([a-z]+)?', size_str, re.IGNORECASE) + if not match: + return None, None, size_unit + w_val, w_unit, h_val, h_unit = match.groups() + unit = (w_unit or h_unit or size_unit).lower() + conv = {'m': 1000, 'dm': 100, 'cm': 10, 'mm': 1} + if unit not in conv: + print(f'未知单位: {unit} size_str:{size_str} size_unit:{size_unit}') + exception_msg.append(f"尺寸解析异常:{size_str}, 未知单位: {unit}") + return None, None, unit + w_mm = conv[unit] * float(w_val) + h_mm = conv[unit] * float(h_val) + return round(w_mm, 3), round(h_mm, 3), unit + + def parse_quantity(qty_str): + qty_str = "".join(qty_str.split()) + multi_match = re.match(( + rf'[共计]?(.+?)款\w*?([共计]?[\d.一二两三四五六七八九十百千万亿]+)({UNIT_PATTERN})' + ), qty_str, re.IGNORECASE) + if multi_match: + style_part, qty_part = multi_match.group(1), multi_match.group(2) + if qty_part[0] in '共计': + qty_part = qty_part[1:] + calc_type = 'total' + else: + calc_type = 'single' + style_count = int(extract_multiplier(style_part)) + quantity_per_style = int(extract_multiplier(qty_part)) + unit = multi_match.group(3) + + return {"style_count": style_count, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type} + + single_match = re.match(rf'(.+?)({UNIT_PATTERN})', qty_str, re.IGNORECASE) + if single_match: + qty_part = single_match.group(1) + if qty_part[0] in '共计': + qty_part = qty_part[1:] + calc_type = 'total' + else: + calc_type = 'single' + quantity_per_style = int(extract_multiplier(qty_part)) + unit = single_match.group(2) + return {"style_count": 1, "quantity_per_style": quantity_per_style, "unit": unit, "calc_type": calc_type} + + return {"style_count": 0, "quantity_per_style": 0, "unit": "个"} + + structured = [] + + for size_info, qty_info in raw_pairs: + s_start, s_end, s_text, *_ = size_info + q_start, q_end, q_text, *_ = qty_info + + if s_text == EXCEPT_TEXT: + width_mm, height_mm = EXCEPT_TEXT, EXCEPT_TEXT + else: + size_parsed = parse_size(s_text) + if size_parsed: + width_mm, height_mm, size_unit = size_parsed + else: + width_mm, height_mm, size_unit = None, None, None + + if q_text == EXCEPT_TEXT: + style_count, quantity_per_style, unit = 0, EXCEPT_TEXT, EXCEPT_TEXT + else: + qty_parsed = parse_quantity(q_text) + style_count, quantity_per_style, unit = qty_parsed["style_count"], qty_parsed["quantity_per_style"], qty_parsed["unit"] + + structured.append(StructuredOrder( + key=f"{s_start}{s_end}{s_text}", + width_mm=width_mm, + height_mm=height_mm, + original_size=s_text, + style_count=style_count, + quantity_per_style=quantity_per_style, + unit=unit, + total_quantity=style_count * quantity_per_style if style_count and style_count else None, + original_qty=q_text, + size_position=[s_start, s_end], + qty_position=[q_start, q_end], + exception_msg="; ".join(exception_msg), + calc_type=qty_parsed.get('calc_type', None), + size_unit=size_unit, + meta={ + "size_text_except": s_text if s_text == EXCEPT_TEXT or (not width_mm and not height_mm) else None, + "qty_text_except": q_text if q_text == EXCEPT_TEXT else None, + } + )) + + return structured + + +def decode(doc_dict_list, is_horizontal=False): + ent_group = { + # "单号": ["单号"], + "刮刮膜尺寸": ["刮刮膜尺寸"], + # "产品": ["产品"], + # "材质": ["材质"], + # "工艺": ["工艺"], + # "用户": ["用户信息"], + # "加急": ["加急"], + "数量": ["尺寸", "数量"], + } + columns = list(ent_group.keys()) + columns.remove('数量') + columns.extend(base_colume) + group_reflect = {v: k for k, vs in ent_group.items() for v in vs} + result_items = [] + exception_list = [] + + for doc_dict in doc_dict_list: + sentence = doc_dict["text"] + entities = doc_dict["entities"] + + is_except = False + + # 强关联实体需要进行组合处理 + # ======================================== 实体分组 ======================================== + ent_group_dict = {k: [] for k in ent_group.keys()} + base_ent_group_dict = {} + + for ent in entities: + start, end, label, text = ent["start"], ent["end"], ent["label"], ent["text"] + group_name = group_reflect[label] + ent_group_dict[group_name].append((start, end, text, label)) + base_ent_group_dict[group_name] = text + + result = [] + + # 解析出尺寸和数量的组合 + # ======================================== 解析尺寸和数量 ======================================== + group_list = [] + size_and_qty_list = [[], []] + + # 数量需要和尺寸进行组合处理 + base_ent_group_dict.pop("数量", None) + for cnt_item in ent_group_dict["数量"]: + size_info, qty_info = size_and_qty_list + label = cnt_item[-1] + if label == '数量': + qty_info.append(cnt_item) + elif label == '尺寸': + start, end, text, _label = cnt_item + if f'-({text[:2]}' in sentence: + continue + if qty_info: + group_list.append(size_and_qty_list) + size_and_qty_list = [[], []] + size_info, qty_info = size_and_qty_list + size_info.append(cnt_item) + + # 存在数量和尺寸 + if size_and_qty_list[0] or size_and_qty_list[1]: + group_list.append(size_and_qty_list) + + # 如果只有一对尺寸,有可能反过来描述 + if len(group_list) == 2: + _1, qty_info = group_list[0] + size_info, _2 = group_list[1] + + if not _1 and not _2: + group_list[:] = [[qty_info, size_info]] + + # print(f'订单中尺寸和数量组合:{group_list}') + + # 解析出尺寸和数量的组合 + # ======================================== 解析长、宽、款数、数量 ======================================== + size_unit = '' + for size_info, qty_info in group_list: + units = set(''.join(re.findall(SIZE_UNITS_PATTERN, ent[2])) for ent in size_info) + units = [unit for unit in units if unit] + if len(units) == 1: + size_unit = units[0] + + size_and_qty_parsed_result = [] + for size_info, qty_info in group_list: + size_len, qty_len = len(size_info), len(qty_info) + if size_len == 0: + print(f'解析异常,尺寸为空,数量为{qty_info},原文:{sentence}', entities) + is_except = True + # 起始位置、结束位置、尺寸文本、尺寸标签 + qty_info = [(0, 0, EXCEPT_TEXT, '数量')] + if qty_len == 0: + print(f'解析异常,数量为空,尺寸为{size_info},原文:{sentence}', entities) + is_except = True + # 起始位置、结束位置、尺寸文本、尺寸标签 + size_info = [(0, 0, EXCEPT_TEXT, '尺寸')] + # continue + + # print('size_info, qty_info', size_info, qty_info) + if size_len == qty_len: + decode_desc = "单尺寸、单款式描述的订单" if size_len == 1 else "多尺寸、多款式描述一一匹配的订单" + + for size, qty in zip(size_info, qty_info): + expand = expand_to_structured([(size, qty)], size_unit) or [] + exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')] + if exceptions: print("解析异常", exceptions, '原文', sentence, entities) + for item in expand: + parsed_item = item.__dict__.copy() + parsed_item['描述'] = f"{size[2]}|{qty[2]}" + parsed_item['解析备注'] = decode_desc + parsed_item['size_len'] = size_len + parsed_item['qty_len'] = qty_len + size_and_qty_parsed_result.append(parsed_item) + elif size_len>1 and qty_len>1: + + if size_len > qty_len: + total_qty = [] + for size, qty in zip(size_info, qty_info): + expand = expand_to_structured([(size, qty)], size_unit) or [] + for item in expand: + total_qty.append(item.style_count) + + if size_len == sum(total_qty): + decode_desc = "多尺寸、多款式描述:多款式描述和尺寸相等的订单" + item = [] + for idx, qty in enumerate(total_qty): + item.extend(qty_info[idx] for _ in range(qty)) + print('解析:', size_info, qty_info, item) + # breakpoint() + qty_info = item + qty_len = len(qty_info) + else: + # qty_info = [] + decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}" + else: + # qty_info = [] + decode_desc = f"匹配异常:多尺寸、多款式描述的订单: {size_info} {qty_info}" + + for size, qty in zip(size_info, qty_info): + expand = expand_to_structured([(size, qty)], size_unit) or [] + exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')] + if exceptions: print("解析异常", exceptions, '原文', sentence, entities) + for item in expand: + parsed_item = item.__dict__.copy() + parsed_item['style_count'] = 1 + parsed_item['描述'] = f"{size[2]}|{qty[2]}" + parsed_item['解析备注'] = decode_desc + parsed_item['size_len'] = size_len + parsed_item['qty_len'] = qty_len + size_and_qty_parsed_result.append(parsed_item) + else: + # 多尺寸-单数量、单尺寸-多数量 告警多尺寸-多数量 + iter_item = ((size, qty) for size in size_info for qty in qty_info) + if size_len > 1 and qty_len == 1: + decode_desc = "多尺寸、单款式描述的订单" + elif size_len == 1 and qty_len > 1: + decode_desc = "单尺寸、多款式描述的订单" + else: + decode_desc = f"异常:多尺寸、多款式描述的订单: {size_info} {qty_info}" + + for item in iter_item: + size, qty = item + expand = expand_to_structured([(size, qty)], size_unit) or [] + exceptions = [item.meta for item in expand if item.meta.get('size_text_except') or item.meta.get('qty_text_except')] + if exceptions: print("解析异常", exceptions, '原文', sentence, entities) + for item in expand: + parsed_item = item.__dict__.copy() + parsed_item['描述'] = f"{size[2]}|{qty[2]}" + parsed_item['解析备注'] = decode_desc + parsed_item['size_len'] = size_len + parsed_item['qty_len'] = qty_len + size_and_qty_parsed_result.append(parsed_item) + + result.extend(size_and_qty_parsed_result) + + for item in result: + # print('item', item) + new_item = base_ent_group_dict.copy() + for key in mapping_size: + col_name = mapping_size[key] + new_item[col_name] = item[key] + columns.append(col_name) + size_len = item.pop('size_len', None) + qty_len = item.pop('qty_len', None) + calc_type = item.pop('calc_type', None) + + if size_len > 1 and calc_type == 'total': + new_item[f'数量'] = f"警告:异常值(总计值:{new_item[f'数量']})" + + if size_len == 1 or size_len == qty_len: + pass + # new_item[f'解析备注'] = '单尺寸的订单' + else: + if size_len == new_item['款数']: + new_item[f'款数'] = 1 + # new_item[f'解析备注'] = '尺寸数量和款数相同的订单' + elif new_item['款数'] == 1: + new_item[f'款数'] = 1 + # new_item[f'解析备注'] = '单款的订单' + else: + new_item[f'款数'] = "异常值:款数和尺寸数不一致" + new_item[f'数量'] = "异常值:款数和尺寸数不一致" + # new_item[f'解析备注'] = '多尺寸并且和款数不同的订单' + + result_items.append(new_item) + # print(json.dumps(new_item, indent=4, ensure_ascii=False)) + + if not result: + if base_ent_group_dict: + base_ent_group_dict['是否异常'] = is_except + base_ent_group_dict[f'解析备注'] = '未解析到实际尺寸的订单1' + result_items.append(base_ent_group_dict.copy()) + else: + result_items.append({ + "是否异常": is_except, + "解析备注": "未解析到任何实体" + }) + + return exception_list, columns, result_items + + +def parse_finance_data(file_path, target_index, is_horizontal, sheet_name="Sheet1"): + + df = pd.read_excel(file_path, dtype=str, sheet_name=sheet_name) + total_amount = len(df) + + items = predict_file(df, './app/resources/models/订单尺寸识别/best_model', target_index) + # items = predict_file(file_path, './resources/models/lintao/best_model', target_index) + exception_list, results, outputs_columns = [], [], [] + origin_item = None + + parsed_dict = {} + mapping_size_values = mapping_size.values() + + for text, origin_item, item in items: + + decode_result = [] + + base_info = {} + result_index = parsed_dict.get(f"{text}_index") + if result_index: + exceptions, columns = [], [] + try: + decode_item = parsed_dict[text][result_index] + for key in origin_item: + if key in mapping_size_values: + continue + decode_item[key] = origin_item[key] + decode_item['尺寸备注'] = '多组尺寸解析' + parsed_dict[f"{text}_index"] += 1 + continue + except IndexError: + print(f'解析到的尺寸不足:{text},{result_index}, {parsed_dict[text]}') + base_item = parsed_dict[text][0] + for col in base_colume: + base_info[col] = base_item.get(col, '') + base_info["长"] = '异常值:没有解析到那么多尺寸' + + decode_items = [] + parsed_dict[f"{text}_index"] += 1 + else: + exceptions, columns, decode_items = decode([item], is_horizontal) or [] + + # if '(250731202428388557)' in text: + # print(result_index, text, exceptions, columns, decode_items) + # breakpoint() + + if len(columns) > len(outputs_columns): + outputs_columns = columns + + if len(decode_items) > 1: + for item in decode_items: + item['尺寸备注'] = '多组尺寸解析' + + for idx, decode_item in enumerate(decode_items): + base_row = origin_item.copy() + if idx and INDEX_COL in base_row: base_row[INDEX_COL] = '' + # base_row = origin_item.copy() if idx == 0 else {} + base_row.update(decode_item) + is_except = base_row.pop('是否异常', False) + if is_except: + for col in base_colume: + origin_item[col] = base_row.get(col, '') + decode_result.append(origin_item) + exception_list.append(base_row) + else: + decode_result.append(base_row) + if not decode_items: + if base_info: + origin_item.update(base_info) + + decode_result.append(origin_item) + else: + if is_horizontal: + new_decode_result = decode_result[0].copy() + for idx, item in enumerate(decode_result[1:], 1): + for key in mapping_size_values: + new_key = f"{key}{idx}" + new_decode_result[new_key] = item[key] + if new_key not in outputs_columns: + outputs_columns.append(new_key) + decode_result = [new_decode_result] + + if text not in parsed_dict: + parsed_dict[text] = decode_result + parsed_dict[f"{text}_index"] = 0 + parsed_dict[f"{text}_index"] += 1 + + # if decode_result: + # results.append(decode_result[0]) + if len(decode_items) > 1: + for item in decode_result[1:]: + item['尺寸备注'] = '复制新增:多组尺寸解析' + results.extend(decode_result) + + # break + + print('横向解析', outputs_columns) + if origin_item: + # outputs_columns. + _outputs_columns = list(origin_item.keys()) + outputs_columns + col_set = set() + outputs_columns = [] + + for col in _outputs_columns: + if col not in col_set: + col_set.add(col) + outputs_columns.append(col) + + df = pd.DataFrame(results, columns=outputs_columns) + + file_name = os.path.basename(file_path).replace(".xlsx", '') + filename = file_name + '_解析' + ('_横向排列' if is_horizontal else '_纵向排列') + + upload_dir = Path("outputs") + upload_dir.mkdir(parents=True, exist_ok=True) + save_file_path = upload_dir / f'{filename}_1.xlsx' + + # 使用 ExcelWriter 同时写入多个 sheet + with pd.ExcelWriter(save_file_path, engine='openpyxl') as writer: + df.to_excel(writer, sheet_name='正常解析', index=False) + if exception_list: + except_df = pd.DataFrame(exception_list, columns=outputs_columns) + except_df.to_excel(writer, sheet_name='异常解析', index=False) + + print(f'解析结果保存在:{save_file_path}') + return save_file_path, total_amount + + +if __name__ == "__main__": + + # print(expand_to_structured([[(0,1,'600x50cm','r'), (0,1,'3款各1张','e')]])) + # exit() + # predict("Apple is opening a new office in Tokyo.") + # predict("Google hired Sarah Connor from Berlin last year.") + file_info = ['d:/会计组/新领图8月.xlsx', '备注'] + # file_info = ['d:/会计组/即客2025年8月尺寸整理.xlsx', '系统文件名'] + file_info = ['d:/会计组/8月订单明细9.6.xlsx', '文件名'] + file_info = ['d:/会计组/ZHX-8月订单明细.xlsx', '文件名'] + file_info = ['d:/会计组/国税数据源/智韬2025年8月尺寸整理(1).xlsx', '系统文件名'] + file_info = ['d:/会计组/彩印通8月数码9.23.xlsx', 'ERP系统文件名'] + file_info = ['d:/会计组/CYT8月明细9.24.xlsx', 'ERP系统文件名'] + file_info = ['d:/会计组/9.1-9.26.xlsx', '备注'] + file_info = ['d:/会计组/JD.xlsx', '文件名'] + file_info = ['d:/会计组/泰州即客2025年9月尺寸整理.xlsx', '系统文件名'] + file_info = ['d:/会计组/ZHX需拆明细9月.xlsx', '文件名', False] + file_info = ['d:/会计组/七彩2024年9月账单尺寸整理.xlsx', '系统文件名', True] + file_info = ['d:/会计组/艾印图文2024年9月账单尺寸整理.xlsx', '系统文件名', True] + file_info = ['d:/会计组/9月转印.xlsx', 'erp', True] + file_info = ['d:/会计组/9月CYT.xlsx', '文件名', False] + file_info = ['d:/会计组/智韬2025年9月尺寸整理.xlsx', '系统文件名', True] + file_info = ['d:/会计组/9月UV转印贴.xlsx', '文件名', True] + file_info = ['d:/会计组/彩印通2025年9月(数码).xlsx', '文件名', True] + file_info = ['d:/会计组/9月名片.xlsx', '文件名', True] + file_info = ['d:/会计组/9月不干胶.xlsx', '文件名', True] + file_path, target_index, is_horizontal = file_info + parse_finance_data(file_path, target_index, is_horizontal) + + \ No newline at end of file diff --git a/app/controllers/finance_parse/requirements.txt b/app/controllers/finance_parse/requirements.txt new file mode 100644 index 0000000..3d5ab79 --- /dev/null +++ b/app/controllers/finance_parse/requirements.txt @@ -0,0 +1,6 @@ +pandas==2.3.3 +spacy==3.8.7 +tqdm==4.67.1 +cn2an==0.5.23 +openpyxl==3.1.5 +aiomysql diff --git a/app/controllers/menu.py b/app/controllers/menu.py new file mode 100644 index 0000000..d42586a --- /dev/null +++ b/app/controllers/menu.py @@ -0,0 +1,16 @@ +from typing import Optional + +from app.core.crud import CRUDBase +from app.models.admin import Menu +from app.schemas.menus import MenuCreate, MenuUpdate + + +class MenuController(CRUDBase[Menu, MenuCreate, MenuUpdate]): + def __init__(self): + super().__init__(model=Menu) + + async def get_by_menu_path(self, path: str) -> Optional["Menu"]: + return await self.model.filter(path=path).first() + + +menu_controller = MenuController() diff --git a/app/controllers/msg.py b/app/controllers/msg.py new file mode 100644 index 0000000..139067c --- /dev/null +++ b/app/controllers/msg.py @@ -0,0 +1,461 @@ +from tortoise.exceptions import IntegrityError +from app.core.crud import CRUDBase +from app.schemas.msg import ( + MsgCreate, + MsgUpdate, + MsgNewOrder, + FollowFormErp, + MsgType +) +import httpx +from app.models.msg import Msg, Follow +from app.models.weixin import WeixinCustomer, WeixinUser +from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info +from some_sdk.services.binder import lintao_client, feishu_client +from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user +from some_sdk.feishu_sdk.apis.doc import batch_create as create_feishu_records + +from datetime import datetime, timedelta +from app.utils.common import async_split_generator, split_generator, gen_random_str +from typing import List +import re + +import hashlib + +import logging +logger = logging.getLogger(__name__) + +import asyncio +_sync_to_feishu_lock = asyncio.Lock() + +class MsgController(CRUDBase[Msg, MsgCreate, MsgUpdate]): + def __init__(self): + super().__init__(model=Msg) + + async def send_to_wexin(self, data:dict ): + # pass + url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=57ba9cd5-2c62-43dd-bfea-78c7b4073128' + + async with httpx.AsyncClient() as client: + resp = await client.post(url, + headers={"Content-Type": "application/json"}, + json=data, + ) + data = resp.json() + print(data) + return data + + async def send_order_to_weixin(self, msg: Msg): + """ + 极简订单通知:仅展示店铺、旺旺ID、金额、归属客服 + """ + url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=57ba9cd5-2c62-43dd-bfea-78c7b4073128' + + order_list = msg.detail or [] + if not order_list: + logger.warning("订单详情为空,跳过发送") + return + + order = order_list[0] + # users = order.get("users", []) + # buyer = next((u for u in users if u.get("role") == "buyer"), None) + + # 关键字段 + shop_name = order.get("shop_name", "未知店铺") + buyer_name = msg.title + # buyer_name = buyer.get("name") if buyer else "" + # buyer_name = buyer_name or msg.content.split(")", 1)[0].split("(")[1] + + is_refund = order.get("is_refund", 0) > 0 + msg_title = "💰 用户退款" if is_refund else "🛒 新订单" + + real_payment = order.get("real_payment", "0.00") + qiwei_name = msg.owner_name or "未知客服" + + # 极简 Markdown + markdown_content = f"""# {msg_title}\n + +**店铺**:{shop_name} +**旺旺ID**:`{buyer_name}` +**金额**:¥{real_payment} +**企微客服**:{qiwei_name} +**订单编号**:{order.get("trade_no", "未知订单号")} +""" + + data = { + "msgtype": "markdown", + "markdown": { + "content": markdown_content + } + } + result = await self.send_to_wexin(data) + logger.info(f"企业微信极简订单通知发送结果: {result}") + + + async def parse_order_msg(self, msg: Msg, send_set: set, msg_dict: dict): + title = msg.title + try: + if title not in send_set: + send_set.add(title) + # try: await self.send_to_wexin(msg) + owner_name = '、'.join([ (name or '').split('(')[0].split('vip客服-')[-1].split('-印刷定制')[0] for name in msg_dict[title] ])msg.owner_name = owner_name + + try: await self.send_order_to_weixin(msg) + except Exception as e: + logger.error(f'发送订单消息到微信失败,{e}') + return + + # await self.send_to_wexin(msg, mentioned_list=msg_dict[title]) + logger.info(f'发送消息到微信成功,{msg.content}') + + msg.is_send = True + msg.send_at = datetime.now() + await msg.save() + except Exception as e: + logger.error(f'发送消息到微信失败,{e}') + + async def new_system_msg(self, title: str, content: str, owner_name: str = None): + await self.create( + dict( + hash_id=hashlib.md5(f'{title}{content}{owner_name}'.encode()).hexdigest(), + title=title, + content=content, + type=MsgType.SYSTEM, + owner_name=owner_name, + ) + ) + + async def send_system_msg(self, msg: Msg): + msg.send_at = datetime.now() + markdown_content = f"""# 📣 系统通知 \n +## {msg.title} \n +{msg.content} \n + +{msg.send_at.strftime("%Y-%m-%d %H:%M:%S")} +""" + + data = { + "msgtype": "markdown", + "markdown": { + "content": markdown_content + } + } + result = await self.send_to_wexin(data) + logger.info(f"企业微信极简订单通知发送结果: {result}") + + msg.is_send = True + await msg.save() + + async def sync_msg(self): + msg_list = await self.model.filter(is_send=False) + + msg_dict = {} + for msg in msg_list: + title = msg.title + msg_dict[title] = msg_dict.get(title, []) + [msg.owner_name] + + send_set = set() + for msg in msg_list: + title = msg.title + if msg.type == MsgType.NEW_ORDER: + await self.parse_order_msg(msg, send_set, msg_dict) + elif msg.type == MsgType.SYSTEM: + await self.send_system_msg(msg) + + async def fill_customer_info(self): + msg_orm_list = await self.model.filter(id__gt=306) + + update_list = [] + for msg in msg_orm_list: + detail = msg.detail or [] + taobao_name = msg.title + + detail_list = [] + for order_item in detail: + customer_orm = await WeixinCustomer.filter(taobao_name=taobao_name).first() + if not customer_orm: + logger.warning(f'未找到淘宝用户{taobao_name}的企微客户') + continue + + for user in order_item.get("users", []): + if user.get("role") != "buyer": continue + user.update(customer_orm.to_dict()) + break + + detail_list.append(order_item) + + if detail_list: + msg.detail = detail_list + update_list.append(msg) + + if update_list: + await self.model.bulk_update(update_list, fields=['detail']) + + + async def new_order(self, order_id: str, customer_orm: WeixinCustomer, is_refund: bool = False): + + msg_set = set() + msg_list = [] + now = datetime.now() + + order_list = get_order_relative_user(lintao_client, order_id) + order_unique_list = {} + async for order in order_list: + order_state_string = order.get('order_state_string') + trade_no = order.get('trade_no') + if not is_refund and order_state_string not in ['待领单', '待抢单']: + logger.warning(f'订单{trade_no} 状态为{order_state_string},跳过') + continue + + remark = order.get('remark') + if not is_refund and remark and '企微联系' in remark: + logger.warning(f'订单{trade_no} 状态为{order_state_string}, 备注:{remark}, 已备注企微联系,跳过') + continue + + create_time = order.get('create_time') + if create_time and not is_refund: + create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00")) + if (now - create_time).total_seconds() > 3600: + logger.warning(f'订单{order_id} 创建时间({create_time})与当前时间({now})相差超过60秒,跳过') + continue + + if trade_no not in order_unique_list: + order_unique_list[trade_no] = order + logger.info(f'订单{order_id} 订单号{trade_no} 创建时间{create_time} 订单状态:{order_state_string} {order.get("title")}') + + # 填充买家信息 + for user in order.get("users", []): + if user.get("role") != "buyer": continue + user.update(customer_orm.to_dict()) + break + + + if not order_unique_list: + logger.warning(f'订单{order_id} 没有符合条件的订单,跳过') + return + + chat_info = await get_external_user_chat_info(customer_orm.weixin_id) + follow_user_list = chat_info.get('follow_user', []) + + order_list = list(order_unique_list.values()) + order_info = [ + f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})" + for order in order_list + ] + for follow_user in follow_user_list: + follow_userid = follow_user.get('userid') + weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first() + owner_name = '未知' + if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username + + qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name + + content = f"{'退款' if is_refund else ''}订单({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}" + logger.info(f'新订单通知:{content}') + msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest() + if msg_hash_id in msg_set: continue + msg_set.add(msg_hash_id) + + msg = self.model( + hash_id=msg_hash_id, + title=qiwei_customer_name, + content=content, + detail=order_list, + type=MsgType.NEW_ORDER, + owner_id=follow_userid, + owner_name=owner_name + ) + msg_list.append(msg) + + if msg_list: + try: await self.model.bulk_create(msg_list) + except IntegrityError as e: + if "Duplicate entry" in str(e) and "hash_id" in str(e): + logger.info("检测到重复消息,已跳过") + # 忽略或处理重复 + else: + # 其他完整性错误(如外键失败),应重新抛出 + raise + + async def get_order_user_days_before(self, days: int): + now = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_date, otherMemo = (now - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S"), "企微" + + # 琪琪——松屿,小小丽——麦子,小小北——大喜,小泉——苏苏,小洋——东东,蓝莓——小满 + index = 0 + update_list, add_list = [], [] + order_iter_list = [ + (get_order_relative_user(lintao_client, start_date=start_date, otherMemo=otherMemo), ''), + (get_order_relative_user(lintao_client, start_date=start_date, customer='琪琪'), '松屿'), + (get_order_relative_user(lintao_client, start_date=start_date, customer='小小丽'), '麦子'), + (get_order_relative_user(lintao_client, start_date=start_date, customer='小小北'), '大喜'), + (get_order_relative_user(lintao_client, start_date=start_date, customer='小泉'), '苏苏'), + (get_order_relative_user(lintao_client, start_date=start_date, customer='小洋'), '东东'), + (get_order_relative_user(lintao_client, start_date=start_date, customer='蓝莓'), '小满'), + ] + for order_iter, staff_name in order_iter_list: + async for order_list in async_split_generator(order_iter, 10): + order_dict = {} + for order in order_list: + designer_list = list(filter(lambda x: x.get("role") == "desiger", order.get("users", []))) + designer = designer_list[0] if designer_list else None + if not designer: continue + + name = designer.get("name", "") + if not name: continue + + key = tuple([order.get("trade_no", ""), name]) + if key in order_dict: continue + order_dict[key] = order + index += 1 + + _update_list, _add_list = await self.process_order_dict(order_dict, staff_name=staff_name) + + if _update_list and _add_list: + logger.info(f'第{index}个订单 处理 {len(_update_list)} 条更新记录, {len(_add_list)} 条新增记录') + update_list.extend(_update_list) + add_list.extend(_add_list) + + print(start_date) + _update_list, _add_list = [], [] + + async with _sync_to_feishu_lock: + task_id = gen_random_str() + _update_list, _add_list = await self.sync_to_feishu(task_id) + + return { + "update_count": len(update_list), + "add_count": len(add_list), + "add_count_to_feishu": len(_add_list), + "update_count_to_feishu": len(_update_list), + "start_date": start_date, + "remark": otherMemo + } + + async def process_order_dict(self, order_dict: dict, staff_name: str = ''): + async def to_follow_orm(order_info: dict): + follow_orm = Follow(**FollowFormErp(**order_info).model_dump()) + # print(follow_orm.order_id, order_info.get("users", [])) + + for user in order_info.get("users", []): + if user.get("role") == "desiger": + follow_orm.designer_id = user.get("id", "") + follow_orm.designer_name = user.get("name", "") + elif user.get("role") == "taobao_kefu": + follow_orm.kefu_id = user.get("id", "") + follow_orm.kefu_name = user.get("name", "") + elif user.get("role") == "buyer": + follow_orm.customer_taobao_id = user.get("id", "") + follow_orm.customer_name = user.get("name", "") + + CustomerUser = await WeixinCustomer.filter(taobao_id=follow_orm.customer_taobao_id).first() + if CustomerUser: + follow_orm.customer_id = CustomerUser.id + + # 企微员工 + if staff_name: + follow_orm.staff_name = staff_name + staff_orm = await WeixinUser.filter(english_name=f"{follow_orm.staff_name}-印刷定制").first() + if staff_orm: + follow_orm.staff_id = staff_orm.userid + else: + remark = follow_orm.remark + if remark: + staff_name_match = re.search(r"企微联系(.*?)拉群", remark) + if staff_name_match: + follow_orm.staff_name = staff_name_match.group(1).strip() + if follow_orm.staff_name: + staff_orm = await WeixinUser.filter(english_name=f"{follow_orm.staff_name}-印刷定制").first() + if staff_orm: + follow_orm.staff_id = staff_orm.userid + + if not staff_name_match: + follow_orm.staff_name = remark + + return follow_orm + + update_list = [] + follow_list = await Follow.filter(order_id__in=[key[0] for key in order_dict.keys()]) + keys, update_keys = Follow.get_all_keys(exclude_fields=['id', 'updated_at', 'created_at', 'feishu_record_id', 'is_update_to_feishu', 'is_add_to_feishu']), set() + for follow in follow_list: + key = tuple([follow.order_id, follow.designer_name]) + if key not in order_dict: continue + + order_info = order_dict.pop(key, None) + follow_orm = await to_follow_orm(order_info) + # 比较是否一样,不一样的话,更新 + updated_dict = {} + for key in keys: + origin_value = str(getattr(follow, key)) + value = str(getattr(follow_orm, key)) + if origin_value != value: + update_keys.add(key) + setattr(follow, key, value) + updated_dict[key] = f'{origin_value} -> {value}' + + if updated_dict: + follow_orm.is_update_to_feishu = False + update_keys.add('is_update_to_feishu') + update_list.append(follow) + logger.info(f'订单 {follow.order_id} 设计员 {follow.designer_name} 更新字段 {updated_dict}') + + if update_list: + await Follow.bulk_update(update_list, fields=update_keys) + + add_list = [] + for key, order_info in order_dict.items(): + follow_orm = await to_follow_orm(order_info) + add_list.append(follow_orm) + + if add_list: + await Follow.bulk_create(add_list) + + return update_list, add_list + + async def sync_to_feishu(self, task_id: str): + + add_list = [] + # 需要追加的记录 + follow_list = await Follow.filter(is_update_to_feishu=False, feishu_record_id__isnull=True) + if follow_list: + for item_list in split_generator(follow_list, 100): + result = await create_feishu_records( + feishu_client, + app_token="HPiNbbW3YaYhBjsjLQychK9gnbf", + table_id="tblQGVfmglJgZuBf", + records=[{"fields": item.to_feishu_record(fields={"同步任务号": task_id})["fields"]} for item in item_list], + ) + + _update_list = [] + result_list = result.get("data", {}).get("records", []) + for record, item in zip(result_list, item_list): + item.feishu_record_id = record["record_id"] + item.is_add_to_feishu = True + item.is_update_to_feishu = True + _update_list.append(item) + + if _update_list: + await Follow.bulk_update(_update_list, fields=['feishu_record_id', 'is_add_to_feishu', 'is_update_to_feishu']) + + logger.info(f'追加 {len(_update_list)} 条记录到飞书') + add_list.extend(_update_list) + + update_list = [] + # 需要更新的记录 + # follow_list = await Follow.filter(is_update_to_feishu=False, feishu_record_id__isnull=False) + # if follow_list: + # for item_list in split_generator(follow_list, 100): + # await create_feishu_records( + # get_feishu_client, + # app_token="JTqnbdjb7aEMm3srrLfcidYcnxc", + # table_id="tblXq8AmG22uK5n1", + # records=item_list, + # ) + + return update_list, add_list + + async def set_read(self, id: str): + await Msg.filter(id=id).update(is_read=True, read_at=datetime.now()) + +msg_controller = MsgController() + diff --git a/app/controllers/role.py b/app/controllers/role.py new file mode 100644 index 0000000..cb6a528 --- /dev/null +++ b/app/controllers/role.py @@ -0,0 +1,27 @@ +from typing import List + +from app.core.crud import CRUDBase +from app.models.admin import Api, Menu, Role +from app.schemas.roles import RoleCreate, RoleUpdate + + +class RoleController(CRUDBase[Role, RoleCreate, RoleUpdate]): + def __init__(self): + super().__init__(model=Role) + + async def is_exist(self, name: str) -> bool: + return await self.model.filter(name=name).exists() + + async def update_roles(self, role: Role, menu_ids: List[int], api_infos: List[dict]) -> None: + await role.menus.clear() + for menu_id in menu_ids: + menu_obj = await Menu.filter(id=menu_id).first() + await role.menus.add(menu_obj) + + await role.apis.clear() + for item in api_infos: + api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first() + await role.apis.add(api_obj) + + +role_controller = RoleController() diff --git a/app/controllers/table.py b/app/controllers/table.py new file mode 100644 index 0000000..a86634b --- /dev/null +++ b/app/controllers/table.py @@ -0,0 +1,62 @@ +from typing import List + +import json +from app.core.crud import CRUDBase +from app.models.admin import Api, Menu, Codegen +from app.schemas.codegen import CodegenCreate, CodegenUpdate +from app.utils.db import DatabaseInfo +from app.utils.codegen import CodeGenerator + +class CodegenController(CRUDBase[Codegen, CodegenCreate, CodegenUpdate]): + def __init__(self): + super().__init__(model=Codegen) + + async def is_exist(self, name: str) -> bool: + return await self.model.filter(name=name).exists() + + async def import_table(self, connect: str='', importTables: List[str] = None) -> Codegen: + + default_frontend_config = { + "editable": True, + "listable": True, + "sortable": True, + "filterable": False, + "filter_operator": '=', # = | >= 默认过滤操作符为包含 + "display_type": 'text', # 默认显示类型为文本 + } + + cnt = 0 + + with DatabaseInfo( + host="lt.330770.xyz", + port=3307, + user="root", + password="rap_sky", + database="rpa" + ) as db: + + for table_name in importTables: + table = db.get_table_structure(table_name) + # result.append(fields) + fields = table.get('fields') + for field in fields: + # if field.name in ['id']: + if 'editable' not in field: + field['editable'] = field['required'] + new_data = {**default_frontend_config, **field} + field.update(**new_data) + description = table.get('tableComment') + await self.create(CodegenCreate(name=table_name, description=description , fields=json.dumps(fields,ensure_ascii=False))) + cnt += 1 + + return cnt + + async def preview(self, table_id): + generator = CodeGenerator() + config_orm = await self.get(id=table_id) + entity_config = await config_orm.to_dict() + generated_files = generator.generate_files(entity_config, 'relation-demo', {}) + return generated_files + + +codegen_controller = CodegenController() diff --git a/app/controllers/user.py b/app/controllers/user.py new file mode 100644 index 0000000..9059a61 --- /dev/null +++ b/app/controllers/user.py @@ -0,0 +1,60 @@ +from datetime import datetime +from typing import List, Optional + +from fastapi.exceptions import HTTPException + +from app.core.crud import CRUDBase +from app.models.admin import User +from app.schemas.login import CredentialsSchema +from app.schemas.users import UserCreate, UserUpdate +from app.utils.password import get_password_hash, verify_password + +from .role import role_controller + + +class UserController(CRUDBase[User, UserCreate, UserUpdate]): + def __init__(self): + super().__init__(model=User) + + async def get_by_email(self, email: str) -> Optional[User]: + return await self.model.filter(email=email).first() + + async def get_by_username(self, username: str) -> Optional[User]: + return await self.model.filter(username=username).first() + + async def create_user(self, obj_in: UserCreate) -> User: + obj_in.password = get_password_hash(password=obj_in.password) + obj = await self.create(obj_in) + return obj + + async def update_last_login(self, id: int) -> None: + user = await self.model.get(id=id) + user.last_login = datetime.now() + await user.save() + + async def authenticate(self, credentials: CredentialsSchema) -> Optional["User"]: + user = await self.model.filter(username=credentials.username).first() + if not user: + raise HTTPException(status_code=400, detail="无效的用户名") + verified = verify_password(credentials.password, user.password) + if not verified: + raise HTTPException(status_code=400, detail="密码错误!") + if not user.is_active: + raise HTTPException(status_code=400, detail="用户已被禁用") + return user + + async def update_roles(self, user: User, role_ids: List[int]) -> None: + await user.roles.clear() + for role_id in role_ids: + role_obj = await role_controller.get(id=role_id) + await user.roles.add(role_obj) + + async def reset_password(self, user_id: int): + user_obj = await self.get(id=user_id) + if user_obj.is_superuser: + raise HTTPException(status_code=403, detail="不允许重置超级管理员密码") + user_obj.password = get_password_hash(password="123456") + await user_obj.save() + + +user_controller = UserController() diff --git a/app/controllers/weixin/customer.py b/app/controllers/weixin/customer.py new file mode 100644 index 0000000..6a25a58 --- /dev/null +++ b/app/controllers/weixin/customer.py @@ -0,0 +1,607 @@ + +from app.core.crud import CRUDBase +from app.schemas.weixin import ( + WeixinCustomerCreate, + WeixinCustomerUpdate, + WeixinCustomerXingyunCreate, +) +from typing import List + +from app.models.weixin import WeixinCustomer, WeixinUser, CustomerGroup +from app.schemas.weixin import WeixinUserBindInfo, WeixinOrderBindInfo + +from some_sdk.services.binder import lintao_client, xy_client, wk_client +from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user, save_other_memo +from some_sdk.xingyun_sdk.apis.customer import bind_user_to_xingyun, list_user_order +from some_sdk.xingyun_sdk.apis.work_external_contact import list_all_contact_by_addtime, list_all_contact_by_addtime_and_tag +from some_sdk.wk_weixin_sdk.apis.corp_user import from_service_external_userid +from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info + +from app.controllers.msg import msg_controller +from app.schemas.msg import MsgType + +from app.utils.common import async_split_generator +import pickle +import hashlib + +import re, os +from datetime import datetime, timedelta +from app.core.cache import cache_if + +import logging +logger = logging.getLogger(__name__) + +EXCLUDE_SHOP_NAMES = ['领淘文具旗舰店'] +XINGYUN_USE_TIME = datetime(2025, 6, 18) + +class WeixinCustomerController(CRUDBase[WeixinCustomer, WeixinCustomerCreate, WeixinCustomerUpdate]): + def __init__(self): + super().__init__(model=WeixinCustomer) + + async def update_from_group(self, external_members: list[dict]): + # 同步更新数据库 + member_dict = {} + for member in external_members: + member = member.copy() + userid = member.pop('userid', None) + + weixin_name = member.pop('name', None) + weixin_unionid = member.pop('unionid', None) + + member_info = WeixinCustomer.orm_format(member) + member_info['weixin_id'] = userid + member_info['weixin_name'] = weixin_name + member_info['weixin_unionid'] = weixin_unionid + member_dict[userid] = member_info + + member_list = await self.model.filter(weixin_id__in=member_dict.keys()) + + update_list = [] + update_fileds = ['weixin_unionid', 'weixin_id', 'weixin_name', 'order_id', 'taobao_id', 'taobao_name', 'extra', 'need_confirm'] + + history = {} + for member in member_list: + member_info = member_dict.pop(member.weixin_id, history.get(member.weixin_id, {})) + if not member_info: continue + history[member.weixin_id] = member_info + + member_info['extra'] = {**(member.extra or {}), **member_info['extra']} + + updated = {} + for field in update_fileds: + old_value = getattr(member, field) + if field not in member_info: + continue + value = member_info.get(field) + if old_value != value: + setattr(member, field, value) + updated[f'{field}:{str(old_value)}'] = value + if updated: + logger.info(f'用户 {member.weixin_name} 更新关系为 {updated}') + update_list.append(member) + if update_list: + await self.model.bulk_update(update_list, fields=update_fileds) + + # 新增用户 + create_list = [] + for userid, member_info in member_dict.items(): + create_list.append(WeixinCustomer( + **member_info, + )) + if create_list: + await self.model.bulk_create(create_list) + + return { + 'update_count': len(update_list), + 'create_count': len(create_list), + } + + async def bind_user(self, user_in: WeixinUserBindInfo): + if not user_in.taobao_id: + raise ValueError("淘宝ID不能为空") + await self.model.filter(taobao_id=user_in.taobao_id).update_from_dict(user_in.model_dump(exclude_unset=True)) + + async def remark_order(self, order: dict, remark: str): + ctid = order.get("ctid") or order.get("trade_no") + # if remark in (order.get("remark") or ''): + if '企微' in (order.get("remark") or ''): + logger.info(f'订单 {ctid} 备注中已包含 企微 备注,无需重复添加') + return + remark_list = [order.get("remark", ""), remark] + append_remark = ';'.join(filter(lambda x: x, remark_list)) + + if os.getenv("APP_ENV") == "prod": + result = await save_other_memo(lintao_client, order_id=ctid, other_memo=append_remark) + assert result["type"] == "success", f"保存备注失败,响应: {result}" + logger.info(f'成功为订单 {ctid} 打上备注 {append_remark}') + else: + logger.info(f'非生产环境,不实际为订单 {ctid} 打上备注 {append_remark}') + + async def remark_order_by_order_id(self, remark: str, order_id: str=None, orders: list=None): + if not orders: + assert order_id, "订单ID不能为空" + orders = [o async for o in get_order_relative_user(lintao_client, trade_no=order_id)] + assert orders, f"订单 {order_id} 不存在" + + # 打上备注 + logger.info(f'即将为以下订单 {[o.get("trade_no") for o in orders]} 打上备注 {remark}') + need_monitor_order_list = [] + for order in orders: + if not order.get('title'): + # 未领单的订单,需要添加到监控列表中进行监控,防止被刷掉 + need_monitor_order_list.append(order) + logger.info(f'订单 {order.get("trade_no")} 未领单,需要添加到监控列表中进行监控,防止被刷掉') + + try: + await self.remark_order(order, remark=remark) + except Exception as e: + logger.error(f'为订单 {order.get("trade_no")} 打上备注 {remark} 失败,异常: {e}') + need_monitor_order_list.append(order) + + return need_monitor_order_list + + + async def bind_order(self, bind_in: WeixinOrderBindInfo): + orders = [o async for o in get_order_relative_user(lintao_client, trade_no=bind_in.order_id)] + assert orders, f"订单 {bind_in.order_id} 不存在" + + orders = [o for o in orders if o.get('trade_no') == bind_in.order_id] + order = orders[0] + + buyer_ids = [] + for user in order.get('users', []): + taobao_id = user.get('id', '') + if not taobao_id or re.match(r'^\d+$', taobao_id): continue + buyer_ids.append(taobao_id) + + # 更新还是新建? + orms = await self.model.filter(weixin_id=bind_in.userid).all() + logger.info(f'用户 {bind_in.userid} 准备绑定订单 {len(orms)} {[orm.weixin_name for orm in orms]}') + + update_list = [] + for orm in orms: + if orm.order_id and orm.order_id != order.get('trade_no'): + logger.warning(f'用户 {orm.weixin_name} 已绑定订单 {orm.order_id},即将覆盖为 {order.get("trade_no")}') + + orm.weixin_id = bind_in.userid + orm.order_id = order.get('trade_no') + orm.shop_name = order.get('shop_name', '') + orm.taobao_id = user.get('id', '') + orm.taobao_name = user.get('name', '') + update_list.append(orm) + + if update_list: + logger.info(f'更新用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}') + await self.model.bulk_update(update_list, fields=['order_id', 'weixin_id', 'taobao_id', 'taobao_name']) + return True, orders + + if not orms: + logger.info(f'绑定用户 {bind_in.userid} 绑定订单 {order.get("trade_no")} 中的用户 {user}') + orm_user = WeixinCustomer( + weixin_id=bind_in.userid, + order_id=order.get('trade_no'), + taobao_id=user.get('id', ''), + taobao_name=user.get('name', ''), + ) + await orm_user.save() + return True, orders + + return False, orders + + async def bind_xingyun_user(self, contact_orm: WeixinCustomer): + assert contact_orm.xingyun_id, "星云用户ID不能为空" + if contact_orm.shop_name in EXCLUDE_SHOP_NAMES: + contact_orm.xingyun_sync = True + contact_orm.extra = contact_orm.extra or {} + contact_orm.extra['system_remark'] = f'店铺名称: {contact_orm.shop_name},没有开通星云有客服务,故此跳过。' + return + + order_id = contact_orm.order_id.split('_')[-1] + logger.info(f'同步客户订单信息到星云: {contact_orm.xingyun_id} -》 {order_id}') + try: + await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id) + except: + logger.error(f'同步客户订单信息到星云失败: {contact_orm.xingyun_id} -》 {order_id}, 尝试寻找同用户的其他订单来绑定') + shop_name = '' + async for order in get_order_relative_user(lintao_client, buyer_nick=contact_orm.taobao_name): + shop_name = order.get('shop_name', '') + if shop_name in EXCLUDE_SHOP_NAMES: + continue + + if not order.get('create_time'): continue + create_time = order.get('create_time') + create_time = datetime.fromisoformat(create_time.replace("Z", "+00:00")) + if create_time < XINGYUN_USE_TIME: + continue + + order_id = order.get('trade_no').split('_')[-1] + await bind_user_to_xingyun(xy_client, contact_orm.xingyun_id, order_id) + logger.info(f'通过同用户的其他订单来同步客户订单信息到星云成功: {contact_orm.xingyun_id} -》 {order_id}') + break + + if shop_name and not contact_orm.shop_name: + contact_orm.shop_name = order.get('shop_name', '') + await contact_orm.save() + + contact_orm.xingyun_sync = True + + async def load_all_user_from_xingyun(self, task_id: str, add_time_start: datetime, add_time_end: datetime): + logger.info(f'从星云加载客户数据,时间范围 {task_id} {add_time_start} - {add_time_end}') + contact_iter = list_all_contact_by_addtime_and_tag(xy_client, task_id=task_id, keyword='', add_time_start=add_time_start, add_time_end=add_time_end) + + return await self._load_user_from_xingyun(contact_iter, eager_load=True) + + async def load_user_from_xingyun(self, add_time_start: datetime, add_time_end: datetime): + logger.info(f'从星云加载客户数据,时间范围 {add_time_start} - {add_time_end}') + contact_iter = list_all_contact_by_addtime(xy_client, keyword='', add_time_start=add_time_start, add_time_end=add_time_end) + + return await self._load_user_from_xingyun(contact_iter) + + async def _load_user_from_xingyun(self, contact_iter: list, eager_load: bool = False): + logger.info(f'从星云加载客户数据,开始同步') + load_result = {"update_count": 0, "add_count": 0} + async for contact_list in async_split_generator(contact_iter, 10): + try: result = await self.save_xingyun_contact_info(contact_list) + except Exception as e: + logger.error(f'从星云加载客户数据,同步失败 {e}') + logger.exception(e) + raise e + + # 全部都已经同步完了 + if not eager_load and result.get("update_count", 0) + result.get("add_count", 0) == 0: + logger.info(f'从星云加载客户数据,全部同步完成') + break + + load_result['update_count'] += result.get("update_count", 0) + load_result['add_count'] += result.get("add_count", 0) + logger.info(f'从星云加载客户数据{eager_load},更新 {result.get("update_count", 0)} 条,新增 {result.get("add_count", 0)} 条') + logger.info(f'从星云加载客户数据,总 {load_result["update_count"] + load_result["add_count"]} 条,更新 {load_result["update_count"]} 条,新增 {load_result["add_count"]} 条') + await self.sync_xingyun_contact_info() + + return load_result + + async def save_xingyun_contact_info(self, contact_list: list): + contact_dict = {contact.get('cid'): contact for contact in contact_list} + contact_orm_list: List[WeixinCustomer] = await self.model.filter(xingyun_id__in=contact_dict.keys()) + + field_names = set() + update_list = [] + + async def update(contact_dict, contact_orm_list, key): + history = {} + for contact_orm in contact_orm_list: + value = getattr(contact_orm, key) + contact = contact_dict.pop(value, history.get(value, {})) + if not contact: continue + history[value] = contact + + try: + contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra() + except Exception as e: + logger.error(f'客户数据转换错误,chat_id: {contact}, {e}') + continue + # print(type(contact_in), contact_in) + updated = False + + contact_in_dict = contact_in.model_dump(exclude_unset=True) + field_names.update(contact_in_dict.keys()) + + for field_name in field_names: + + old_value = getattr(contact_orm, field_name) + value = contact_in_dict.get(field_name) + if old_value != value: + setattr(contact_orm, field_name, value) + updated = True + + new_extra = {**(contact_orm.extra or {}), **contact_in.extra} + if contact_in.extra != new_extra: + if new_extra != contact_orm.extra: + contact_orm.extra = new_extra + updated = True + + # 触发同步信息到星云 + if updated and not contact_orm.xingyun_sync and contact_orm.xingyun_id: + try: + await self.bind_xingyun_user(contact_orm) + update_list.append(contact_orm) + except Exception as e: + logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}') + continue + + if update_list: + await self.model.bulk_update(update_list, list(field_names) + ['extra', 'xingyun_sync']) + + await update(contact_dict, contact_orm_list, 'xingyun_id') + + add_dict = {} + for contact in contact_dict.values(): + externalUserid = contact.get('externalUserid', None) + if not externalUserid: + continue + try: + resp = await from_service_external_userid(wk_client, externalUserid) + except Exception as e: + logger.error(f'从星云加载客户数据,获取用户ID失败,externalUserid: {externalUserid}, {e}') + continue + if not resp or not resp.get('external_userid'): + continue + + external_userid = resp.get('external_userid') + contact['external_userid'] = external_userid + add_dict[external_userid] = contact + + contact_orm_list = await self.model.filter(weixin_id__in=add_dict.keys()) + await update(add_dict, contact_orm_list, 'weixin_id') + + add_list = [] + for contact in add_dict.values(): + contact_in = WeixinCustomerXingyunCreate(**contact).transform_to_extra() + add_list.append(self.model(**contact_in.model_dump(exclude_unset=True))) + + if add_list: + await self.model.bulk_create(add_list) + + return { + 'update_count': len(update_list), + 'add_count': len(add_list), + } + + async def bind_user(self, bind_info: WeixinUserBindInfo): + return await self.bind_user(bind_info) + + async def monitor_erp_order(self): + # 从领淘ERP加载订单相关用户 + # 获取上一个订单的创建时间 + cache_file = 'env/order_cache.pkl' + newest_order_create_time = None + today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0) + try: + with open(cache_file, 'rb') as f: + last_order = pickle.load(f) + newest_order_create_time = last_order.get('create_time') + logger.info(f'从缓存文件加载最新订单创建时间,{newest_order_create_time}') + except FileNotFoundError: + newest_order_create_time = today_end.replace(hour=0, minute=0, second=0, microsecond=0) + newest_order_create_time = newest_order_create_time.strftime('%Y-%m-%d %H:%M:%S') + + order_list = [o async for o in get_order_relative_user(lintao_client, start_date=newest_order_create_time, end_date=today_end.strftime('%Y-%m-%d %H:%M:%S'))] + msg_list, newest_order = await self.find_bind_order(order_list) + + # 保存最新订单到缓存文件 + if newest_order: + newest_order['create_time'] = newest_order['create_time'].replace('T', ' ') + with open(cache_file, 'wb') as f: + pickle.dump(newest_order, f) + + return msg_list + + async def find_bind_order(self, order_list: list): + + buyer_order_dict = {} + newest_order = None + last_erp_id = 0 + + order_id_set = set() + for order in order_list: + erp_id = order.get('id') + if erp_id <= 6368700: break + + create_time = order.get('create_time') + if last_erp_id < erp_id: + if create_time: newest_order = order + last_erp_id = erp_id + + order_id = order.get('trade_no') + if order_id in order_id_set: continue + order_id_set.add(order_id) + + users = order.get("users", []) + buyer_id_set = set() + for user in users: + role = user.get('role') + if role != 'buyer': continue + buyer_id = user.get('id') + if not buyer_id: continue + if buyer_id in buyer_id_set: continue + buyer_id_set.add(buyer_id) + + buyer_order_dict.setdefault(buyer_id, []).append(order) + + customer_list = await self.model.filter(taobao_id__in=buyer_order_dict.keys()).all() + + # 通知新订单 + # (客户id)-(所属企微客服)刚在(店铺名)下单-(金额) + msg_set = set() + msg_list = [] + for customer_orm in customer_list: + order_list = buyer_order_dict.get(customer_orm.taobao_id, []) + + order_info = [ + f"在({order.get('shop_name')})下单{order.get('price')}元,单号({order.get('trade_no')})" + for order in order_list + ] + chat_info = await get_external_user_chat_info(customer_orm.weixin_id) + follow_user_list = chat_info.get('follow_user', []) + + for follow_user in follow_user_list: + follow_userid = follow_user.get('userid') + weixin_user_orm: WeixinUser = await WeixinUser.filter(userid=follow_userid).first() + owner_name = '未知' + if weixin_user_orm: owner_name = weixin_user_orm.english_name or weixin_user_orm.username + qiwei_customer_name = customer_orm.taobao_name or customer_orm.weixin_name + + content = f"({qiwei_customer_name}) -({owner_name})- 刚{', '.join(order_info)}" + msg_hash_id = hashlib.md5(content.encode('utf-8')).hexdigest() + if msg_hash_id in msg_set: continue + msg_set.add(msg_hash_id) + + # 检查消息是否已存在 + msg_orm = await msg_controller.model.filter(hash_id=msg_hash_id).first() + if msg_orm: continue + + msg = msg_controller.model( + hash_id=msg_hash_id, + title=qiwei_customer_name, + content=content, + detail=order_list, + type=MsgType.NEW_ORDER, + owner_id=follow_userid, + owner_name=owner_name + ) + logger.info(f'创建新订单通知消息,{msg.content}') + msg_list.append(msg) + + if msg_list: + await msg_controller.model.bulk_create(msg_list) + + return msg_list, newest_order + + async def get_customer_by_weixin_id(self, weixin_id: str): + async with cache_if(f'customer:user_datail_0:{weixin_id}', ttl=3600*24) as cache: + if cache.hit: + result = cache.value + return result + else: + result = await self._get_customer_by_weixin_id(weixin_id) + cache.set(result) + return result + + async def _get_customer_by_weixin_id(self, weixin_id: str): + chat_info = await get_external_user_chat_info(weixin_id) + follow_user_list = chat_info.get('follow_user', []) + external_contact = chat_info.get('external_contact', {}) + + if external_contact: + customer_orm = await self.model.filter(weixin_id=weixin_id).first() + if customer_orm: + customer_orm.weixin_avatar = external_contact.get('avatar') + customer_orm.weixin_unionid = external_contact.get('unionid') + customer_orm.weixin_name = external_contact.get('name') + await customer_orm.save() + + user_ids = [follow_user.get('userid') for follow_user in follow_user_list] + weixin_user_orm_list = await WeixinUser.filter(userid__in=user_ids).all() + weixin_user_orm_dict = {weixin_user_orm.userid: weixin_user_orm for weixin_user_orm in weixin_user_orm_list} + for follow_user in follow_user_list: + follow_userid = follow_user.get('userid') + weixin_user_orm = weixin_user_orm_dict.get(follow_userid) + + if weixin_user_orm: + follow_user['username'] = weixin_user_orm.english_name or weixin_user_orm.username + + return { + **external_contact, + 'follow_user_list': follow_user_list, + } + + # for follow_user in follow_user_list: + # follow_userid = follow_user.get('userid') + + async def get_user_detail(self, weixin_id: str): + async with cache_if(f'customer:user_datail:{weixin_id}', ttl=3600) as cache: + if cache.hit: + result = cache.value + return result + else: + customer = await weixin_customer_controller.model.get_or_none(weixin_id=weixin_id) + # if customer: return customer.to_dict() + print(f'get_user_detail, weixin_id: {weixin_id}, customer: {customer}') + + # 通过中间表 CustomerGroup 查询其加入的所有群 + memberships = await CustomerGroup.filter( + customer=customer + ).prefetch_related('group') # 预加载 group 信息 + + group_list = [] + result = {'group_list': group_list} + for m in memberships: + data = await m.to_dict() + # data['group'] = await m.group.to_dict() + data['chat_name'] = m.group.name + data['create_time'] = m.group.create_time + group_list.append(data) + + chat_info = await self.get_customer_by_weixin_id(weixin_id) + result['user_detail'] = chat_info + cache.set(result) + + return result + + # 同步星云和数据库中的客户信息: 同步绑定信息到星云、从星云中加载订单信息 + async def sync_xingyun_contact_info(self, eager_load: bool = False): + # 从星云同步绑定信息到本地 + await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=False) + # 从本地同步订单信息到星云 + await self._sync_xingyun_contact_info(eager_load=eager_load, order_id__isnull=True, limit=10) + + async def _sync_xingyun_contact_info(self, eager_load: bool = False, max_try: int = 2, limit: int = 20, **query_kwargs): + query_kwargs = query_kwargs or {} + + last_id = 0 + while eager_load or max_try > 0: + max_try -= 1 + if last_id: + contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, id__lt=last_id, **query_kwargs).order_by('-id').limit(limit).all() + else: + contact_orm_list = await self.model.filter(xingyun_sync=False, xingyun_id__isnull=False, **query_kwargs).order_by('-id').limit(limit).all() + + if not contact_orm_list: break + logger.debug(f'同步客户数据,{len(contact_orm_list)}') + + has_new = False + update_list = [] + for contact_orm in contact_orm_list: + last_id = contact_orm.id + has_new = True + + if contact_orm.need_confirm: + logger.info(f'客户需要确认绑定: {contact_orm}') + continue + + if contact_orm.order_id: + try: + await self.bind_xingyun_user(contact_orm) + update_list.append(contact_orm) + except Exception as e: + logger.error(f'同步客户订单信息到星云错误,xingyun_id: {contact_orm.xingyun_id}, {e}') + continue + else: + # list_user_order + try: + order_list = list_user_order(xy_client, cid=contact_orm.xingyun_id) + except Exception as e: + logger.error(f'客户数据同步错误,xingyun_id: {contact_orm.xingyun_id}, {e}') + logger.exception(e) + continue + + shopNameSet, order_id_list = set(), [] + async for order in order_list: + shopName = order.get('shopName') + if shopName in shopNameSet: + continue + shopNameSet.add(shopName) + order_id_list.append(order.get('orderId')) + # 多店铺用的用的名称和ID都一样,所以不区分 + break + + for order_id in order_id_list: + try: + await self.bind_order(WeixinOrderBindInfo( + userid=contact_orm.weixin_id, + order_id=order_id, + )) + except AssertionError as e: + logger.error(f'同步客户订单信息到本地错误,xingyun_id: {contact_orm.xingyun_id}, {e}') + logger.exception(e) + continue + contact_orm.xingyun_sync = True + update_list.append(contact_orm) + logger.info(f'同步客户订单信息到本地: {contact_orm.xingyun_id} -》 {order_id}') + + if update_list: + await self.model.bulk_update(update_list, ['xingyun_sync']) + + if not has_new: break + +weixin_customer_controller = WeixinCustomerController() diff --git a/app/controllers/weixin/group.py b/app/controllers/weixin/group.py new file mode 100644 index 0000000..362c02a --- /dev/null +++ b/app/controllers/weixin/group.py @@ -0,0 +1,408 @@ +from tortoise.expressions import Q + +from app.core.crud import CRUDBase +from app.schemas.weixin import ( + WeixinGroupChatCreate, + WeixinGroupChatUpdate, + WeixinGroupChatXingyunCreate, +) + +from some_sdk.services.binder import wk_client, lintao_client, xy_client +from some_sdk.wk_weixin_sdk.apis import corp_group +from some_sdk.wk_weixin_sdk.apis.extern_user import get_external_user_chat_info +from some_sdk.xingyun_sdk.apis.work_user import list_work_group +from some_sdk.xingyun_sdk.apis.customer import list_user_join_group + +from some_sdk.lintao_sdk.biz.by_order import get_order_relative_user +from some_sdk.lintao_sdk.apis.orderlist import list_product_raw, get_order_log + +from app.models.weixin import WeixinGroupChat, RoleType, CustomerGroup +from app.controllers.weixin.user import weixin_user_controller +from app.controllers.weixin.customer import weixin_customer_controller +from app.utils.common import async_split_generator, async_generator_to_list +from app.core.cache import cache_if, invalidate_cache + +import re + +import logging +logger = logging.getLogger(__name__) + +ROLE_LIST = [role.value for role in RoleType] + +class WeixinGroupChatController(CRUDBase[WeixinGroupChat, WeixinGroupChatCreate, WeixinGroupChatUpdate]): + def __init__(self): + super().__init__(model=WeixinGroupChat) + + async def get_external_group_chat_info(self, chat_id: str): + group_info = await corp_group.get_external_group_chat_info(wk_client, chat_id) + + internal_member_list = group_info['internal_member_list'] + external_member_list = group_info['external_member_list'] + group_name = group_info.get('name') + + external_member_list.sort(key=lambda user: user.get('join_time')) + + unique_dict = { + 'name': group_name, + 'external_user_count': len(external_member_list), + 'internal_member_count': len(internal_member_list), + } + group_info.update(unique_dict) + # 同步更新数据库 + status, obj = await self.create_or_update(group_info, query_kwargs={'chat_id': chat_id}, update_kwargs=unique_dict) + if not status: + logger.debug(f'群聊数据不变,不用更新,chat_id: {chat_id}') + # elif status == 'create': + # await self.create_customer_group(chat_id) + else: + # 群更新:触发数据同步事件 + # event_manager.subscribe("group_chat_updated", update_cache_handler, EventPriority.MEDIUM) + + # 只保存准确无误的数据 + # buyer_nick = get_buyer_nick_from_group_name(group_name) + # logger.debug(f'group_name: {group_name}; buyer_nick: {buyer_nick}') + # if not buyer_nick: + # logger.error(f'群聊未命名,无法绑定订单,chat_id: {chat_id}') + # return group_info + + # self.load_erp_order_info_by_group(buyer_nick, internal_member_list, external_member_list) + + update_result1 = await weixin_user_controller.update_from_group([*internal_member_list]) + update_result2 = await weixin_customer_controller.update_from_group([*external_member_list]) + logger.debug(f'群聊用户数据更新结果,chat_id: {chat_id}, {update_result1}') + logger.debug(f'群聊客户数据更新结果,chat_id: {chat_id}, {update_result2}') + # 刷新用户缓存 + for user in [*internal_member_list, *external_member_list]: + await self.expire_user_cache(user.get('userid')) + + group_info['is_create'] = status == 'create' + group_info['detect_update'] = status != False + return group_info + + # 从星云有客中加载群聊信息 + async def load_xingyun_group_info(self, chat_name: str=''): + # 检索出最新的记录,只加载最新的记录 + newest_group = await self.model.filter(xingyun_chat_id__isnull=False).order_by('-create_time').first() + newest_time = newest_group.create_time if newest_group else 1735660800 # 2025-01-01 00:00:00 + + load_result = {"update_count": 0, "add_count": 0} + group_iter = list_work_group(xy_client, chatName=chat_name) + async for group_list in async_split_generator(group_iter): + if group_list[-1].get('createTime') < newest_time: + break + logger.debug(f'处理群聊信息,chat_name: {chat_name}, {len(group_list)}') + result = await self.save_xingyun_group_info(group_list) + load_result['update_count'] += result['update_count'] + load_result['add_count'] += result['add_count'] + logger.debug(f'群聊数据更新结果,chat_name: {chat_name}, {result}') + + if load_result['add_count'] + load_result['update_count'] > 300: + break + + await self.refresh_xingyun_group_info() + return load_result + + async def save_xingyun_group_info(self, group_list: list): + group_dict = {group.get('groupChatId'): group for group in group_list} + group_orm_list = await self.model.filter(chat_id__in=group_dict.keys()) + + update_list = [] + history = {} + for group_orm in group_orm_list: + group = group_dict.pop(group_orm.chat_id, history.get(group_orm.chat_id, {})) + if not group: continue + history[group_orm.chat_id] = group + + try: + group_in = WeixinGroupChatXingyunCreate(**group) + except Exception as e: + logger.error(f'群聊数据转换错误,chat_id: {group}, {e}') + continue + updated = False + if group_in.avatars: + group_orm.avatars = group_in.avatars + updated = True + if group_orm.xingyun_chat_id != group_in.xingyun_chat_id: + group_orm.xingyun_chat_id = group_in.xingyun_chat_id + updated = True + if updated: + update_list.append(group_orm) + if update_list: + await self.model.bulk_update(update_list, ['avatars', 'xingyun_chat_id']) + + add_list = [] + for chat_id, group in group_dict.items(): + group_in = WeixinGroupChatXingyunCreate(**group) + add_list.append(self.model(**group_in.model_dump(exclude_unset=True))) + + if add_list: + await self.model.bulk_create(add_list) + + return { + 'update_count': len(update_list), + 'add_count': len(add_list), + } + + # 刷新数据库中的群聊信息 + async def refresh_xingyun_group_info(self): + + for _ in range(10): + group_orm_list = await self.model.filter(xingyun_chat_id__isnull=False, external_user_count__isnull=True).order_by('-create_time').limit(20).all() + if not group_orm_list: break + logger.debug(f'刷新群聊数据,{len(group_orm_list)}') + + for group_orm in group_orm_list: + try: + await self.get_external_group_chat_info(group_orm.chat_id) + except Exception as e: + logger.error(f'刷新群聊数据错误,chat_id: {group_orm.chat_id}, {e}') + continue + logger.debug(f'刷新群聊数据结果,chat_id: {group_orm.name} 已更新') + + # 同步erp中的用户和客户信息到数据库 + async def load_erp_order_info_by_group(self, buyer_nick: str, internal_member_list: list, external_member_list: list): + if not buyer_nick: + logger.error(f'群聊未命名,无法绑定订单,buyer_nick: {buyer_nick}') + return + + orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick)) + assert orders, f'未找到订单,buyer_nick: {buyer_nick}' + + internal_member_dict = {user.get('name'): user for user in internal_member_list} + + order = orders[0] + + order_relative_user = order.get("users", []) + order_relative_user_dict = {user.get('name'): user for user in order_relative_user} + + for name, user in order_relative_user_dict.items(): + erp_id = user.get('id', '') + if not erp_id: continue + if re.match(r'^\d+$', erp_id) is None: + # 客户 + if not external_member_list: + logger.error(f'客户群聊用户数为0,无法绑定客户, name: {name}, erp_id: {erp_id}') + continue + if name != buyer_nick: + logger.error(f'客户群聊用户与ERP卖家名称不一致,无法绑定客户, name: {name}, erp_id: {erp_id}') + continue + if len(external_member_list) != 1: + external_member_list.sort(key=lambda user: user.get('join_time')) + logger.warning(f'客户群聊用户数不是1个,将选取最先入群的用户作为客户,name: {name}, erp_id: {erp_id}') + + for member in external_member_list: + member['order_id'] = order.get('trade_no') + + weixin_user = external_member_list[0] + weixin_user.update({ + 'order_id': order.get('trade_no'), + 'taobao_id': user.get('id', ''), + 'taobao_name': user.get('name', ''), + 'erp_message': user.get('message', ''), + 'erp_remark': user.get('memo', ''), + 'need_confirm': len(external_member_list) > 1, + }) + + continue + else: + # 员工 + erp_id = int(erp_id) + weixin_user = internal_member_dict.get(name) + logger.debug(f'name: {name}, erp_id: {erp_id}, weixin_user: {weixin_user}') + if not weixin_user: + logger.error(f'{internal_member_dict.keys()} {name}') + logger.error(f'客户群聊用户与ERP员工名称不一致,无法绑定员工, name: {name}, erp_id: {erp_id}') + continue + weixin_user.update({ + 'erp_id': erp_id, + 'erp_name': name, + 'role': user.get('role', ''), + }) + continue + + # 通过客户ID获取客户的所有订单 + async def get_order_relative_user_list_by_weixin_userid(self, user_id: str): + customers = await weixin_customer_controller.model.filter(weixin_id=user_id) + if not customers: + await weixin_customer_controller.create({ + "weixin_id": user_id, + }) + # 新用户:触发数据同步事件 + return False, [] + + order_list = [] + buyer_set = set() + for customer in customers: + buyer_nick = customer.taobao_name + buyer_id = customer.taobao_id + if buyer_nick: + logger.debug(f'name: {user_id}; buyer_nick: {buyer_nick}') + if buyer_nick in buyer_set: continue + buyer_set.add(buyer_nick) + orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick)) + elif buyer_id: + if buyer_id in buyer_set: continue + buyer_set.add(buyer_id) + orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id)) + else: + assert buyer_nick, "当前客户未绑定淘宝账号" + + logger.debug(f'orders.length: {len(orders)}') + order_list.extend(orders) + + # for order in orders: + # 原地按照id排序 + order_list.sort(key=lambda order: order.get('id'), reverse=True) + if order_list: + await self.get_user_detail_by_order(order=order_list[0]) + + return True, order_list + + # 根据客户名字,获取群聊中客户的所有订单 + async def get_order_relative_user_list_by_weixin_group_name(self, buyer_nick: str): + async with cache_if(f'order:relative_user:{buyer_nick}', ttl=10) as cache: + if cache.hit: + order_list = cache.value + logger.debug(f'{cache.key} hit') + else: + logger.debug(f'{cache.key} not hit') + order_list = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_nick=buyer_nick)) + cache.set(order_list) + logger.debug(f'order_list.length: {len(order_list)}') + + # for order in order_list: + order_list.sort(key=lambda order: order.get('id'), reverse=True) + if order_list: + await self.get_user_detail_by_order(order=order_list[0]) + order_list[0]['__type__'] = 'load_order_by_weixin_group_name' + + return order_list + + # 根据客户ID,获取群聊中客户的所有订单 + async def get_order_relative_user_list_by_weixin_group_buyer_id(self, buyer_id: str): + + orders = await async_generator_to_list(get_order_relative_user(lintao_client, buyer_id=buyer_id)) + logger.debug(f'orders.length: {len(orders)}') + + # for order in orders: + if orders: + await self.get_user_detail_by_order(order=orders[0]) + + return orders + + async def get_user_detail_by_order(self, order_id: str='', order: dict = None): + if not order: + orders = await async_generator_to_list(get_order_relative_user(lintao_client, trade_no=order_id)) + if not orders: return + _orders = [o for o in orders if o.get('ctid') == order_id] + # assert len(orders) == 1, f'订单号 {order_id} 对应多个订单' + if not _orders: + for order in orders: + logger.warning(f'订单号 {order_id} 对应多个订单,{order}') + # return {} + _orders = orders + + order = _orders[0] + + # 缓存订单产品信息 + tid = order.get("ctid") + async with cache_if(f'order:product:{tid}', ttl=3600*24) as cache: + if cache.hit: + product_resp = cache.value + else: + product_resp = (await list_product_raw(lintao_client, tid=tid)).get('data', {}) + if product_resp: cache.set(product_resp) + + if product_resp: + order['product_title'] = product_resp[0].get("title") + order['product_pic'] = product_resp[0].get("pic_path") + + order['has_fetched_user_detail'] = True + user_list = order.get("users", []) + + async with cache_if(f'user:auto_relate', ttl=600) as cache: + if cache.hit: + default_user_list = cache.value + else: + default_user_list = await weixin_user_controller.all(Q(auto_relate=True)) + default_user_list = [user.to_dict() for user in default_user_list] + cache.set(default_user_list) + default_user_set = {str(user.get('erp_id')) for user in default_user_list} + + for user in user_list: + erp_id = user.get('id', '') + if erp_id in default_user_set: continue + await self.get_order_user_info(user) + + order['users'].extend(default_user_list) + + # 根据 role_list 中的顺序进行排序 + role_order = {role: index for index, role in enumerate(ROLE_LIST)} + order['users'].sort(key=lambda user: role_order.get(user['role'], len(ROLE_LIST))) + + return order + + async def get_order_user_info(self, user: dict=None): + if not user: return + erp_id = user.get('id', '') + if not erp_id: return + if re.match(r'^\d+$', erp_id) is None: + # logger.debug(f'淘宝客户, erp_id: {erp_id}, user: {user}') + # 客户 + taobao_id = user.get('id', '') + query = Q(taobao_id=taobao_id) + weixin_customer_list = await weixin_customer_controller.all(query) + if not weixin_customer_list: + logger.error(f'客户未绑定订单, taobao_id: {taobao_id}, user: {user}') + return + weixin_customer = weixin_customer_list[0] + buyer = weixin_customer.to_dict() + if not buyer.get('name') and user.get('name'): + weixin_customer.taobao_name = user.get('name') + await weixin_customer.save() + user.update({**buyer, **user}) + else: + # 员工 + erp_id = int(erp_id) + # logger.debug(f'员工, erp_id: {erp_id}, user: {user}') + query = Q(erp_id=erp_id) + weixin_user_list = await weixin_user_controller.all(query) + if not weixin_user_list: + user['erp_id'] = erp_id + logger.error(f'员工未绑定ERP, erp_id: {erp_id}, user: {user}') + return + weixin_user = weixin_user_list[0] + user.update(weixin_user.to_dict()) + + # 获取erp日志 + async def get_erp_order_log(self, order_id: str): + return await get_order_log(lintao_client, order_id) + + async def expire_user_cache(self, weixin_id: str): + await invalidate_cache(f'customer:user_datail:{weixin_id}') + await invalidate_cache(f'customer:user_datail_0:{weixin_id}') + + # async def list_user_join_group(self, xingyun_id: int): + # assert xingyun_id, 'id 不能为空' + # result = await async_generator_to_list(list_user_join_group(xy_client, cid=xingyun_id)) + + # xingyun_chat_ids = [item.get('id') for item in result] + # all_group_chat = await self.model.filter(xingyun_chat_id__in=xingyun_chat_ids) + # group_chat_dict = {item.xingyun_chat_id: item for item in all_group_chat} + # logger.debug(f'list_user_join_group, xingyun_chat_ids: {xingyun_chat_ids}') + + # remain_chat_ids = set() + # for item in result: + # group_chat_id = item.get('id') + # if group_chat_id in group_chat_dict: continue + # remain_chat_ids.add(group_chat_id) + + # chat_name = item.get('chatName') + # if not chat_name: continue + # await self.load_xingyun_group_info(chat_name=chat_name) + + # remain_chat_list = await self.model.filter(xingyun_chat_id__in=remain_chat_ids) + # all_group_chat.extend(remain_chat_list) + # return all_group_chat + +weixin_group_chat_controller = WeixinGroupChatController() diff --git a/app/controllers/weixin/schedule.py b/app/controllers/weixin/schedule.py new file mode 100644 index 0000000..f70a889 --- /dev/null +++ b/app/controllers/weixin/schedule.py @@ -0,0 +1,169 @@ +from .customer import weixin_customer_controller +from .user import weixin_user_controller +from app.controllers.msg import msg_controller +from app.controllers.action import action_controller +from app.schemas.weixin import WeixinGroupChatEvent, BindOrderResultEvent, CustomerRepeatPurchaseEvent, CustomerAssignOrderEvent, CustomerRefundOrderEvent +from app.utils.common import transform_pydantic_to_list + +from datetime import datetime, timedelta +from app.utils.event_task import event_manager, EventType +import os +import asyncio + +from typing import Any + +from apscheduler.schedulers.asyncio import AsyncIOScheduler as BackgroundScheduler + +from app.controllers.automation.scenario import automation_scenario_controller +from app.controllers.automation.task import task_controller + + +sync_lock = asyncio.Lock() +import logging +logger = logging.getLogger(__name__) + +async def sync_xingyun_contact_info_async(event_type, *args, **kwargs): + if os.getenv("APP_ENV") != "prod": + logger.info(f'非生产环境,不实际同步客户数据,{args} {kwargs}') + return + + logger.info(f'同步客户数据,{args} {kwargs}') + now_time = datetime.now() + start_time = (now_time - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + end_time = now_time + timedelta(days=1) + await weixin_customer_controller.load_user_from_xingyun(add_time_start=start_time, add_time_end=end_time) + +async def load_all_user_from_xingyun(event_type, *args, **kwargs): + if os.getenv("APP_ENV") != "prod": + logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}') + return + + logger.info(f'从星云加载全量客户数据,{args} {kwargs}') + start_time = datetime(2025, 6, 1) + end_time = datetime.now() + timedelta(days=1) + return await weixin_customer_controller.load_all_user_from_xingyun(task_id='load_all_user_from_xingyun', add_time_start=start_time, add_time_end=end_time) + +async def monitor_erp_order(event_type, *args, **kwargs): + if os.getenv("APP_ENV") != "prod": + logger.info(f'非生产环境,不实际从星云加载全量客户数据,{args} {kwargs}') + return + + now_time = datetime.now() + logger.info(f'监控ERP订单,{args} {kwargs}') + new_order_list = [1] + new_order_list = await weixin_customer_controller.monitor_erp_order() + if new_order_list: + logger.info(f'监控到新订单,{new_order_list}') + await msg_controller.sync_msg() + logger.debug(f'耗时:{datetime.now() - now_time}') + +async def update_erp_order(*args, **kwargs): + logger.info(f'更新ERP订单,{args} {kwargs}') + await msg_controller.get_order_user_days_before(2) + +async def check_remark_action_is_done(*args, **kwargs): + if os.getenv("APP_ENV") != "prod": + logger.info(f'非生产环境,不实际检查备注操作是否完成,{args} {kwargs}') + return + + logger.info(f'检查备注操作是否完成,{args} {kwargs}') + await action_controller.check_remark_action_is_done() + + # 每天到点执行一次 + today = datetime.now() + todayStart = today.replace(hour=0, minute=0, second=0, microsecond=0) + if today.hour in [23] and today.minute >= 50: + todayStart += timedelta(days=1) + class_type = '早班' + elif today.hour in [0, 1] and today.minute <= 10: + class_type = '早班' + elif today.hour in [16] and today.minute >= 50 or today.hour in [17,18] and today.minute <= 10: + class_type = '晚班' + else: + return + + date = todayStart.strftime('%Y-%m-%d') + title = f'设置 {date} {class_type} 的员工活码' + success_title = title+' 成功' + fail_title = title+' 失败' + + async with sync_lock: + has_set = await msg_controller.model.filter(title__in=[success_title]).first() + if has_set: + logger.info(f'【已操作】{title}') + return + + logger.info(f'【开始】{title}') + try: + result, next_class = await weixin_user_controller.set_user_online(class_type=class_type, date=todayStart) + except Exception as e: + logger.exception(f'设置 {date} {class_type} 的员工活码失败,{e}') + result, next_class = {}, None + + is_failed = bool(list(filter(lambda x: x.get('fail') != 0, result.values()))) or not result + logger.info(f'【完成】{title},结果:{result} {"失败" if is_failed else "成功"}') + + result_str = '\n'.join([f'{k}:{v}' for k, v in result.items()]) + + logger.info(f'{success_title if not is_failed else fail_title},{result_str}') + content = f'\n\n{result_str}' + if result and not next_class: + content += f'\n\n⚠️ 未设置下一班值班人员,请及时设置' + logger.info(f'消息通知:{content}') + await msg_controller.new_system_msg(title=success_title if not is_failed else fail_title, content=content) + await msg_controller.sync_msg() + +async def group_chat_update(event_type, group_info, *args): + if os.getenv("APP_ENV") != "prod": + logger.info(f'非生产环境,不实际检查群聊更新,{group_info} {args}') + return + + group_event = WeixinGroupChatEvent(**group_info) + await sync_xingyun_contact_info_async() + + +async def test_event(event_type, event_data=None, *args, **kwargs): + # logger.info(f'测试事件,{args} {kwargs}') + # scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data) + # logger.info(f'触发事件,{len(scenario_list)}') + # for scenario in scenario_list: + # task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data) + # logger.info(f'创建任务,{task}') + pass + +async def trigger_event(event_type: str, event_data: Any): + """解析事件数据""" + logger.info(f'测试事件,{event_type} {event_data}') + scenario_list = await automation_scenario_controller.find_active_scenarios(event_type, event_data=event_data) + logger.info(f'触发事件,{len(scenario_list)}') + for scenario, reason in scenario_list: + logger.info(f'触发场景,{scenario},{reason}') + try: + task = await task_controller.create_from_scenario(scenario=scenario, event_data=event_data, reason=reason) + logger.info(f'创建任务,{task}') + except Exception as e: + logger.error(f'创建任务失败,{e}') + +# print(f'事件订阅:{transform_pydantic_to_list(WeixinGroupChatEvent)}') + +event_manager.subscribe(EventType.SYNC_XINGYUN_CONTACT_INFO, load_all_user_from_xingyun) +event_manager.subscribe(EventType.OPEN_PERSONAL_CHAT, sync_xingyun_contact_info_async) +event_manager.subscribe(EventType.GROUP_CHAT_UPDATED, group_chat_update, input_model=transform_pydantic_to_list(WeixinGroupChatEvent)) +event_manager.subscribe(EventType.BIND_ORDER_FOR_USER, sync_xingyun_contact_info_async, input_model=transform_pydantic_to_list(BindOrderResultEvent)) +event_manager.subscribe(EventType.OLD_CUSTOMER_REPEAT_PURCHASE, test_event, input_model=transform_pydantic_to_list(CustomerRepeatPurchaseEvent)) +event_manager.subscribe(EventType.CUSTOMER_SERVICE_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent)) +event_manager.subscribe(EventType.DESIGNER_ASSIGN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerAssignOrderEvent)) +event_manager.subscribe(EventType.CUSTOMER_RETURN_ORDER, test_event, input_model=transform_pydantic_to_list(CustomerRefundOrderEvent)) +event_manager.subscribe(EventType.DESIGNER_UPLOAD_DESIGN, test_event) + +event_manager.register_pretask(trigger_event) + +if os.getenv("APP_ENV") == "prod": + logger.info('生产环境,启动定时任务') + # 创建调度器 + scheduler = BackgroundScheduler() + + # scheduler.add_job(monitor_erp_order, "interval", seconds=60) # 每90秒执行一次 + scheduler.add_job(update_erp_order, "interval", seconds=240) # 每90秒执行一次 + scheduler.add_job(check_remark_action_is_done, "interval", seconds=180) # 每90秒执行一次 + scheduler.start() diff --git a/app/controllers/weixin/user.py b/app/controllers/weixin/user.py new file mode 100644 index 0000000..ab1c01e --- /dev/null +++ b/app/controllers/weixin/user.py @@ -0,0 +1,201 @@ + +from app.core.crud import CRUDBase +from app.schemas.weixin import ( + WeixinUserCreate, + WeixinUserUpdate, +) + +from app.models.weixin import WeixinUser +from app.schemas.weixin import WeixinUserBindInfo + +from some_sdk.services.binder import feishu_client, xy_client +from some_sdk.feishu_sdk.biz.doc import search_file_record +from some_sdk.xingyun_sdk.apis.channel import list_channel_group, list_channel_group_user_list, set_online_staff + +import logging +logger = logging.getLogger(__name__) + +from datetime import datetime + +def split_generator(iterable, chunk_size=50): + """ + 将可迭代对象拆分为多个子列表 + """ + chunk = [] + for item in iterable: + chunk.append(item) + if len(chunk) == chunk_size: + yield chunk + chunk = [] + if chunk: + yield chunk + +class WeixinUserController(CRUDBase[WeixinUser, WeixinUserCreate, WeixinUserUpdate]): + def __init__(self): + super().__init__(model=WeixinUser) + + async def update_from_group(self, internal_members: list[dict]): + # 同步更新数据库 + member_dict = {} + for member in internal_members: + member = member.copy() + userid = member.get('userid', None) + member['alias'] = member.pop('name', None) + member_dict[userid] = member + + member_list = await self.model.filter(userid__in=member_dict.keys()) + + update_list = [] + update_fileds = ['alias', 'role', 'erp_id', 'erp_name'] + + history = {} + for member in member_list: + member_info = member_dict.pop(member.userid, history.get(member.userid, {})) + if member.role in ['owner', 'admin', 'follower']: + continue + if not member_info: continue + history[member.userid] = member_info + + updated = {} + for field in update_fileds: + old_value = getattr(member, field) + if field not in member_info: + continue + value = member_info.get(field) + if old_value != value: + setattr(member, field, value) + updated[f'{field}:{str(old_value)}'] = value + if updated: + logger.info(f'用户 {member.userid} 更新关系为 {updated}') + update_list.append(member) + if update_list: + await self.model.bulk_update(update_list, fields=update_fileds) + + # 新增用户 + create_list = [] + for userid, member_info in member_dict.items(): + create_list.append(WeixinUser( + **member_info, + name=member_info.get('alias'), + )) + if create_list: + await self.model.bulk_create(create_list) + + return { + 'update_count': len(update_list), + 'create_count': len(create_list), + } + + + async def bind_user(self, user_in: WeixinUserBindInfo): + if not user_in.userid: + raise ValueError("用户ID不能为空") + if user_in.erp_id == 0: + del user_in.erp_id + + user = await self.model.filter(userid=user_in.userid).first() + if not user: + await self.create({**user_in.model_dump(exclude_unset=True), 'name': user_in.username}) + return {} + await self.model.filter(userid=user_in.userid).update(**user_in.model_dump(exclude_unset=True)) + + async def set_user_online(self, class_type: str, date: datetime): + daysTimestamp = lambda nowStamp, days: nowStamp + days * 24 * 60 * 60 * 1000 + formatDate = lambda ts: datetime.fromtimestamp(ts / 1000).strftime('%Y-%m-%d') + + today = int(date.timestamp() * 1000) + logger.info(f'设置 {formatDate(today)} 班次为 {class_type} 的员工为在线状态') + yestoday = daysTimestamp(today, -1) + tomorrow = daysTimestamp(today, 1) + + # 查询日期为 yestoday 到 后天 之间的所有记录 + resp = search_file_record( + feishu_client, + app_token="V4qebMG2kamCWMslJmdcQMbtngW", + table_id="tblKgjc3F2jZ2inC", + filter={ + "conjunction": "and", + "conditions": [{ + "field_name": "日期", + "operator": "isGreater", + "value": ["ExactDate", yestoday] + },{ + "field_name": "日期", + "operator": "isLess", + "value": ["ExactDate", daysTimestamp(today, 2)] + }] + }, + ) + + set_result = {} + current_class = (class_type, today) + next_class = ('早班', tomorrow) if class_type == '晚班' else ('晚班', today) + + current_class_mapping = {} + next_class_mapping = {} + async for data in resp: + user = data.get('data', {}) + user_online = ((user.get('班次') or '')[:2], user.get('日期', '')) + # logger.info(f'用户{user.get("接量成员", "")} 在线时间为{user_online} 当前班次为{current_class} 下一班次为{next_class}') + class_group = user.get('组别', '') + class_mapping = None + # 当前班次 + if user_online == current_class: + class_mapping = current_class_mapping + set_result[class_group] = {"接量成员": user.get('接量成员', '')} + # 下一班次 + elif user_online == next_class: + class_mapping = next_class_mapping + if class_mapping is None: + continue + + if user.get('id', ''): + class_mapping.setdefault(class_group, []) + class_mapping[class_group].append(user.get('id', '')) + + assert current_class_mapping, f"根据日期{current_class[1]} {formatDate(current_class[1])}没有找到班次为{class_type}的员工" + logger.info(f'根据日期{current_class[1]} {formatDate(current_class[1])} 找到班次为{class_type}的员工: {current_class_mapping} {set_result}') + if next_class_mapping: + logger.info(f'根据日期{next_class[1]} {formatDate(next_class[1])} 找到班次为{next_class[0]}的员工: {next_class_mapping}') + + group_list = list_channel_group(xy_client) + async for group in group_list: + title = group.get("title", "") + if title not in current_class_mapping: + continue + + set_result[title] = set_result.get(title) or {} + try: + user_list = list_channel_group_user_list(xy_client, groupId=group.get("id", "")) + + huomaList = [] + async for user in user_list: + huomaList.append(user.get('id')) + + if not huomaList: + logger.error(f'组 {title} {group.get("id", "")} 中没有员工') + continue + + is_faild, is_success = [], [] + # 按照50个一组设置在线状态 + for i in range(0, len(huomaList), 50): + result = await set_online_staff(xy_client, allDayUserIds=','.join(current_class_mapping[title]), huomaList=huomaList[i:i+50]) + + is_faild += list(filter(lambda x: x.get('errorMsg', ''), result.get('data', []))) + is_success += list(filter(lambda x: x.get('success', True), result.get('data', []))) + + is_faild = set(is_faild) + if is_faild: logger.error(f'设置失败的员工: {is_faild}') + else: logger.info(f'全部员工设置成功') + + set_result[title] = { + 'success': len(is_success), + 'fail': len(is_faild), + **set_result[title] + } + except Exception as e: + logger.exception(f'设置群 {group.get("id", "")} 中员工 {huomaList} 为在线状态失败: {e}') + + return set_result, next_class_mapping + +weixin_user_controller = WeixinUserController() diff --git a/app/controllers/weixin/utils.py b/app/controllers/weixin/utils.py new file mode 100644 index 0000000..c8c48bf --- /dev/null +++ b/app/controllers/weixin/utils.py @@ -0,0 +1,4 @@ +def get_buyer_nick_from_group_name(group_name: str): + buyer_nick = group_name.rsplit('-', 1)[0] if '服务群' in group_name else group_name + return buyer_nick + diff --git a/app/controllers/weixin_sdk.py b/app/controllers/weixin_sdk.py new file mode 100644 index 0000000..d0b233f --- /dev/null +++ b/app/controllers/weixin_sdk.py @@ -0,0 +1,20 @@ +from some_sdk.weixin_sdk.session import get_logged_in_client +from some_sdk.weixin_sdk.apis.userlist import list_department_user, get_user_detail_by_vid + +# 首次调用:登录并创建 client +client = get_logged_in_client() + +def test_list_department_user(partyid: str): + department_list = list_department_user(client, partyid) + for index, department in enumerate(department_list, 1): + print(f'{index}. {department}') + print('==== ' * 10) + + return + + +if __name__ == "__main__": + test_list_department_user('1688852859949799') + + + diff --git a/app/controllers/xingyun_sdk.py b/app/controllers/xingyun_sdk.py new file mode 100644 index 0000000..7bab3ca --- /dev/null +++ b/app/controllers/xingyun_sdk.py @@ -0,0 +1,66 @@ +from some_sdk.xingyun_sdk.session import get_logged_in_client +from some_sdk.xingyun_sdk.biz.by_user import get_user_order +from .wechat_sdk import get_external_group_chat_info +from app.models.weixin import WeixinUser + +# 首次调用:登录并创建 client +client = get_logged_in_client() + +def get_user_order_list(group_name: str="", keyword: str="", regrex: str=None, userIds: list = None): + result = get_user_order(client, keyword=keyword, group_name=group_name, regrex=regrex, userIds=userIds) + if not result: + return {} + + order_list = result.pop("order_list", []) + print(f'在群聊 {result["group_name"]} 中的客户 {result["name"]}({result["keyword"]}) 有订单数: {len(order_list)}') + + subTradeNo_list = [] + for order in order_list: + result_order = dict(tradeNo='', tradeTime=order.get("tradeTime", ''), payTime=order.get("payTime", '')) + orderList = order.get("orderList", []) + + for item in orderList: + result_order['shopName'] = order.get("shopName", "") + result_order['goodsName'] = item.get("goodsName", "") + result_order['actuPayment'] = order.get("actuPayment", "") + result_order['goodsCount'] = order.get("goodsCount", "") + result_order['tradeNo'] = item.get("subTradeNo", "") + result_order['pic'] = item.get("pic", "") + result_order['other_status'] = order.get("orderStatus", 0) + result_order['other_status_name'] = order.get("status_name", "") + subTradeNo_list.append(result_order.copy()) + + import json; print(json.dumps(subTradeNo_list, indent=4, ensure_ascii=False)) + result['order_list'] = subTradeNo_list + return result + +async def get_user_order_list_by_chatid(chat_id: str): + group_chat_info = await get_external_group_chat_info(chat_id) + external_member_list = group_chat_info.get('external_member_list', []) + + trade_list = [] + group_chat_info['trade_list'] = trade_list + + invitor = group_chat_info.get('owner') + qiwei_kefu = await WeixinUser.filter(userid=invitor).first() + print(f'邀请人:{invitor} {qiwei_kefu.username} {qiwei_kefu.crmid}') + + group_name = group_chat_info.get('name') + # if not group_name: + # group_name = '未知群聊' + + print(f'群聊名称{group_name},有{len(external_member_list)}位客户', flush=True) + for user in external_member_list: + print(f'正在处理客户:{user.get("name")}', flush=True) + result = get_user_order_list(group_name=group_name, keyword=user.get('name'), userIds=[qiwei_kefu.crmid] if qiwei_kefu.crmid else None) + if not result: + continue + + trade_list.append(result) + + return group_chat_info + +if __name__ == "__main__": + get_user_order("fuyixuan0628-印刷vip客户服务群", "去看海吗") + get_user_order("小旋honey-印刷vip客户服务群@喜印说", "宇宙大女神") + diff --git a/app/core/bgtask.py b/app/core/bgtask.py new file mode 100644 index 0000000..6d5b6fe --- /dev/null +++ b/app/core/bgtask.py @@ -0,0 +1,31 @@ +from starlette.background import BackgroundTasks + +from .ctx import CTX_BG_TASKS + + +class BgTasks: + """后台任务统一管理""" + + @classmethod + async def init_bg_tasks_obj(cls): + """实例化后台任务,并设置到上下文""" + bg_tasks = BackgroundTasks() + CTX_BG_TASKS.set(bg_tasks) + + @classmethod + async def get_bg_tasks_obj(cls): + """从上下文中获取后台任务实例""" + return CTX_BG_TASKS.get() + + @classmethod + async def add_task(cls, func, *args, **kwargs): + """添加后台任务""" + bg_tasks = await cls.get_bg_tasks_obj() + bg_tasks.add_task(func, *args, **kwargs) + + @classmethod + async def execute_tasks(cls): + """执行后台任务,一般是请求结果返回之后执行""" + bg_tasks = await cls.get_bg_tasks_obj() + if bg_tasks.tasks: + await bg_tasks() diff --git a/app/core/cache.py b/app/core/cache.py new file mode 100644 index 0000000..e6d0f3a --- /dev/null +++ b/app/core/cache.py @@ -0,0 +1,163 @@ +# core/cache.py +import json as _stdlib_json +from contextlib import asynccontextmanager +from typing import Any, AsyncGenerator, Optional, Union + +import logging +logger = logging.getLogger(__name__) + +from redis.asyncio import Redis + +# ===== 尝试使用你的 fff,否则回退到 stdlib ===== +try: + import orjson as json +except ImportError: + json = _stdlib_json # type: ignore + + +# ===== Redis 客户端单例(可替换为你自己的)===== +class RedisClient: + _instance: Optional["RedisClient"] = None + _redis: Optional[Redis] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + async def init_redis(self, url: str = "redis://localhost:6379/0") -> None: + if self._redis is None: + self._redis = Redis.from_url(url, decode_responses=False) + + @property + def client(self) -> Redis: + if self._redis is None: + raise RuntimeError("Redis not initialized. Call init_redis() first.") + return self._redis + + async def close(self) -> None: + if self._redis: + await self._redis.close() + self._redis = None + + +redis_client = RedisClient() + + +# ===== FastAPI 依赖注入 ===== +async def get_redis() -> Redis: + """FastAPI 依赖:获取 Redis 客户端""" + return redis_client.client + +async def invalidate_cache(key: str) -> bool: + """ + 主动删除缓存键。 + 返回是否成功删除(Redis delete 返回被删除的 key 数量)。 + """ + try: + result = await redis_client.client.delete(key) + return result > 0 + except Exception as e: + logger.exception(f"Failed to invalidate cache key: {key}", exc_info=e) + return False + +# ===== 缓存上下文管理器 ===== +class CacheResult: + __slots__ = ("key", "hit", "value", "_to_set", "_set_called") + + def __init__(self, key: str) -> None: + self.key = key + self.hit = False + self.value: Any = None + self._to_set: Any = None + self._set_called = False + + def set(self, value: Any) -> None: + """标记要缓存的值(可为 None)""" + self._to_set = value + self._set_called = True + + +@asynccontextmanager +async def cache_if( + key: str, + ttl: int = 3600, + redis: Optional[Redis] = None, +) -> AsyncGenerator[CacheResult, None]: + """ + 异步缓存上下文管理器,支持 None 值缓存(防穿透)。 + + Args: + key: 缓存键 + ttl: 正常值缓存时间(秒) + redis: 可选 Redis 客户端(用于测试或自定义) + + Usage: + async with cache_if("report:123") as cache: + if cache.hit: + return cache.value + result = await compute() + cache.set(result) # result 可为 None + """ + result = CacheResult(key) + client = redis or redis_client.client + + # 尝试读缓存 + try: + cached_val = await client.get(key) + if cached_val is not None: + # 解码 + if cached_val == b"__NULL__": + result.hit = True + result.value = None + else: + result.hit = True + result.value = json.loads(cached_val) + except Exception: + # Redis 不可用,降级(不中断主流程) + logger.exception(f"Redis get error for key: {key}") + pass + + yield result + + # 写缓存(仅当调用了 set()) + if result._set_called: + try: + if result._to_set is None: + val = b"__NULL__" + ex = 60 # 空值短 TTL + else: + # 注意:orjson.dumps 返回 bytes,stdlib 返回 str → 统一转 bytes + serialized = json.dumps(result._to_set) + val = serialized if isinstance(serialized, bytes) else serialized.encode("utf-8") + ex = ttl + await client.setex(key, ex, val) + except Exception: + # 写缓存失败,不影响主流程 + pass + + +# ===== 装饰器版(可选补充)===== +from functools import wraps +import asyncio +import hashlib + +def cached(ttl: int = 3600): + """函数缓存装饰器(使用 cache_if)""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # 生成 key(简单版,可替换为更 robust 的) + key_data = str(args) + str(sorted(kwargs.items())) + key = f"cached:{func.__name__}:{hashlib.md5(key_data.encode()).hexdigest()}" + logger.debug(f"Cache key: {key}") + + async with cache_if(key, ttl) as cache: + if cache.hit: + logger.debug(f"Cache hit for key: {key}") + return cache.value + result = await func(*args, **kwargs) + cache.set(result) + return result + return wrapper + return decorator \ No newline at end of file diff --git a/app/core/crud.py b/app/core/crud.py new file mode 100644 index 0000000..ae2ee40 --- /dev/null +++ b/app/core/crud.py @@ -0,0 +1,78 @@ +from typing import Any, Callable, Dict, Generic, List, NewType, Tuple, Type, TypeVar, Union + +from pydantic import BaseModel +from tortoise.expressions import Q +from tortoise.models import Model + +Total = NewType("Total", int) +ModelType = TypeVar("ModelType", bound=Model) +CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) + + +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + def __init__(self, model: Type[ModelType]): + self.model = model + + async def is_exist(self, **kwargs) -> bool: + return await self.model.filter(**kwargs).first() + + async def all(self, search: Q = Q()) -> List[ModelType]: + return await self.model.filter(search).all() + + async def get(self, id: int) -> ModelType: + return await self.model.get(id=id) + + async def list(self, page: int, page_size: int, search: Q = Q(), order: list = []) -> Tuple[Total, List[ModelType]]: + query = self.model.filter(search) + return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order) + + async def create(self, obj_in: CreateSchemaType) -> ModelType: + if isinstance(obj_in, Dict): + obj_dict = obj_in + else: + obj_dict = obj_in.model_dump() + obj = self.model(**obj_dict) + await obj.save() + return obj + + async def copy(self, id: int, handler: Callable[[Dict[str, Any]], Dict[str, Any]] = None) -> ModelType: + obj = await self.get(id=id) + obj_dict = await obj.to_dict() + drop_keys = ["id", "created_at", "updated_at"] + for key in drop_keys: + obj_dict.pop(key, None) + if handler: + obj_dict = handler(obj_dict) + obj = self.model(**obj_dict) + await obj.save() + return obj + + async def update(self, id: int, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> ModelType: + if isinstance(obj_in, Dict): + obj_dict = obj_in + else: + obj_dict = obj_in.model_dump(exclude_unset=True, exclude={"id"}) + obj = await self.get(id=id) + obj = obj.update_from_dict(obj_dict) + await obj.save() + return obj + + async def create_or_update(self, obj_in: CreateSchemaType, query_kwargs: Dict[str, Any], update_kwargs: Dict[str, Any] = None) -> ModelType: + update_kwargs = update_kwargs or {} + orm = await self.model.filter(**query_kwargs).first() + if orm: + need_update = False if update_kwargs else True + for key, value in update_kwargs.items(): + if getattr(orm, key) != value: + need_update = True + break + if need_update: + return 'update', await self.update(orm.id, obj_in) + else: + return False, orm + return 'create', await self.create(obj_in) + + async def remove(self, id: int) -> None: + obj = await self.get(id=id) + await obj.delete() diff --git a/app/core/ctx.py b/app/core/ctx.py new file mode 100644 index 0000000..c97a0fc --- /dev/null +++ b/app/core/ctx.py @@ -0,0 +1,17 @@ +import contextvars + +from starlette.background import BackgroundTasks + +CTX_USER_ID: contextvars.ContextVar[int] = contextvars.ContextVar("user_id", default=0) +CTX_USER_NAME: contextvars.ContextVar[str] = contextvars.ContextVar("user_name", default="") +CTX_BG_TASKS: contextvars.ContextVar[BackgroundTasks] = contextvars.ContextVar("bg_task", default=None) + +def set_ctx_weixin_user(user_id: int, user_name: str): + CTX_USER_ID.set(user_id) + CTX_USER_NAME.set(user_name) + +def get_ctx_weixin_user(): + return { + "user_id": CTX_USER_ID.get(), + "user_name": CTX_USER_NAME.get(), + } diff --git a/app/core/dependency.py b/app/core/dependency.py new file mode 100644 index 0000000..fb454db --- /dev/null +++ b/app/core/dependency.py @@ -0,0 +1,69 @@ +from typing import Optional + +import jwt +from fastapi import Depends, Header, HTTPException, Request + +from app.core.ctx import CTX_USER_ID +from app.models import Role, User, WeixinUser +from app.settings import settings + + +class AuthControl: + @classmethod + async def is_authed(cls, token: str = Header(..., description="token验证")) -> Optional["User"]: + try: + if token == "dev": + user = await User.filter().first() + user_id = user.id + else: + decode_data = jwt.decode(token, settings.SECRET_KEY, algorithms=settings.JWT_ALGORITHM) + user_id = decode_data.get("user_id") + user = await User.filter(id=user_id).first() + if not user: + raise HTTPException(status_code=401, detail="Authentication failed") + CTX_USER_ID.set(int(user_id)) + return user + except jwt.DecodeError: + raise HTTPException(status_code=401, detail="无效的Token") + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="登录已过期") + except Exception as e: + raise HTTPException(status_code=500, detail=f"{repr(e)}") + + @classmethod + async def weixin_user(cls, request: Request) -> WeixinUser: + path = request.url.path + if path in [ + "/api/v1/msg/new_order", + "/api/v1/event/feishu", + "/api/v1/msg/get_order_user_days_before", + ]: + return None + + token = request.headers.get("token") + user = await WeixinUser.filter(userid=token).first() + assert user, "Authentication failed" + CTX_USER_ID.set(int(user.id)) + return user # 返回的是 WeixinUser 实例 + +class PermissionControl: + @classmethod + async def has_permission(cls, request: Request, current_user: User = Depends(AuthControl.is_authed)) -> None: + if current_user.is_superuser: + return + method = request.method + path = request.url.path + roles: list[Role] = await current_user.roles + if not roles: + raise HTTPException(status_code=403, detail="The user is not bound to a role") + apis = [await role.apis for role in roles] + permission_apis = list(set((api.method, api.path) for api in sum(apis, []))) + # path = "/api/v1/auth/userinfo" + # method = "GET" + if (method, path) not in permission_apis: + raise HTTPException(status_code=403, detail=f"Permission denied method:{method} path:{path}") + + +DependAuth = Depends(AuthControl.is_authed) +DependWeixinUser = Depends(AuthControl.weixin_user) +DependPermisson = Depends(PermissionControl.has_permission) diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..af4f17a --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,150 @@ +from fastapi.exceptions import ( + HTTPException, + RequestValidationError, + ResponseValidationError, +) +from fastapi.requests import Request +from fastapi.responses import JSONResponse +from tortoise.exceptions import DoesNotExist, IntegrityError + +from app.http_base import HttpResp + +import logging +logger = logging.getLogger(__name__) + +class SettingNotFound(Exception): + pass + + +class AssertException(Exception): + """断言异常""" + def __init__(self, message: str, code: int = 1001): + self.message = message + self.code = code + super().__init__(message) + + +async def DoesNotExistHandle(req: Request, exc: DoesNotExist) -> JSONResponse: + """处理对象不存在异常""" + resp = HttpResp.REQUEST_404_ERROR + content = dict( + code=resp.code, + msg=f"Object has not found, exc: {exc}, query_params: {req.query_params}", + data=None + ) + logger.warning(f"Object not found: {exc}, path: {req.url.path}") + return JSONResponse(content=content, status_code=200) + + +async def IntegrityHandle(req: Request, exc: IntegrityError) -> JSONResponse: + """处理数据完整性异常""" + resp = HttpResp.SYSTEM_ERROR + content = dict( + code=resp.code, + msg=f"IntegrityError: {str(exc)}", + data=None + ) + logger.error(f"Integrity error: {exc}, path: {req.url.path}", exc_info=True) + return JSONResponse(content=content, status_code=200) + + +async def HttpExcHandle(req: Request, exc: HTTPException) -> JSONResponse: + """处理 HTTP 异常""" + content = dict( + code=exc.status_code, + msg=exc.detail, + data=None + ) + logger.warning(f"HTTP exception: {exc.status_code} - {exc.detail}, path: {req.url.path}") + return JSONResponse(content=content, status_code=200) + + +async def RequestValidationHandle(req: Request, exc: RequestValidationError) -> JSONResponse: + """处理请求验证异常""" + resp = HttpResp.PARAMS_VALID_ERROR + errors = [] + for error in exc.errors(): + errors.append({ + "loc": error["loc"], + "msg": error["msg"], + "type": error["type"] + }) + + content = dict( + code=resp.code, + msg=f"{resp.msg}, errors: {errors}", + data=errors + ) + logger.warning(f"Request validation error: {errors}, path: {req.url.path}") + return JSONResponse(content=content, status_code=200) + + +async def ResponseValidationHandle(req: Request, exc: ResponseValidationError) -> JSONResponse: + """处理响应验证异常""" + resp = HttpResp.SYSTEM_ERROR + content = dict( + code=resp.code, + msg=f"Response validation error: {str(exc)}", + data=None + ) + logger.error(f"Response validation error: {exc}, path: {req.url.path}", exc_info=True) + return JSONResponse(content=content, status_code=200) + + +async def AssertExceptionHandle(request: Request, exc: AssertException) -> JSONResponse: + """处理断言异常""" + resp = HttpResp.ASSERT_ARGUMENT_ERROR + content = dict( + code=exc.code, + msg=exc.message, + data=None + ) + logger.warning(f"Assert exception: {exc.message}, path: {request.url.path}") + return JSONResponse( + content=content, + status_code=200 + ) + + +async def AssertionErrorHandle(request: Request, exc: AssertionError) -> JSONResponse: + """处理 Python assert 异常""" + resp = HttpResp.ASSERT_ARGUMENT_ERROR + message = str(exc) if exc.args else resp.msg + content = dict( + code=resp.code, + msg=message, + data=None + ) + logger.warning(f"Assertion error: {message}, path: {request.url.path}") + return JSONResponse( + content=content, + status_code=200 + ) + + +async def GlobalExceptionHandler(request: Request, exc: Exception) -> JSONResponse: + """全局异常处理器""" + resp = HttpResp.SYSTEM_ERROR + content = dict( + code=resp.code, + msg=f"{resp.msg}: {str(exc)}", + data=None + ) + logger.error(f"Global exception: {str(exc)}, path: {request.url.path}", exc_info=True) + return JSONResponse( + content=content, + status_code=200 + ) + + +# 便捷函数用于注册所有异常处理器 +def register_exception_handlers(app): + """注册所有异常处理器到 FastAPI 应用""" + app.add_exception_handler(DoesNotExist, DoesNotExistHandle) + app.add_exception_handler(IntegrityError, IntegrityHandle) + app.add_exception_handler(HTTPException, HttpExcHandle) + app.add_exception_handler(RequestValidationError, RequestValidationHandle) + app.add_exception_handler(ResponseValidationError, ResponseValidationHandle) + app.add_exception_handler(AssertException, AssertExceptionHandle) + app.add_exception_handler(AssertionError, AssertionErrorHandle) # 捕获 Python assert + app.add_exception_handler(Exception, GlobalExceptionHandler) # 全局异常处理器 diff --git a/app/core/init_app.py b/app/core/init_app.py new file mode 100644 index 0000000..9f000ea --- /dev/null +++ b/app/core/init_app.py @@ -0,0 +1,273 @@ +import shutil + +from aerich import Command +from fastapi import FastAPI +from fastapi.middleware import Middleware +from fastapi.middleware.cors import CORSMiddleware +from tortoise.expressions import Q + +from app.api import api_router +from app.controllers.api import api_controller +from app.controllers.user import UserCreate, user_controller +from app.core.exceptions import register_exception_handlers +from app.log import logger +from app.models.admin import Api, Menu, Role, Dept +from app.models.automation import Task, Scenario +from app.schemas.menus import MenuType +from app.settings.config import settings +from app.core.cache import redis_client + +from .middlewares import BackGroundTaskMiddleware, HttpAuditLogMiddleware + + +def make_middlewares(): + middleware = [ + Middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=settings.CORS_ALLOW_CREDENTIALS, + allow_methods=settings.CORS_ALLOW_METHODS, + allow_headers=settings.CORS_ALLOW_HEADERS, + ), + Middleware(BackGroundTaskMiddleware), + Middleware( + HttpAuditLogMiddleware, + methods=["GET", "POST", "PUT", "DELETE"], + exclude_paths=[ + "/api/v1/msg/new_order", + "/api/v1/weixin/jssdk-config", + "/api/v1/base/access_token", + "/docs", + "/api-docs", + "/openapi.json", + "/static/*", + "/gen/*", + ], + ), + ] + return middleware + +def mount_static_and_config_swagger(app: FastAPI): + + from fastapi.staticfiles import StaticFiles + from fastapi.openapi.docs import get_swagger_ui_html + + app.mount("/static", StaticFiles(directory="static"), name="静态文件") + + @app.get(app.docs_url, include_in_schema=False) + async def custom_swagger_ui_html(): + print('访问文档') + return get_swagger_ui_html( + openapi_url=app.openapi_url, + title=app.title, + swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js", + swagger_css_url="/static/swagger-ui/swagger-ui.css" + ) + +def register_exceptions(app: FastAPI): + register_exception_handlers(app) + print('注册异常处理完成') + + +def register_routers(app: FastAPI, prefix: str = "/api"): + app.include_router(api_router, prefix=prefix) + + +async def init_superuser(): + user = await user_controller.model.exists() + if not user: + await user_controller.create_user( + UserCreate( + username="admin", + email="admin@admin.com", + password="123456", + is_active=True, + is_superuser=True, + ) + ) + + +async def init_menus(): + menus = await Menu.exists() + if not menus: + parent_menu = await Menu.create( + menu_type=MenuType.CATALOG, + name="系统管理", + path="/system", + order=1, + parent_id=0, + icon="carbon:gui-management", + is_hidden=False, + component="Layout", + keepalive=False, + redirect="/system/user", + ) + children_menu = [ + Menu( + menu_type=MenuType.MENU, + name="用户管理", + path="user", + order=1, + parent_id=parent_menu.id, + icon="material-symbols:person-outline-rounded", + is_hidden=False, + component="/system/user", + keepalive=False, + ), + Menu( + menu_type=MenuType.MENU, + name="角色管理", + path="role", + order=2, + parent_id=parent_menu.id, + icon="carbon:user-role", + is_hidden=False, + component="/system/role", + keepalive=False, + ), + Menu( + menu_type=MenuType.MENU, + name="菜单管理", + path="menu", + order=3, + parent_id=parent_menu.id, + icon="material-symbols:list-alt-outline", + is_hidden=False, + component="/system/menu", + keepalive=False, + ), + Menu( + menu_type=MenuType.MENU, + name="API管理", + path="api", + order=4, + parent_id=parent_menu.id, + icon="ant-design:api-outlined", + is_hidden=False, + component="/system/api", + keepalive=False, + ), + Menu( + menu_type=MenuType.MENU, + name="部门管理", + path="dept", + order=5, + parent_id=parent_menu.id, + icon="mingcute:department-line", + is_hidden=False, + component="/system/dept", + keepalive=False, + ), + Menu( + menu_type=MenuType.MENU, + name="审计日志", + path="auditlog", + order=6, + parent_id=parent_menu.id, + icon="ph:clipboard-text-bold", + is_hidden=False, + component="/system/auditlog", + keepalive=False, + ), + ] + await Menu.bulk_create(children_menu) + await Menu.create( + menu_type=MenuType.MENU, + name="一级菜单", + path="/top-menu", + order=2, + parent_id=0, + icon="material-symbols:featured-play-list-outline", + is_hidden=False, + component="/top-menu", + keepalive=False, + redirect="", + ) + + +async def init_apis(): + apis = await api_controller.model.exists() + if not apis: + await api_controller.refresh_api() + + +async def init_db(): + command = Command(tortoise_config=settings.TORTOISE_ORM) + try: + await command.init_db(safe=True) + except FileExistsError: + pass + + await command.init() + try: + await command.migrate() + except AttributeError: + logger.warning("unable to retrieve model history from database, model history will be created from scratch") + shutil.rmtree("migrations") + await command.init_db(safe=True) + + await command.upgrade(run_in_transaction=True) + + +async def init_roles(): + roles = await Role.exists() + if not roles: + admin_role = await Role.create( + name="管理员", + desc="管理员角色", + ) + user_role = await Role.create( + name="普通用户", + desc="普通用户角色", + ) + + await Dept.create(name="默认部门") + + # 分配所有API给管理员角色 + all_apis = await Api.all() + await admin_role.apis.add(*all_apis) + # 分配所有菜单给管理员和普通用户 + all_menus = await Menu.all() + await admin_role.menus.add(*all_menus) + await user_role.menus.add(*all_menus) + + # 为普通用户分配基本API + basic_apis = await Api.filter(Q(method__in=["GET"]) | Q(tags="基础模块")) + await user_role.apis.add(*basic_apis) + +async def init_cache(): + await redis_client.init_redis(settings.REDIS_URL) + +async def init_task(): + + task = await Task.exists(title="系统待办") + if task: return + + scenario = await Scenario.create( + title="系统待办", + visible=False, + trigger={}, + actions=[], + enabled=False, + notes="用于为所有存量数据创建的待办任务", + ) + task = await Task.create( + title="系统待办", + status='success', + ui_schema={}, + source_scenario=scenario, + notes="当系统有新的待办任务时关联动作", + ) + + +async def init_data(): + await init_db() + await init_task() + await init_cache() + await init_superuser() + await init_menus() + await init_apis() + await init_roles() + +async def tear_down(): + await redis_client.close() diff --git a/app/core/middlewares.py b/app/core/middlewares.py new file mode 100644 index 0000000..e4f2290 --- /dev/null +++ b/app/core/middlewares.py @@ -0,0 +1,192 @@ +import json +import re +from datetime import datetime +from typing import Any, AsyncGenerator + +from fastapi import FastAPI +from fastapi.responses import Response +from fastapi.routing import APIRoute +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.types import ASGIApp, Receive, Scope, Send + +from app.core.dependency import AuthControl +from app.models.admin import AuditLog, User + +from .bgtask import BgTasks + + +class SimpleBaseMiddleware: + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + request = Request(scope, receive=receive) + + response = await self.before_request(request) or self.app + await response(request.scope, request.receive, send) + await self.after_request(request) + + async def before_request(self, request: Request): + return self.app + + async def after_request(self, request: Request): + return None + + +class BackGroundTaskMiddleware(SimpleBaseMiddleware): + async def before_request(self, request): + await BgTasks.init_bg_tasks_obj() + + async def after_request(self, request): + await BgTasks.execute_tasks() + + +class HttpAuditLogMiddleware(BaseHTTPMiddleware): + def __init__(self, app, methods: list[str], exclude_paths: list[str]): + super().__init__(app) + self.methods = methods + self.exclude_paths = exclude_paths + self.audit_log_paths = ["/api/v1/auditlog/list"] + self.max_body_size = 1024 * 1024 # 1MB 响应体大小限制 + + async def get_request_args(self, request: Request) -> dict: + args = {} + # 获取查询参数 + for key, value in request.query_params.items(): + args[key] = value + + # 判断是否为文件上传请求 + content_type = request.headers.get("content-type", "").lower() + is_upload = ( + request.method in {"POST", "PUT"} # 上传通常用 POST/PUT + and ( + "multipart/form-data" in content_type # 标准文件上传 + or "application/octet-stream" in content_type # 二进制流上传(较少见) + ) + ) + + if is_upload: + return {} + + # 获取请求体 + if request.method in ["POST", "PUT", "PATCH"]: + try: + body = await request.json() + args.update(body) + except json.JSONDecodeError: + try: + body = await request.form() + args.update(body) + except Exception: + pass + + return args + + async def get_response_body(self, request: Request, response: Response) -> Any: + content_type = response.headers.get("content-type", "").lower() + if not content_type.startswith("application/json"): + return {"msg": "Non-JSON response (e.g., file download), skipped for audit"} + + # 检查Content-Length + content_length = response.headers.get("content-length") + if content_length and int(content_length) > self.max_body_size: + return {"code": 0, "msg": "Response too large to log", "data": None} + + if hasattr(response, "body"): + body = response.body + else: + body_chunks = [] + async for chunk in response.body_iterator: + if not isinstance(chunk, bytes): + chunk = chunk.encode(response.charset) + body_chunks.append(chunk) + + response.body_iterator = self._async_iter(body_chunks) + body = b"".join(body_chunks) + + if any(request.url.path.startswith(path) for path in self.audit_log_paths): + try: + data = self.lenient_json(body) + # 只保留基本信息,去除详细的响应内容 + if isinstance(data, dict): + data.pop("response_body", None) + if "data" in data and isinstance(data["data"], list): + for item in data["data"]: + item.pop("response_body", None) + return data + except Exception: + return None + + return self.lenient_json(body) + + def lenient_json(self, v: Any) -> Any: + if isinstance(v, (str, bytes)): + try: + return json.loads(v) + except (ValueError, TypeError): + pass + return v + + async def _async_iter(self, items: list[bytes]) -> AsyncGenerator[bytes, None]: + for item in items: + yield item + + async def get_request_log(self, request: Request, response: Response) -> dict: + """ + 根据request和response对象获取对应的日志记录数据 + """ + data: dict = {"path": request.url.path, "status": response.status_code, "method": request.method} + # 路由信息 + app: FastAPI = request.app + for route in app.routes: + if ( + isinstance(route, APIRoute) + and route.path_regex.match(request.url.path) + and request.method in route.methods + ): + data["module"] = ",".join(route.tags) + data["summary"] = route.summary + # 获取用户信息 + try: + token = request.headers.get("token") + user_obj = None + if token: + user_obj: User = await AuthControl.is_authed(token) + data["user_id"] = user_obj.id if user_obj else 0 + data["username"] = user_obj.username if user_obj else "" + except Exception: + data["user_id"] = 0 + data["username"] = "" + return data + + async def before_request(self, request: Request): + request_args = await self.get_request_args(request) + request.state.request_args = request_args + + async def after_request(self, request: Request, response: Response, process_time: int): + if request.method in self.methods: + for path in self.exclude_paths: + if re.search(path, request.url.path, re.I) is not None: + return + data: dict = await self.get_request_log(request=request, response=response) + data["response_time"] = process_time + + data["request_args"] = request.state.request_args + data["response_body"] = await self.get_response_body(request, response) + await AuditLog.create(**data) + + return response + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + start_time: datetime = datetime.now() + await self.before_request(request) + response = await call_next(request) + end_time: datetime = datetime.now() + process_time = int((end_time.timestamp() - start_time.timestamp()) * 1000) + await self.after_request(request, response, process_time) + return response diff --git a/app/http_base.py b/app/http_base.py new file mode 100644 index 0000000..d102df2 --- /dev/null +++ b/app/http_base.py @@ -0,0 +1,66 @@ +import inspect +from collections import namedtuple +from datetime import datetime +from functools import wraps +from typing import Callable, TypeVar + +import pytz +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse + +from app.settings import settings + +__all__ = ['HttpCode', 'HttpResp', 'unified_resp'] + +RT = TypeVar('RT') # 返回类型 +HttpCode = namedtuple('HttpResp', ['code', 'msg']) + + +class HttpResp: + """HTTP响应结果 + """ + SUCCESS = HttpCode(200, '成功') + FAILED = HttpCode(300, '失败') + PARAMS_VALID_ERROR = HttpCode(310, '参数校验错误') + PARAMS_TYPE_ERROR = HttpCode(311, '参数类型错误') + REQUEST_METHOD_ERROR = HttpCode(312, '请求方法错误') + ASSERT_ARGUMENT_ERROR = HttpCode(313, '断言参数错误') + + LOGIN_ACCOUNT_ERROR = HttpCode(330, '登录账号或密码错误') + LOGIN_DISABLE_ERROR = HttpCode(331, '登录账号已被禁用了') + TOKEN_EMPTY = HttpCode(332, 'token参数为空') + TOKEN_INVALID = HttpCode(333, 'token参数无效') + + NO_PERMISSION = HttpCode(403, '无相关权限') + REQUEST_404_ERROR = HttpCode(404, '请求接口不存在') + + SYSTEM_ERROR = HttpCode(500, '系统错误') + SYSTEM_TIMEOUT_ERROR = HttpCode(504, '请求超时') + +# 时区 +timezone = pytz.timezone('Asia/Shanghai') + +def unified_resp(func: Callable[..., RT]) -> Callable[..., RT]: + """统一响应格式 + 接口正常返回时,统一响应结果格式 + """ + + @wraps(func) + async def wrapper(*args, **kwargs) -> RT: + if inspect.iscoroutinefunction(func): + resp = await func(*args, **kwargs) or [] + else: + resp = func(*args, **kwargs) or [] + return JSONResponse( + content=jsonable_encoder( + # 正常请求响应 + {'code': HttpResp.SUCCESS.code, 'msg': HttpResp.SUCCESS.msg, 'data': resp}, + by_alias=False, + # 自定义日期时间格式编码器 + custom_encoder={ + datetime: lambda dt: dt.replace(tzinfo=pytz.utc).astimezone(timezone) + .strftime(settings.DATETIME_FORMAT)}), + media_type='application/json;charset=utf-8' + ) + + return wrapper diff --git a/app/log/__init__.py b/app/log/__init__.py new file mode 100644 index 0000000..968bf67 --- /dev/null +++ b/app/log/__init__.py @@ -0,0 +1 @@ +from .log import logger as logger diff --git a/app/log/log.py b/app/log/log.py new file mode 100644 index 0000000..d7c9f2a --- /dev/null +++ b/app/log/log.py @@ -0,0 +1,105 @@ +from app.settings import settings +import logging +import logging.handlers +from pathlib import Path + +class Loggin: + def __init__(self) -> None: + debug = settings.DEBUG + if debug: + self.level = "DEBUG" + else: + self.level = "INFO" + + def setup_logger(self): + """根据settings配置设置日志系统,控制台输出支持颜色""" + # 获取日志配置 + log_level = self.level + log_format = settings.LOG_FORMAT + + # 获取文件日志配置 + file_logging_enabled = True + log_file_path = '' + + # 创建日志目录 + if file_logging_enabled: + log_file_path = Path(settings.LOGS_ROOT) / "app.log" + log_file_path.parent.mkdir(exist_ok=True, parents=True) + + # 构建处理器列表 + handlers = [] + + # 控制台处理器(支持彩色输出) + console_enabled = self.level == "DEBUG" + if console_enabled: + print('添加控制台日志打印') + import colorlog # 新增:导入colorlog库 + + # 定义彩色日志级别对应的颜色 + log_colors = { + 'DEBUG': 'white', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'bold_red', + } + + # 创建彩色格式化器 + console_formatter = colorlog.ColoredFormatter( + # 彩色格式中需要使用%(log_color)s作为颜色标记 + "%(log_color)s" + log_format, + datefmt=settings.DATETIME_FORMAT, + log_colors=log_colors, + reset=True, + style='%' + ) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(console_formatter) + handlers.append(console_handler) + + # 文件处理器(如果启用) + if file_logging_enabled: + # 获取文件大小限制和备份数量 + max_bytes = settings.LOG_FILE_MAX_SIZE + backup_count = settings.LOG_FILE_BACKUP_COUNT + + # 文件日志使用普通格式化器(不添加颜色代码) + file_formatter = logging.Formatter( + log_format, + datefmt=settings.DATETIME_FORMAT + ) + + file_handler = logging.handlers.RotatingFileHandler( + log_file_path, + maxBytes=max_bytes, + backupCount=backup_count, + encoding='utf-8' + ) + file_handler.setFormatter(file_formatter) + handlers.append(file_handler) + + # 配置根日志记录器 + logging.basicConfig( + level=getattr(logging, log_level.upper(), logging.INFO), + format=log_format, + handlers=handlers, + datefmt=settings.DATETIME_FORMAT + ) + + # 设置第三方库的日志级别,减少噪音 + logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("selenium").setLevel(logging.WARNING) + logging.getLogger("playwright").setLevel(logging.WARNING) + logging.getLogger("tortoise").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) + + # 记录日志系统初始化 + logger = logging.getLogger(__name__) + logger.info(f"Logging configured - Level: {log_level}, File: {log_file_path if file_logging_enabled else 'Disabled'}") + return logger + + + +loggin = Loggin() +logger = loggin.setup_logger() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..ff7b280 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +# 新增model需要在这里导入 +from .admin import * +from .automation import * +from .weixin import * +from .msg import * diff --git a/app/models/admin.py b/app/models/admin.py new file mode 100644 index 0000000..2f40ab6 --- /dev/null +++ b/app/models/admin.py @@ -0,0 +1,297 @@ +from tortoise import fields +from tortoise import fields as Fields + +from app.schemas.menus import MenuType + +from .base import BaseModel, TimestampMixin +from .enums import MethodType + + +class User(BaseModel, TimestampMixin): + username = fields.CharField(max_length=20, unique=True, description="用户名称", index=True) + alias = fields.CharField(max_length=30, null=True, description="姓名", index=True) + email = fields.CharField(max_length=255, unique=True, description="邮箱", index=True) + phone = fields.CharField(max_length=20, null=True, description="电话", index=True) + password = fields.CharField(max_length=128, null=True, description="密码") + is_active = fields.BooleanField(default=True, description="是否激活", index=True) + is_superuser = fields.BooleanField(default=False, description="是否为超级管理员", index=True) + last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True) + roles = fields.ManyToManyField("models.Role", related_name="user_roles") + dept_id = fields.IntField(null=True, description="部门ID", index=True) + + class Meta: + table = "user" + + +class Role(BaseModel, TimestampMixin): + name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True) + desc = fields.CharField(max_length=500, null=True, description="角色描述") + menus = fields.ManyToManyField("models.Menu", related_name="role_menus") + apis = fields.ManyToManyField("models.Api", related_name="role_apis") + + class Meta: + table = "role" + + +class Api(BaseModel, TimestampMixin): + path = fields.CharField(max_length=100, description="API路径", index=True) + method = fields.CharEnumField(MethodType, description="请求方法", index=True) + summary = fields.CharField(max_length=500, description="请求简介", index=True) + tags = fields.CharField(max_length=100, description="API标签", index=True) + + class Meta: + table = "api" + + +class Menu(BaseModel, TimestampMixin): + name = fields.CharField(max_length=20, description="菜单名称", index=True) + remark = fields.JSONField(null=True, description="保留字段") + menu_type = fields.CharEnumField(MenuType, null=True, description="菜单类型") + icon = fields.CharField(max_length=100, null=True, description="菜单图标") + path = fields.CharField(max_length=100, description="菜单路径", index=True) + order = fields.IntField(default=0, description="排序", index=True) + parent_id = fields.IntField(default=0, description="父菜单ID", index=True) + is_hidden = fields.BooleanField(default=False, description="是否隐藏") + component = fields.CharField(max_length=100, description="组件") + keepalive = fields.BooleanField(default=True, description="存活") + redirect = fields.CharField(max_length=100, null=True, description="重定向") + + class Meta: + table = "menu" + + +class Dept(BaseModel, TimestampMixin): + name = fields.CharField(max_length=20, unique=True, description="部门名称", index=True) + desc = fields.CharField(max_length=500, null=True, description="备注") + is_deleted = fields.BooleanField(default=False, description="软删除标记", index=True) + order = fields.IntField(default=0, description="排序", index=True) + parent_id = fields.IntField(default=0, max_length=10, description="父部门ID", index=True) + + class Meta: + table = "dept" + + +class DeptClosure(BaseModel, TimestampMixin): + ancestor = fields.IntField(description="父代", index=True) + descendant = fields.IntField(description="子代", index=True) + level = fields.IntField(default=0, description="深度", index=True) + + +class AuditLog(BaseModel, TimestampMixin): + user_id = fields.IntField(description="用户ID", index=True) + username = fields.CharField(max_length=64, default="", description="用户名称", index=True) + module = fields.CharField(max_length=64, default="", description="功能模块", index=True) + summary = fields.CharField(max_length=128, default="", description="请求描述", index=True) + method = fields.CharField(max_length=10, default="", description="请求方法", index=True) + path = fields.CharField(max_length=255, default="", description="请求路径", index=True) + status = fields.IntField(default=-1, description="状态码", index=True) + response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True) + request_args = fields.JSONField(null=True, description="请求参数") + response_body = fields.JSONField(null=True, description="返回数据") + + +class Codegen(BaseModel, TimestampMixin): + """ + { + "name": "user", + "author": "Ly997", + "description": "系统用户管理", + "version": "1.0.0", + "fields": [ + { + "name": "id", + "type": "Long", + "primary_key": true, + "description": "用户ID", + "required": true, + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + }, + { + "name": "username", + "type": "String", + "length": 50, + "description": "用户名", + "required": true, + "unique": true, + "validations": [ + { + "type": "min_length", + "value": 4 + }, + { + "type": "max_length", + "value": 20 + } + ], + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + }, + { + "name": "email", + "type": "String", + "description": "电子邮箱", + "required": true, + "validations": [ + { + "type": "email" + } + ], + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + }, + { + "name": "password_hash", + "type": "String", + "description": "密码哈希", + "required": true, + "secret": true, + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + }, + { + "name": "is_active", + "type": "Boolean", + "description": "是否激活", + "default": true, + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + }, + { + "name": "created_at", + "type": "DateTime", + "description": "创建时间", + "auto_now_add": true, + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + }, + { + "name": "updated_at", + "type": "DateTime", + "description": "更新时间", + "auto_now": true, + "editable": true, + "listable": true, + "detailable": true, + "sortable": true, + "filterable": true, + "filter_operator": "equal", + "display_type": "text" + } + ], + "relations": [ + { + "name": "roles", + "type": "many-to-many", + "target": "role", + "through": "user_roles", + "description": "用户角色关联" + }, + { + "name": "posts", + "type": "one-to-many", + "target": "post", + "description": "用户发表的文章" + } + ], + "api": { + "operations": [ + "create", + "read", + "update", + "delete", + "list" + ], + "base_path": "/users", + "auth_required": true, + "permissions": { + "create": [ + "admin", + "manager" + ], + "delete": [ + "admin" + ] + } + }, + "menu": { + "label": "用户管理", + "icon": "user", + "order": 10, + "submenu": [ + { + "label": "用户列表", + "path": "/users", + "icon": "list" + }, + { + "label": "角色管理", + "path": "/roles" + } + ] + } +} """ + name = Fields.CharField(max_length=64, description="名称", index=True) + author = Fields.CharField(max_length=64, description="作者", index=True) + description = Fields.CharField(max_length=500, description="描述") + version = Fields.CharField(max_length=64, description="版本", index=True) + fields = Fields.JSONField(description="字段") + relations = Fields.JSONField(description="关系") + api = Fields.JSONField(description="API", null=True) + menu = Fields.JSONField(description="菜单", null=True) + + class Meta: + table = "codegen" + + async def dumps(self, output_type='pydantic'): + from app.schemas.codegen import BaseCodegen + + obj_dict = await self.to_dict() + # print('type(obj_dict["fields"])', type(obj_dict["fields"])) + # print('type(obj_dict["relations"])', type(obj_dict["relations"])) + # print('type(obj_dict["api"])', type(obj_dict["api"])) + # obj_dict['fields'] = json.loads(obj_dict['fields']) + # obj_dict['relations'] = json.loads(obj_dict['relations']) + # obj_dict['api'] = json.loads(obj_dict['api']) + # obj_dict['menu'] = json.loads(obj_dict['menu']) + + if output_type == 'pydantic': return BaseCodegen(**obj_dict) + else : return obj_dict + + +class Datasource(BaseModel, TimestampMixin): + tableName = Fields.CharField(max_length=32, description="名称", index=True) + tableComment = Fields.CharField(max_length=64, description="注释", index=True) + + class Meta: + table = "datasource" \ No newline at end of file diff --git a/app/models/automation.py b/app/models/automation.py new file mode 100644 index 0000000..f1343e3 --- /dev/null +++ b/app/models/automation.py @@ -0,0 +1,119 @@ +from tortoise import fields + +from .base import BaseModel, TimestampMixin +from .enums import TaskStatus, TaskType, ActionType, TaskStatus, ScenarioScope + + +class Scenario(BaseModel, TimestampMixin): + title = fields.CharField(max_length=255, description="场景标题", index=True) + trigger = fields.JSONField(description="触发条件(事件/时间)") + actions = fields.JSONField(default=list, description="执行动作模板列表") + visible = fields.BooleanField(default=True, description="是否可见", index=True) + notes = fields.TextField(null=True, description="备注") + is_global = fields.BooleanField(default=True, description="是否全局场景", index=True) + owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID(非全局时有效)", index=True) + enabled = fields.BooleanField(default=True, description="是否启用", index=True) + scope = fields.CharEnumField(ScenarioScope, default=ScenarioScope.ALL, description="作用角色范围", index=True) + due_days = fields.IntField(null=True, description="截止时间(天)") + + class Meta: + table = "automation_scenario" + indexes = [ + ["is_global", "enabled"], + ["owner_user_id", "enabled"], + ] + +""" +CREATE TABLE automation_scenario_trigger_index ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + scenario_id BIGINT NOT NULL, + event_name VARCHAR(64) NOT NULL COMMENT '监听的事件名,如 group_chat_updated', + enabled TINYINT(1) NOT NULL DEFAULT 1, + is_global TINYINT(1) NOT NULL DEFAULT 1, + owner_user_id VARCHAR(64) DEFAULT NULL, + scope VARCHAR(8) NOT NULL DEFAULT 'all', + + PRIMARY KEY (id), + KEY idx_event_enabled (event_name, enabled), + KEY idx_scenario_id (scenario_id), + CONSTRAINT fk_scenario_def FOREIGN KEY (scenario_id) REFERENCES automation_scenario(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +""" +class ScenarioTriggerIndex(BaseModel, TimestampMixin): + scenario_id = fields.BigIntField(description="场景ID") + type = fields.CharEnumField(TaskType, default=TaskType.EVENT, description="事件类型", index=True) + event_name = fields.CharField(max_length=64, description="监听的事件名,如 group_chat_updated", index=True) + enabled = fields.BooleanField(default=True, description="是否启用", index=True) + is_global = fields.BooleanField(default=True, description="是否全局场景", index=True) + owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID(非全局时有效)", index=True) + scope = fields.CharEnumField(ScenarioScope, default=ScenarioScope.ALL, description="作用角色范围") + + class Meta: + table = "automation_scenario_trigger_index" + + +class Task(BaseModel, TimestampMixin): + title = fields.CharField(max_length=255, description="待办标题", index=True) + event_data = fields.JSONField(default=dict, description="事件数据") + notes = fields.TextField(null=True, description="备注") + reason = fields.TextField(null=True, description="触发原因") + + completed_at = fields.DatetimeField(null=True, description="完成时间") + + # 关联字段 + source_scenario = fields.ForeignKeyField( + "models.Scenario", + related_name="generated_tasks", + null=True, + on_delete=fields.SET_NULL, + description="来源场景" + ) + + related_order_id = fields.CharField(max_length=64, null=True, description="关联订单ID", index=True) + owner_user_id = fields.CharField(max_length=64, null=True, description="归属用户ID", index=True) + + # 分配与状态 + assignee_user_id = fields.CharField(max_length=64, null=True, description="指派人用户ID", index=True) + assignee_username = fields.CharField(max_length=64, null=True, description="指派人用户名", index=True) + status = fields.CharEnumField(TaskStatus, default=TaskStatus.PENDING, description="任务状态", index=True) + + # 自动化控制 + due_at = fields.DatetimeField(null=True, description="截止时间") + auto_closeable = fields.BooleanField(default=True, description="是否允许自动消除") + closed_by = fields.CharField(max_length=32, null=True, description="关闭方式: manual/auto/expired", index=True) + + class Meta: + table = "automation_task" + indexes = [ + ["assignee_user_id", "status"], + ] + + +class Action(BaseModel, TimestampMixin): + type = fields.CharEnumField(ActionType, null=True, description="操作类型", index=True) + detail = fields.JSONField(default={}, description="操作详情") + done = fields.BooleanField(default=False, description="是否已完成", index=True) + done_at = fields.DatetimeField(null=True, description="完成时间") + result = fields.JSONField(default={}, description="操作结果") + userid = fields.CharField(max_length=64, null=True, description="操作用户ID", index=True) + username = fields.CharField(max_length=64, null=True, description="操作用户名称", index=True) + notes = fields.TextField(null=True, description="操作备注") + + # 硬外键:必须属于一个 Task + task = fields.ForeignKeyField( + "models.Task", + related_name="actions", + null=True, # ← 允许为空 + on_delete=fields.SET_NULL, # ← 注意:CASCADE 不能和 null=True 同时用于 SET_NULL + description="所属任务" + ) + + # 可选:记录来自哪个动作模板(场景中的定义) + action_template_id = fields.CharField(max_length=64, null=True, description="操作模板ID", index=True) + + class Meta: + table = "automation_action" + indexes = [ + ["task_id", "done"], # 优化查询未完成动作 + ["type", "done"], + ] \ No newline at end of file diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..33e0b51 --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,68 @@ +import asyncio +from datetime import datetime + +from tortoise import fields, models + +from app.settings import settings + + +class BaseModel(models.Model): + id = fields.BigIntField(pk=True, index=True) + + async def to_dict(self, m2m: bool = False, exclude_fields: list[str] | None = None): + if exclude_fields is None: + exclude_fields = [] + + d = {} + for field in self._meta.db_fields: + if field not in exclude_fields: + value = getattr(self, field) + if isinstance(value, datetime): + value = value.strftime(settings.DATETIME_FORMAT) + d[field] = value + + if m2m: + tasks = [ + self.__fetch_m2m_field(field, exclude_fields) + for field in self._meta.m2m_fields + if field not in exclude_fields + ] + results = await asyncio.gather(*tasks) + for field, values in results: + d[field] = values + + return d + + async def __fetch_m2m_field(self, field, exclude_fields): + values = await getattr(self, field).all().values() + formatted_values = [] + + for value in values: + formatted_value = {} + for k, v in value.items(): + if k not in exclude_fields: + if isinstance(v, datetime): + formatted_value[k] = v.strftime(settings.DATETIME_FORMAT) + else: + formatted_value[k] = v + formatted_values.append(formatted_value) + + return field, formatted_values + + @classmethod + def get_all_keys(cls, exclude_fields: list[str] | None = None): + if exclude_fields is None: + exclude_fields = [] + return [field for field in cls._meta.fields_map.keys() if field not in exclude_fields] + + class Meta: + abstract = True + + +class UUIDModel: + uuid = fields.UUIDField(unique=True, pk=False, index=True) + + +class TimestampMixin: + created_at = fields.DatetimeField(auto_now_add=True, index=True) + updated_at = fields.DatetimeField(auto_now=True, index=True) diff --git a/app/models/enums.py b/app/models/enums.py new file mode 100644 index 0000000..962437e --- /dev/null +++ b/app/models/enums.py @@ -0,0 +1,64 @@ +from enum import Enum, StrEnum + + +class EnumBase(Enum): + @classmethod + def get_member_values(cls): + return [item.value for item in cls._member_map_.values()] + + @classmethod + def get_member_names(cls): + return [name for name in cls._member_names_] + + +class MethodType(StrEnum): + GET = "GET" + POST = "POST" + PUT = "PUT" + DELETE = "DELETE" + PATCH = "PATCH" + +class TaskStatus(Enum): + PENDING = "pending" + RUNNING = "running" + SUCCESS = "success" + FAILED = "failed" + CANCELLED = "cancelled" + EXPIRED = "expired" + +class TaskType(Enum): + DECODE_ORDER = "decode_order" + EVENT = "event" + TIMER = "timer" + +class ScenarioScope(str, Enum): + ALL = "all" + PERSONAL = "personal" + KEFU = "kefu" # 接待员 + FOLLOW = "follow" # 跟单员 + DESIGNER = "designer" # 设计师 + +class RoleType(StrEnum): + BUYER = "buyer" + DESIGER = "desiger" + FOLLOWER = "follower" + ADMIN = "admin" + KEFU = "taobao_kefu" + +class MsgType(StrEnum): + NEW_ORDER = "new_order" + COMMENT = "comment" + SYSTEM = "system" + NOTIFY = "notify" + +class ActionType(StrEnum): + CREATE_GROUP = "create_group" + VIEW_ERP_LOG = "view_erp_log" + BIND_USER = "bind_user" + BIND_ORDER = "bind_order" + WRITE_REMARK = "write_remark" + SEND_WECHAT_NOTIFY = "send_notify" + # 清理群聊 + CLEAN_GROUP = "clean_group" + # 设置群管理员 + SET_GROUP_ADMIN = "set_group_admin" diff --git a/app/models/msg.py b/app/models/msg.py new file mode 100644 index 0000000..953823f --- /dev/null +++ b/app/models/msg.py @@ -0,0 +1,72 @@ +from tortoise import fields + +from .base import BaseModel, TimestampMixin +from .enums import MsgType, ActionType + +class Msg(BaseModel, TimestampMixin): + hash_id = fields.CharField(max_length=64, null=True, description="消息哈希ID", index=True, unique=True) + title = fields.CharField(max_length=64, null=True, description="消息标题", index=True) + content = fields.TextField(null=True, description="消息内容") + detail = fields.JSONField(default={}, description="消息详情") + is_send = fields.BooleanField(default=False, description="是否已发送", index=True) + send_at = fields.DatetimeField(null=True, description="发送时间") + is_read = fields.BooleanField(default=False, description="是否已读", index=True) + read_at = fields.DatetimeField(null=True, description="已读时间") + is_delete = fields.BooleanField(default=False, description="是否已删除", index=True) + delete_at = fields.DatetimeField(null=True, description="删除时间") + type = fields.CharEnumField(MsgType, null=True, description="消息类型", index=True) + owner_id = fields.CharField(max_length=64, null=True, description="消息所有者ID", index=True) + owner_name = fields.CharField(max_length=64, null=True, description="消息所有者名称", index=True) + + class Meta: + table = "system_msg" + + +class Follow(BaseModel, TimestampMixin): + order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True) + order_title = fields.TextField(null=True, description="订单标题") + order_pic = fields.CharField(max_length=256, null=True, description="订单图片") + pay_time = fields.DatetimeField(null=True, description="支付时间", index=True) + pay_price = fields.FloatField(null=True, description="支付金额", index=True) + remark = fields.TextField(null=True, description="erp额外备注") + + customer_id = fields.CharField(max_length=64, null=True, description="客户ID", index=True) + customer_name = fields.CharField(max_length=64, null=True, description="客户名称", index=True) + customer_taobao_id = fields.CharField(max_length=64, null=True, description="客户淘宝ID", index=True) + + staff_id = fields.CharField(max_length=64, null=True, description="企微员工ID", index=True) + staff_name = fields.CharField(max_length=64, null=True, description="企微员工名称", index=True) + staff_time = fields.DatetimeField(null=True, description="添加时间", index=True) + + designer_id = fields.CharField(max_length=64, null=True, description="设计师ID", index=True) + designer_name = fields.CharField(max_length=64, null=True, description="设计师名称", index=True) + + shop_name = fields.CharField(max_length=64, null=True, description="店铺名称", index=True) + kefu_id = fields.CharField(max_length=64, null=True, description="淘宝客服ID", index=True) + kefu_name = fields.CharField(max_length=64, null=True, description="淘宝客服名称", index=True) + + is_delete = fields.BooleanField(default=False, description="是否已删除", index=True) + + feishu_record_id = fields.CharField(max_length=64, null=True, description="飞书记录ID", index=True) + is_add_to_feishu = fields.BooleanField(default=False, description="是否已添加到飞书", index=True) + is_update_to_feishu = fields.BooleanField(default=False, description="是否已更新到飞书", index=True) + is_delete_from_feishu = fields.BooleanField(default=False, description="是否已从飞书删除", index=True) + + class Meta: + table = "system_follow" + + def to_feishu_record(self, fields: dict=None): + fields = fields or {} + return { + "record_id": self.feishu_record_id, + "fields": { + "付款时间": self.pay_time.strftime("%Y-%m-%d %H:%M:%S"), + "店铺名": self.shop_name, + "成交客服": self.kefu_name, + "旺旺id": self.customer_name, + "添加企微客服": self.staff_name, + "订单编号": self.order_id, + "设计师": self.designer_name, + **fields + } + } diff --git a/app/models/weixin.py b/app/models/weixin.py new file mode 100644 index 0000000..82a6b9e --- /dev/null +++ b/app/models/weixin.py @@ -0,0 +1,260 @@ +from tortoise import fields + +from .base import BaseModel, TimestampMixin +from .enums import RoleType + +class WeixinUser(BaseModel, TimestampMixin): + # 自动成为订单关联方 + auto_relate = fields.BooleanField(default=False, description="是否自动成为订单关联方") + userid = fields.CharField(max_length=64, description="用户微信id", index=True) # acctid + corp_id = fields.CharField(max_length=64, description="企业微信id", default="1970325009092879", index=True) + vid = fields.CharField(max_length=64, description="用户微信id", default='', index=True) + wx_id_hash = fields.CharField(max_length=64, null=True, description="微信id hash") + is_quit = fields.BooleanField(default=False, description="是否离职") + has_external_user_permit = fields.BooleanField(default=False, description="是否具有对外联系权限") + wx_nick_name = fields.CharField(max_length=64, null=True, description="微信昵称") + account = fields.CharField(max_length=64, null=True, description="账号") + + position = fields.CharField(max_length=64, null=True, description="职位") + role = fields.CharEnumField(RoleType, null=True, description="用户角色", index=True) + depart_ids = fields.JSONField(default=[], description="部门ID列表") + + username = fields.CharField(max_length=64, null=True, description="用户微信名称", index=True) + english_name = fields.CharField(max_length=64, null=True, description="英文名") + name = fields.CharField(max_length=64, null=True, description="用户名称", index=True) + realname = fields.CharField(max_length=64, null=True, description="用户真实名字", index=True) + alias = fields.CharField(max_length=30, null=True, description="别名") + avatar = fields.CharField(max_length=512, null=True, description="头像") + mobile = fields.CharField(max_length=11, null=True, description="手机号") + email = fields.CharField(max_length=64, null=True, description="邮箱") + gender = fields.IntField(null=True, description="性别") + + erp_id = fields.IntField(null=True, description="ERP中的用户ID", index=True) + erp_name = fields.CharField(max_length=64, null=True, description="ERP中的用户名称") + crm_id = fields.IntField(null=True, description="星云有客中的用户ID", index=True) + crm_name = fields.CharField(max_length=64, null=True, description="星云有客中的用户名称") + + def to_dict(self, exclude_fields=None, include_sensitive=False): + """ + 自定义字典转换行为 + + Args: + exclude_fields: 要排除的字段列表 + include_sensitive: 是否包含敏感信息(如手机号、邮箱等) + + Returns: + dict: 转换后的字典 + """ + exclude_fields = exclude_fields or ['staff_memberships'] + data = {} + + # 获取所有字段名 + model_fields = self._meta.fields_map.keys() + + for field_name in model_fields: + if field_name in exclude_fields: + continue + + # 如果不包含敏感信息,则跳过敏感字段 + if not include_sensitive and field_name in ['mobile', 'email']: + continue + + value = getattr(self, field_name) + + # 处理枚举字段 + if field_name == 'role' and value is not None: + data[field_name] = value.value if hasattr(value, 'value') else value + # 处理布尔字段的默认值显示 + elif field_name == 'is_quit': + data[field_name] = bool(value) if value is not None else False + elif field_name == 'has_external_user_permit': + data[field_name] = bool(value) if value is not None else False + else: + data[field_name] = value + + return data + + def to_public_dict(self): + """ + 返回公开信息的字典(不包含敏感信息) + """ + sensitive_fields = ['mobile', 'email'] + exclude_fields = ['wx_id_hash'] # 可能还有其他不想暴露的字段 + all_exclude = sensitive_fields + exclude_fields + + return self.to_dict(exclude_fields=all_exclude, include_sensitive=False) + + def to_detail_dict(self): + """ + 返回详细信息的字典(包含所有信息) + """ + return self.to_dict(include_sensitive=True) + + def to_safe_dict(self, visible_fields=None): + """ + 返回指定字段的安全字典 + + Args: + visible_fields: 指定要包含的字段列表,如果为None则使用默认安全字段 + """ + if visible_fields is None: + # 默认的安全字段(不包含敏感信息) + visible_fields = [ + 'userid', 'vid', 'wx_nick_name', 'username', 'name', + 'realname', 'alias', 'avatar', 'position', 'role', + 'depart_ids', 'erpid', 'crmid', 'created_at', 'updated_at' + ] + + data = {} + for field_name in visible_fields: + if hasattr(self, field_name): + value = getattr(self, field_name) + if field_name == 'role' and value is not None: + data[field_name] = value.value if hasattr(value, 'value') else value + else: + data[field_name] = value + + return data + + class Meta: + table = "weixin_user" + +class WeixinGroupChat(BaseModel, TimestampMixin): + chat_id = fields.CharField(max_length=64, description="群聊ID", index=True) + xingyun_chat_id = fields.IntField(null=True, description="星云中的群聊ID", index=True) + name = fields.CharField(max_length=64, null=True, description="群聊名称") + create_time = fields.IntField(description="创建时间") + admin_list = fields.JSONField(default=[], description="所有管理员") + owner = fields.CharField(max_length=64, null=True, description="群主") + member_version = fields.CharField(max_length=64, null=True, description="群成员版本") + external_user_count = fields.IntField(null=True, description="外部群成员数量") + external_member_list = fields.JSONField(default=[], description="外部群成员列表") + internal_member_count = fields.IntField(null=True, description="内部群成员数量") + internal_member_list = fields.JSONField(default=[], description="内部群成员列表") + avatars = fields.JSONField(default=[], description="群成员头像") + + class Meta: + table = "weixin_group_chat" + +class WeixinCustomer(BaseModel, TimestampMixin): + order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True) + shop_name = fields.CharField(max_length=64, null=True, description="店铺名称") + weixin_id = fields.CharField(max_length=64, null=True, description="微信体系中的id", index=True) + weixin_name = fields.CharField(max_length=64, null=True, description="微信体系中的用户名") + weixin_unionid = fields.CharField(max_length=64, null=True, description="微信体系中的unionid", index=True) + weixin_avatar = fields.CharField(max_length=512, null=True, description="微信头像") + xingyun_id = fields.IntField(null=True, description="星云有客中的用户ID", index=True) + xingyun_sex = fields.IntField(null=True, description="星云有客中的用户性别", index=True) + xingyun_name = fields.CharField(max_length=64, null=True, description="星云有客中的用户名") + xingyun_avatar = fields.CharField(max_length=512, null=True, description="星云有客头像") + xingyun_tags = fields.JSONField(default=[], null=True, description="星云用户标签") + xingyun_external_userid = fields.CharField(max_length=64, null=True, description="星云有客中的外部用户ID") + xingyun_sync = fields.BooleanField(default=False, description="是否已同步到星云", index=True) + # erp_id = fields.IntField(null=True, description="ERP中的客户ID", index=True) + # erp_name = fields.CharField(max_length=64, null=True, description="ERP中的客户名称") + # erp_avatar = fields.CharField(max_length=512, null=True, description="ERP头像") + taobao_id = fields.CharField(max_length=64, null=True, description="淘宝中的用户ID", index=True) + taobao_name = fields.CharField(max_length=64, null=True, description="淘宝中的用户名") + taobao_avatar = fields.CharField(max_length=512, null=True, description="淘宝头像") + need_confirm = fields.BooleanField(null=True, description="是否需要确认") + extra = fields.JSONField(default={}, description="额外信息") + + # 使用 through 指向自定义中间模型 + groups = fields.ManyToManyField( + "models.WeixinGroupChat", + through="customer_group", # 必须是中间模型的 table 名(或模型名) + related_name="customers" + ) + + @classmethod + def orm_format(cls, data: dict): + # 将不在字段中的字段保存到extra字段中 + all_fields = set(cls._meta.fields_map.keys()) + + extra = {} + data_in = {"extra": extra} + for k, v in data.copy().items(): + if k not in all_fields: + extra[k] = v + else: + data_in[k] = v + + id = data_in.pop('id', None) + if id: data_in['platform_id'] = id + return data_in + + def to_dict(self, *args, **kwargs): + # 类似于WeixinUser参数 + return { + "id": self.id, + "userid": self.weixin_id, + "name": self.taobao_name, + "avatar": self.xingyun_avatar, + "weixin_name": self.weixin_name, + "role": 'buyer', + "is_customer": True, + } + # data = await super().to_dict(*args, **kwargs) + # data['id'] = data.pop('platform_id', None) + # data.update(data.pop('extra', {})) + # return data + + def dump_dict(self): + all_fields = set(self._meta.fields_map.keys()) + data = {} + for k, v in self.__dict__.items(): + if k in all_fields: + data[k] = v + return data + + @classmethod + def create_bind(cls, data: dict): + # 将不在字段中的字段保存到extra字段中 + data_in = cls.orm_format(data) + return cls(**data_in) + + class Meta: + table = "weixin_customer" + +# === 中间模型:CustomerGroup === +class CustomerGroup(BaseModel, TimestampMixin): + """顾客与群聊的关联关系(带额外信息)""" + + # 外键指向顾客 + customer = fields.ForeignKeyField( + "models.WeixinCustomer", + related_name="group_memberships" # 从 Customer 反向查关系 + ) + + # 外键指向群聊 + group = fields.ForeignKeyField( + "models.WeixinGroupChat", + related_name="customer_memberships" # 从 Group 反向查关系 + ) + + # 外键指向群聊 + staff = fields.ForeignKeyField( + "models.WeixinUser", + related_name="staff_memberships" # 从 User 反向查关系 + ) + + # 额外字段 + join_time = fields.DatetimeField(auto_now_add=True, description="入群时间", index=True) + role = fields.CharField( + max_length=32, + default="member", + description="群内角色:member / admin / owner", + index=True + ) + + staff_userid = fields.CharField(max_length=64, null=True, description="客服ID", index=True) + customer_userid = fields.CharField(max_length=64, null=True, description="顾客的ID", index=True) + group_chatid = fields.CharField(max_length=64, null=True, description="群聊ID", index=True) + order_id = fields.CharField(max_length=64, null=True, description="订单ID", index=True) + shop_name = fields.CharField(max_length=64, null=True, description="店铺名称", index=True) + remark = fields.CharField(max_length=255, null=True, description="备注") + + class Meta: + table = "customer_group" + # 确保同一个顾客不能重复加入同一个群 + unique_together = ("customer", "group") \ No newline at end of file diff --git a/app/resources/.gitignore b/app/resources/.gitignore new file mode 100644 index 0000000..ad414b4 --- /dev/null +++ b/app/resources/.gitignore @@ -0,0 +1,13 @@ + +train/ +# 忽略 models 目录下的所有文件和子目录 +models/* + +# 递归保留所有子目录中的 best_model 目录 +models/订单尺寸识别/* +!models/订单尺寸识别/ +!models/订单尺寸识别/best_model/ + +models/定制印刷品-订单描述/* +!models/定制印刷品-订单描述/ +!models/定制印刷品-订单描述/best_model/ diff --git a/app/resources/20260616154526_多淘印6.16_解析_横向排列_1.xlsx b/app/resources/20260616154526_多淘印6.16_解析_横向排列_1.xlsx new file mode 100644 index 0000000..5e0e20a Binary files /dev/null and b/app/resources/20260616154526_多淘印6.16_解析_横向排列_1.xlsx differ diff --git a/app/resources/20260623112828_1_解析_横向排列_1.xlsx b/app/resources/20260623112828_1_解析_横向排列_1.xlsx new file mode 100644 index 0000000..c5547d5 Binary files /dev/null and b/app/resources/20260623112828_1_解析_横向排列_1.xlsx differ diff --git a/app/resources/models/定制印刷品-订单描述/best_model/config.cfg b/app/resources/models/定制印刷品-订单描述/best_model/config.cfg new file mode 100644 index 0000000..28cff7e --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/config.cfg @@ -0,0 +1,135 @@ +[paths] +train = null +dev = null +vectors = null +init_tok2vec = null + +[system] +seed = 0 +gpu_allocator = null + +[nlp] +lang = "zh" +pipeline = ["ner"] +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null +batch_size = 1000 +vectors = {"@vectors":"spacy.Vectors.v1"} + +[nlp.tokenizer] +@tokenizers = "spacy.zh.ChineseTokenizer" +segmenter = "char" + +[components] + +[components.ner] +factory = "ner" +incorrect_spans_key = null +moves = null +scorer = {"@scorers":"spacy.ner_scorer.v1"} +update_with_oracle_cut_size = 100 + +[components.ner.model] +@architectures = "spacy.TransitionBasedParser.v2" +state_type = "ner" +extra_state_tokens = false +hidden_width = 64 +maxout_pieces = 2 +use_upper = true +nO = null + +[components.ner.model.tok2vec] +@architectures = "spacy.HashEmbedCNN.v2" +pretrained_vectors = null +width = 96 +depth = 4 +embed_size = 2000 +window_size = 1 +maxout_pieces = 3 +subword_features = true + +[corpora] + +[corpora.dev] +@readers = "spacy.Corpus.v1" +path = ${paths.dev} +gold_preproc = false +max_length = 0 +limit = 0 +augmenter = null + +[corpora.train] +@readers = "spacy.Corpus.v1" +path = ${paths.train} +gold_preproc = false +max_length = 0 +limit = 0 +augmenter = null + +[training] +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +accumulate_gradient = 1 +patience = 1600 +max_epochs = 0 +max_steps = 20000 +eval_frequency = 200 +frozen_components = [] +annotating_components = [] +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +before_to_disk = null +before_update = null + +[training.batcher] +@batchers = "spacy.batch_by_words.v1" +discard_oversize = false +tolerance = 0.2 +get_length = null + +[training.batcher.size] +@schedules = "compounding.v1" +start = 100 +stop = 1000 +compound = 1.001 +t = 0.0 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = false + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 1.0 +use_averages = false +eps = 0.00000001 +learn_rate = 0.001 + +[training.score_weights] +ents_f = 1.0 +ents_p = 0.0 +ents_r = 0.0 +ents_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] +pkuseg_model = null +pkuseg_user_dict = "default" \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/meta.json b/app/resources/models/定制印刷品-订单描述/best_model/meta.json new file mode 100644 index 0000000..79c80c2 --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/meta.json @@ -0,0 +1,41 @@ +{ + "lang":"zh", + "name":"pipeline", + "version":"0.0.0", + "spacy_version":">=3.8.7,<3.9.0", + "description":"", + "author":"", + "email":"", + "url":"", + "license":"", + "spacy_git_version":"4b65aa7", + "vectors":{ + "width":0, + "vectors":0, + "keys":0, + "name":null, + "mode":"default" + }, + "labels":{ + "ner":[ + "\u4ea7\u54c1", + "\u522e\u522e\u819c\u5c3a\u5bf8", + "\u52a0\u6025", + "\u5355\u53f7", + "\u5c3a\u5bf8", + "\u5de5\u827a", + "\u6570\u91cf", + "\u6750\u8d28", + "\u7528\u6237\u4fe1\u606f" + ] + }, + "pipeline":[ + "ner" + ], + "components":[ + "ner" + ], + "disabled":[ + + ] +} \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/ner/cfg b/app/resources/models/定制印刷品-订单描述/best_model/ner/cfg new file mode 100644 index 0000000..6cd11cf --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/ner/cfg @@ -0,0 +1,13 @@ +{ + "moves":null, + "update_with_oracle_cut_size":100, + "multitasks":[ + + ], + "min_action_freq":1, + "learn_tokens":false, + "beam_width":1, + "beam_density":0.0, + "beam_update_prob":0.0, + "incorrect_spans_key":null +} \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/ner/model b/app/resources/models/定制印刷品-订单描述/best_model/ner/model new file mode 100644 index 0000000..4d5d207 Binary files /dev/null and b/app/resources/models/定制印刷品-订单描述/best_model/ner/model differ diff --git a/app/resources/models/定制印刷品-订单描述/best_model/ner/moves b/app/resources/models/定制印刷品-订单描述/best_model/ner/moves new file mode 100644 index 0000000..3068416 --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/ner/moves @@ -0,0 +1 @@ +moves0{"0":{},"1":{"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"2":{"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"3":{"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"4":{"":1,"\u52a0\u6025":-1,"\u7528\u6237\u4fe1\u606f":-2,"\u5355\u53f7":-3,"\u6750\u8d28":-4,"\u522e\u522e\u819c\u5c3a\u5bf8":-5,"\u5de5\u827a":-6,"\u6570\u91cf":-7,"\u5c3a\u5bf8":-8,"\u4ea7\u54c1":-9},"5":{"":1}}cfgneg_key \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/tokenizer/cfg b/app/resources/models/定制印刷品-订单描述/best_model/tokenizer/cfg new file mode 100644 index 0000000..a3e8b3d --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/tokenizer/cfg @@ -0,0 +1,3 @@ +{ + "segmenter":"char" +} \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/vocab/key2row b/app/resources/models/定制印刷品-订单描述/best_model/vocab/key2row new file mode 100644 index 0000000..5416677 --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/vocab/key2row @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/vocab/lookups.bin b/app/resources/models/定制印刷品-订单描述/best_model/vocab/lookups.bin new file mode 100644 index 0000000..5416677 --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/vocab/lookups.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/vocab/strings.json b/app/resources/models/定制印刷品-订单描述/best_model/vocab/strings.json new file mode 100644 index 0000000..e1ea66e --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/vocab/strings.json @@ -0,0 +1,675 @@ +[ + " ", + "(", + ")", + "+", + ",", + "-", + ".", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "J", + "K", + "L", + "M", + "N", + "P", + "Q", + "R", + "ROOT", + "S", + "T", + "U", + "V", + "Y", + "Z", + "[", + "]", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "zh", + "\u00d7", + "\u2606", + "\u3010", + "\u3011", + "\u4e00", + "\u4e03", + "\u4e07", + "\u4e09", + "\u4e0a", + "\u4e0b", + "\u4e0d", + "\u4e0e", + "\u4e13", + "\u4e16", + "\u4e1c", + "\u4e1d", + "\u4e24", + "\u4e2a", + "\u4e2b", + "\u4e2d", + "\u4e30", + "\u4e3a", + "\u4e3b", + "\u4e3d", + "\u4e3e", + "\u4e43", + "\u4e48", + "\u4e50", + "\u4e86", + "\u4e8c", + "\u4e91", + "\u4e92", + "\u4e94", + "\u4ea7\u54c1", + "\u4eac", + "\u4eae", + "\u4eba", + "\u4ec0", + "\u4ed8", + "\u4ef6", + "\u4f0d", + "\u4f1f", + "\u4f38", + "\u4f3c", + "\u4f53", + "\u4f5c", + "\u4f69", + "\u4fa8", + "\u4fdd", + "\u4fe1", + "\u4fee", + "\u5012", + "\u502a", + "\u505a", + "\u513f", + "\u5149", + "\u514b", + "\u515c", + "\u516c", + "\u516d", + "\u5170", + "\u5171", + "\u5177", + "\u5185", + "\u5199", + "\u51b0", + "\u51bb", + "\u51c0", + "\u51c9", + "\u51e4", + "\u51fa", + "\u5206", + "\u5207", + "\u5229", + "\u522b", + "\u522e", + "\u522e\u522e\u819c\u5c3a\u5bf8", + "\u5230", + "\u5236", + "\u5237", + "\u523b", + "\u529b", + "\u52a0", + "\u52a0\u6025", + "\u52a8", + "\u5305", + "\u5316", + "\u5317", + "\u533a", + "\u533b", + "\u5341", + "\u5343", + "\u534a", + "\u534e", + "\u5355", + "\u5355\u53f7", + "\u5357", + "\u5361", + "\u5370", + "\u5382", + "\u538b", + "\u5398", + "\u53a8", + "\u53cc", + "\u53d1", + "\u53e4", + "\u53eb", + "\u53f6", + "\u53f7", + "\u53f8", + "\u53fb", + "\u5403", + "\u5404", + "\u5408", + "\u5409", + "\u540c", + "\u540d", + "\u5416", + "\u5446", + "\u548c", + "\u5495", + "\u5496", + "\u54aa", + "\u54c1", + "\u54c6", + "\u54c8", + "\u54d1", + "\u554a", + "\u5561", + "\u5566", + "\u5584", + "\u55b7", + "\u563f", + "\u565c", + "\u56db", + "\u56de", + "\u56e1", + "\u56e2", + "\u56ed", + "\u56fd", + "\u56fe", + "\u5706", + "\u5728", + "\u5730", + "\u573a", + "\u5740", + "\u574a", + "\u574f", + "\u5766", + "\u579a", + "\u57ce", + "\u57fa", + "\u58ee", + "\u58f0", + "\u58f3", + "\u58f9", + "\u5904", + "\u5907", + "\u590d", + "\u590f", + "\u5915", + "\u591a", + "\u5927", + "\u5929", + "\u5934", + "\u5947", + "\u5948", + "\u5956", + "\u5957", + "\u5973", + "\u597d", + "\u5986", + "\u5988", + "\u598d", + "\u5999", + "\u59ae", + "\u59d0", + "\u59d1", + "\u59ec", + "\u59ff", + "\u5a18", + "\u5a1c", + "\u5a1f", + "\u5b50", + "\u5b54", + "\u5b57", + "\u5b5f", + "\u5b64", + "\u5b66", + "\u5b69", + "\u5b81", + "\u5b85", + "\u5b87", + "\u5b89", + "\u5b8b", + "\u5b9a", + "\u5b9d", + "\u5ba2", + "\u5bb0", + "\u5bb6", + "\u5bb9", + "\u5bbe", + "\u5bc4", + "\u5bf8", + "\u5c01", + "\u5c0f", + "\u5c1d", + "\u5c27", + "\u5c3a", + "\u5c3a\u5bf8", + "\u5c3c", + "\u5c71", + "\u5c81", + "\u5c9b", + "\u5ddd", + "\u5dde", + "\u5de5", + "\u5de5\u827a", + "\u5de6", + "\u5deb", + "\u5dee", + "\u5df1", + "\u5e02", + "\u5e03", + "\u5e06", + "\u5e08", + "\u5e0c", + "\u5e1c", + "\u5e45", + "\u5e54", + "\u5e72", + "\u5e73", + "\u5e74", + "\u5e7f", + "\u5e86", + "\u5e8f", + "\u5e95", + "\u5ea6", + "\u5efa", + "\u5f04", + "\u5f0f", + "\u5f20", + "\u5f39", + "\u5f69", + "\u5f71", + "\u5f84", + "\u5fbd", + "\u5fc3", + "\u5fc6", + "\u601d", + "\u6021", + "\u6025", + "\u6027", + "\u602a", + "\u603b", + "\u610f", + "\u61a8", + "\u6210", + "\u6237", + "\u624b", + "\u6253", + "\u6263", + "\u627e", + "\u627f", + "\u62a4", + "\u62c9", + "\u62fe", + "\u6302", + "\u6309", + "\u6392", + "\u6446", + "\u644a", + "\u6495", + "\u6539", + "\u653b", + "\u653e", + "\u653f", + "\u654f", + "\u6551", + "\u6570", + "\u6570\u91cf", + "\u6587", + "\u658c", + "\u6599", + "\u65a4", + "\u65b9", + "\u65cf", + "\u65d7", + "\u65e0", + "\u65e5", + "\u65e9", + "\u65f6", + "\u660e", + "\u6613", + "\u6615", + "\u661f", + "\u662f", + "\u6643", + "\u6653", + "\u665a", + "\u6668", + "\u667a", + "\u66f2", + "\u6700", + "\u6709", + "\u6714", + "\u671f", + "\u6728", + "\u6746", + "\u674e", + "\u6750\u8d28", + "\u6761", + "\u6770", + "\u677f", + "\u6797", + "\u679c", + "\u679d", + "\u67ab", + "\u67c4", + "\u67d1", + "\u67d2", + "\u67d4", + "\u6811", + "\u6816", + "\u6837", + "\u6839", + "\u683c", + "\u6843", + "\u6865", + "\u6881", + "\u6885", + "\u6893", + "\u68a6", + "\u68cd", + "\u6930", + "\u697c", + "\u6a21", + "\u6a58", + "\u6a59", + "\u6a80", + "\u6b21", + "\u6b23", + "\u6b27", + "\u6b3e", + "\u6bcf", + "\u6c34", + "\u6c49", + "\u6c5f", + "\u6c7d", + "\u6c81", + "\u6cb3", + "\u6cbb", + "\u6ce1", + "\u6ce2", + "\u6ce8", + "\u6cea", + "\u6cf0", + "\u6d01", + "\u6d12", + "\u6d25", + "\u6d41", + "\u6d59", + "\u6d77", + "\u6db5", + "\u6dcb", + "\u6df7", + "\u6dfc", + "\u6e14", + "\u6e21", + "\u6e2f", + "\u6e56", + "\u6e58", + "\u6e7e", + "\u6e90", + "\u6f47", + "\u706b", + "\u7070", + "\u7075", + "\u70ad", + "\u70ae", + "\u70b9", + "\u70df", + "\u70eb", + "\u7167", + "\u718a", + "\u7231", + "\u7247", + "\u7248", + "\u7259", + "\u725b", + "\u7269", + "\u7279", + "\u72ec", + "\u732b", + "\u738b", + "\u73ab", + "\u73b0", + "\u73b2", + "\u73e0", + "\u73ed", + "\u7433", + "\u7470", + "\u74dc", + "\u7518", + "\u751c", + "\u751f", + "\u7528", + "\u7528\u6237\u4fe1\u606f", + "\u75d5", + "\u767d", + "\u767e", + "\u7684", + "\u76ae", + "\u76f4", + "\u76f8", + "\u7701", + "\u77e5", + "\u77f3", + "\u7801", + "\u7802", + "\u786c", + "\u7941", + "\u798f", + "\u79be", + "\u79cb", + "\u79cd", + "\u7a7a", + "\u7a7f", + "\u7a9d", + "\u7ae0", + "\u7aef", + "\u7b19", + "\u7b2c", + "\u7b3a", + "\u7b52", + "\u7b7e", + "\u7bb1", + "\u7c73", + "\u7c91", + "\u7c98", + "\u7ca5", + "\u7ccd", + "\u7cd6", + "\u7d2b", + "\u7ea2", + "\u7eb1", + "\u7eb8", + "\u7eb9", + "\u7ebf", + "\u7ec4", + "\u7ecf", + "\u7edf", + "\u7ef3", + "\u7eff", + "\u7f0e", + "\u7f1d", + "\u7f29", + "\u7f2a", + "\u7f51", + "\u7f8e", + "\u7fbd", + "\u7fca", + "\u7fd4", + "\u8001", + "\u8010", + "\u803f", + "\u8083", + "\u80e1", + "\u80f6", + "\u80fd", + "\u8111", + "\u819c", + "\u81ea", + "\u81f4", + "\u820d", + "\u8239", + "\u8272", + "\u827a", + "\u827e", + "\u8299", + "\u82ad", + "\u82b1", + "\u82cf", + "\u82f1", + "\u8303", + "\u8304", + "\u8309", + "\u831c", + "\u8336", + "\u8339", + "\u8349", + "\u8363", + "\u8389", + "\u8393", + "\u83c7", + "\u83dc", + "\u840c", + "\u8425", + "\u8431", + "\u843d", + "\u8499", + "\u84c9", + "\u84dd", + "\u85af", + "\u85cf", + "\u86cb", + "\u86d9", + "\u874e", + "\u884c", + "\u8865", + "\u88c1", + "\u88c5", + "\u897f", + "\u8981", + "\u8986", + "\u89d2", + "\u8ba1", + "\u8bb8", + "\u8bbe", + "\u8bfa", + "\u8d21", + "\u8d22", + "\u8d27", + "\u8d28", + "\u8d34", + "\u8d35", + "\u8d70", + "\u8d77", + "\u8d85", + "\u8f66", + "\u8f6c", + "\u8fb9", + "\u8fbd", + "\u8fce", + "\u8fd9", + "\u8fdc", + "\u8ff7", + "\u9001", + "\u900f", + "\u9053", + "\u90c1", + "\u914d", + "\u9152", + "\u9171", + "\u9192", + "\u91ca", + "\u91cc", + "\u91cd", + "\u91cf", + "\u91d1", + "\u946b", + "\u94b0", + "\u94dc", + "\u94ed", + "\u94f6", + "\u9542", + "\u95f4", + "\u9632", + "\u9633", + "\u963f", + "\u9648", + "\u9655", + "\u9675", + "\u96c5", + "\u96e8", + "\u96ea", + "\u96f6", + "\u9732", + "\u9752", + "\u9762", + "\u9879", + "\u987a", + "\u9891", + "\u9897", + "\u9898", + "\u989c", + "\u98ce", + "\u98de", + "\u98df", + "\u996d", + "\u9970", + "\u9986", + "\u9999", + "\u9a74", + "\u9ad8", + "\u9b3c", + "\u9c7c", + "\u9e1f", + "\u9e25", + "\u9e45", + "\u9e7f", + "\u9ea6", + "\u9ebb", + "\u9ecf", + "\u9ed1", + "\u9f99", + "\u9f9f", + "\uff08", + "\uff09", + "\uff0c", + "\uff1a", + "\uff1b" +] \ No newline at end of file diff --git a/app/resources/models/定制印刷品-订单描述/best_model/vocab/vectors b/app/resources/models/定制印刷品-订单描述/best_model/vocab/vectors new file mode 100644 index 0000000..ebadaa5 Binary files /dev/null and b/app/resources/models/定制印刷品-订单描述/best_model/vocab/vectors differ diff --git a/app/resources/models/定制印刷品-订单描述/best_model/vocab/vectors.cfg b/app/resources/models/定制印刷品-订单描述/best_model/vocab/vectors.cfg new file mode 100644 index 0000000..32c800a --- /dev/null +++ b/app/resources/models/定制印刷品-订单描述/best_model/vocab/vectors.cfg @@ -0,0 +1,3 @@ +{ + "mode":"default" +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/config.cfg b/app/resources/models/订单尺寸识别/best_model/config.cfg new file mode 100644 index 0000000..28cff7e --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/config.cfg @@ -0,0 +1,135 @@ +[paths] +train = null +dev = null +vectors = null +init_tok2vec = null + +[system] +seed = 0 +gpu_allocator = null + +[nlp] +lang = "zh" +pipeline = ["ner"] +disabled = [] +before_creation = null +after_creation = null +after_pipeline_creation = null +batch_size = 1000 +vectors = {"@vectors":"spacy.Vectors.v1"} + +[nlp.tokenizer] +@tokenizers = "spacy.zh.ChineseTokenizer" +segmenter = "char" + +[components] + +[components.ner] +factory = "ner" +incorrect_spans_key = null +moves = null +scorer = {"@scorers":"spacy.ner_scorer.v1"} +update_with_oracle_cut_size = 100 + +[components.ner.model] +@architectures = "spacy.TransitionBasedParser.v2" +state_type = "ner" +extra_state_tokens = false +hidden_width = 64 +maxout_pieces = 2 +use_upper = true +nO = null + +[components.ner.model.tok2vec] +@architectures = "spacy.HashEmbedCNN.v2" +pretrained_vectors = null +width = 96 +depth = 4 +embed_size = 2000 +window_size = 1 +maxout_pieces = 3 +subword_features = true + +[corpora] + +[corpora.dev] +@readers = "spacy.Corpus.v1" +path = ${paths.dev} +gold_preproc = false +max_length = 0 +limit = 0 +augmenter = null + +[corpora.train] +@readers = "spacy.Corpus.v1" +path = ${paths.train} +gold_preproc = false +max_length = 0 +limit = 0 +augmenter = null + +[training] +seed = ${system.seed} +gpu_allocator = ${system.gpu_allocator} +dropout = 0.1 +accumulate_gradient = 1 +patience = 1600 +max_epochs = 0 +max_steps = 20000 +eval_frequency = 200 +frozen_components = [] +annotating_components = [] +dev_corpus = "corpora.dev" +train_corpus = "corpora.train" +before_to_disk = null +before_update = null + +[training.batcher] +@batchers = "spacy.batch_by_words.v1" +discard_oversize = false +tolerance = 0.2 +get_length = null + +[training.batcher.size] +@schedules = "compounding.v1" +start = 100 +stop = 1000 +compound = 1.001 +t = 0.0 + +[training.logger] +@loggers = "spacy.ConsoleLogger.v1" +progress_bar = false + +[training.optimizer] +@optimizers = "Adam.v1" +beta1 = 0.9 +beta2 = 0.999 +L2_is_weight_decay = true +L2 = 0.01 +grad_clip = 1.0 +use_averages = false +eps = 0.00000001 +learn_rate = 0.001 + +[training.score_weights] +ents_f = 1.0 +ents_p = 0.0 +ents_r = 0.0 +ents_per_type = null + +[pretraining] + +[initialize] +vectors = ${paths.vectors} +init_tok2vec = ${paths.init_tok2vec} +vocab_data = null +lookups = null +before_init = null +after_init = null + +[initialize.components] + +[initialize.tokenizer] +pkuseg_model = null +pkuseg_user_dict = "default" \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/meta.json b/app/resources/models/订单尺寸识别/best_model/meta.json new file mode 100644 index 0000000..9c57bd4 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/meta.json @@ -0,0 +1,35 @@ +{ + "lang":"zh", + "name":"pipeline", + "version":"0.0.0", + "spacy_version":">=3.8.7,<3.9.0", + "description":"", + "author":"", + "email":"", + "url":"", + "license":"", + "spacy_git_version":"4b65aa7", + "vectors":{ + "width":0, + "vectors":0, + "keys":0, + "name":null, + "mode":"default" + }, + "labels":{ + "ner":[ + "\u522e\u522e\u819c\u5c3a\u5bf8", + "\u5c3a\u5bf8", + "\u6570\u91cf" + ] + }, + "pipeline":[ + "ner" + ], + "components":[ + "ner" + ], + "disabled":[ + + ] +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/ner/cfg b/app/resources/models/订单尺寸识别/best_model/ner/cfg new file mode 100644 index 0000000..6cd11cf --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/ner/cfg @@ -0,0 +1,13 @@ +{ + "moves":null, + "update_with_oracle_cut_size":100, + "multitasks":[ + + ], + "min_action_freq":1, + "learn_tokens":false, + "beam_width":1, + "beam_density":0.0, + "beam_update_prob":0.0, + "incorrect_spans_key":null +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/ner/model b/app/resources/models/订单尺寸识别/best_model/ner/model new file mode 100644 index 0000000..7e8a367 Binary files /dev/null and b/app/resources/models/订单尺寸识别/best_model/ner/model differ diff --git a/app/resources/models/订单尺寸识别/best_model/ner/moves b/app/resources/models/订单尺寸识别/best_model/ner/moves new file mode 100644 index 0000000..3f5b269 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/ner/moves @@ -0,0 +1 @@ +movesP{"0":{},"1":{"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3},"2":{"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3},"3":{"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3},"4":{"":1,"\u522e\u522e\u819c\u5c3a\u5bf8":-1,"\u6570\u91cf":-2,"\u5c3a\u5bf8":-3},"5":{"":1}}cfgneg_key \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/tokenizer/cfg b/app/resources/models/订单尺寸识别/best_model/tokenizer/cfg new file mode 100644 index 0000000..a3e8b3d --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/tokenizer/cfg @@ -0,0 +1,3 @@ +{ + "segmenter":"char" +} \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/key2row b/app/resources/models/订单尺寸识别/best_model/vocab/key2row new file mode 100644 index 0000000..5416677 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/key2row @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/lookups.bin b/app/resources/models/订单尺寸识别/best_model/vocab/lookups.bin new file mode 100644 index 0000000..5416677 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/lookups.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/strings.json b/app/resources/models/订单尺寸识别/best_model/vocab/strings.json new file mode 100644 index 0000000..6d921d2 --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/strings.json @@ -0,0 +1,928 @@ +[ + "\n", + "!", + "(", + ")", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "ROOT", + "S", + "T", + "U", + "V", + "Y", + "Z", + "[", + "]", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "zh", + "{", + "}", + "\u00d7", + "\u2014", + "\u3010", + "\u3011", + "\u4e00", + "\u4e03", + "\u4e07", + "\u4e09", + "\u4e0a", + "\u4e0b", + "\u4e0d", + "\u4e0e", + "\u4e13", + "\u4e16", + "\u4e1b", + "\u4e1c", + "\u4e1d", + "\u4e24", + "\u4e25", + "\u4e2a", + "\u4e2b", + "\u4e2d", + "\u4e30", + "\u4e3a", + "\u4e3b", + "\u4e3d", + "\u4e48", + "\u4e4b", + "\u4e4c", + "\u4e50", + "\u4e71", + "\u4e7e", + "\u4e86", + "\u4e8c", + "\u4e91", + "\u4e94", + "\u4e95", + "\u4e9b", + "\u4ea6", + "\u4eac", + "\u4eae", + "\u4eba", + "\u4ec0", + "\u4eca", + "\u4ed3", + "\u4ed4", + "\u4ed8", + "\u4ee3", + "\u4eec", + "\u4ef6", + "\u4efb", + "\u4f18", + "\u4f1a", + "\u4f1f", + "\u4f26", + "\u4f38", + "\u4f3c", + "\u4f4e", + "\u4f55", + "\u4f59", + "\u4f5b", + "\u4f5c", + "\u4f60", + "\u4f73", + "\u4f8b", + "\u4f9d", + "\u4fa0", + "\u4faf", + "\u4fdd", + "\u4fe1", + "\u4fee", + "\u5019", + "\u5029", + "\u503e", + "\u5047", + "\u505a", + "\u5065", + "\u513f", + "\u5143", + "\u5148", + "\u5149", + "\u514b", + "\u5154", + "\u516b", + "\u516c", + "\u516d", + "\u5170", + "\u5171", + "\u5174", + "\u5176", + "\u5185", + "\u518d", + "\u5199", + "\u519b", + "\u51ac", + "\u51b0", + "\u51bb", + "\u51c0", + "\u51c9", + "\u51cc", + "\u51ef", + "\u51f8", + "\u51f9", + "\u51fa", + "\u5206", + "\u5207", + "\u5212", + "\u5218", + "\u5219", + "\u521a", + "\u521b", + "\u5229", + "\u522b", + "\u522e", + "\u522e\u522e\u819c\u5c3a\u5bf8", + "\u5230", + "\u5236", + "\u5237", + "\u5238", + "\u523b", + "\u524d", + "\u529b", + "\u52a0", + "\u52a8", + "\u52a9", + "\u52b3", + "\u52c7", + "\u5305", + "\u5316", + "\u5317", + "\u533a", + "\u533b", + "\u534a", + "\u534e", + "\u5355", + "\u5357", + "\u535a", + "\u535c", + "\u5361", + "\u5370", + "\u5373", + "\u5382", + "\u538b", + "\u5398", + "\u539f", + "\u53bb", + "\u53ca", + "\u53cb", + "\u53cc", + "\u53cd", + "\u53d1", + "\u53d4", + "\u53d6", + "\u53e3", + "\u53e4", + "\u53ea", + "\u53ef", + "\u53f3", + "\u53f6", + "\u53f7", + "\u53f8", + "\u5403", + "\u5404", + "\u5408", + "\u5409", + "\u540a", + "\u540c", + "\u540d", + "\u540e", + "\u5416", + "\u541b", + "\u542b", + "\u5434", + "\u543e", + "\u544a", + "\u5468", + "\u5473", + "\u548c", + "\u5495", + "\u5496", + "\u54c1", + "\u54c6", + "\u54d1", + "\u54e5", + "\u54e6", + "\u54f2", + "\u5510", + "\u551b", + "\u5546", + "\u5561", + "\u5565", + "\u5566", + "\u55b7", + "\u565c", + "\u56db", + "\u56de", + "\u56e0", + "\u56ed", + "\u56f0", + "\u56fd", + "\u56fe", + "\u5706", + "\u571f", + "\u5728", + "\u5730", + "\u5733", + "\u5740", + "\u5764", + "\u578b", + "\u579a", + "\u57ab", + "\u57ce", + "\u57fa", + "\u5802", + "\u5821", + "\u58c1", + "\u58ee", + "\u58f3", + "\u58f9", + "\u5904", + "\u5907", + "\u590f", + "\u5915", + "\u5916", + "\u591a", + "\u591c", + "\u5927", + "\u5929", + "\u5934", + "\u5939", + "\u5947", + "\u5956", + "\u5965", + "\u5973", + "\u597d", + "\u5982", + "\u598d", + "\u5996", + "\u5999", + "\u59b9", + "\u59d0", + "\u59ff", + "\u5a01", + "\u5a03", + "\u5a77", + "\u5a9b", + "\u5b50", + "\u5b54", + "\u5b57", + "\u5b66", + "\u5b81", + "\u5b87", + "\u5b89", + "\u5b8b", + "\u5b8c", + "\u5b9a", + "\u5b9d", + "\u5b9e", + "\u5ba2", + "\u5ba4", + "\u5bb5", + "\u5bb6", + "\u5bb9", + "\u5bbd", + "\u5bc4", + "\u5bc6", + "\u5bcc", + "\u5bf8", + "\u5bf9", + "\u5c01", + "\u5c04", + "\u5c0f", + "\u5c11", + "\u5c14", + "\u5c1a", + "\u5c24", + "\u5c31", + "\u5c3a", + "\u5c3a\u5bf8", + "\u5c3c", + "\u5c42", + "\u5c45", + "\u5c71", + "\u5cb8", + "\u5ddd", + "\u5dde", + "\u5de5", + "\u5de6", + "\u5df1", + "\u5df2", + "\u5df4", + "\u5e02", + "\u5e03", + "\u5e05", + "\u5e06", + "\u5e08", + "\u5e0c", + "\u5e18", + "\u5e1b", + "\u5e1c", + "\u5e26", + "\u5e45", + "\u5e54", + "\u5e72", + "\u5e73", + "\u5e74", + "\u5e78", + "\u5e7c", + "\u5e7f", + "\u5e86", + "\u5e8f", + "\u5e94", + "\u5e95", + "\u5ea6", + "\u5eb7", + "\u5efa", + "\u5f00", + "\u5f02", + "\u5f04", + "\u5f0f", + "\u5f20", + "\u5f2f", + "\u5f39", + "\u5f3a", + "\u5f53", + "\u5f62", + "\u5f69", + "\u5f80", + "\u5f84", + "\u5f85", + "\u5f88", + "\u5f90", + "\u5f97", + "\u5fb7", + "\u5fbd", + "\u5fc3", + "\u5fc6", + "\u5fe7", + "\u5feb", + "\u5ff5", + "\u601d", + "\u6021", + "\u6025", + "\u602a", + "\u603b", + "\u60dc", + "\u60f3", + "\u60f9", + "\u6101", + "\u610f", + "\u6148", + "\u6167", + "\u61d2", + "\u61ff", + "\u6210", + "\u6211", + "\u6237", + "\u624b", + "\u6253", + "\u6263", + "\u626b", + "\u6279", + "\u6280", + "\u628a", + "\u6298", + "\u62a5", + "\u62c9", + "\u62cd", + "\u62d2", + "\u62db", + "\u62fc", + "\u62fe", + "\u6302", + "\u6309", + "\u631e", + "\u6363", + "\u636e", + "\u6392", + "\u63a5", + "\u63d0", + "\u63d2", + "\u63e1", + "\u6446", + "\u644a", + "\u6495", + "\u64e6", + "\u6539", + "\u653e", + "\u6551", + "\u6570", + "\u6570\u91cf", + "\u6587", + "\u658c", + "\u6599", + "\u65b0", + "\u65b9", + "\u65cf", + "\u65d7", + "\u65e0", + "\u65e5", + "\u65e9", + "\u65f6", + "\u65fa", + "\u660e", + "\u6613", + "\u6615", + "\u661f", + "\u662f", + "\u6641", + "\u6643", + "\u664b", + "\u6653", + "\u665a", + "\u6668", + "\u6674", + "\u667a", + "\u66b4", + "\u66f2", + "\u66f9", + "\u66fc", + "\u6700", + "\u6708", + "\u6709", + "\u670b", + "\u6728", + "\u673a", + "\u6746", + "\u674e", + "\u6750", + "\u6761", + "\u6765", + "\u6768", + "\u676d", + "\u676f", + "\u6770", + "\u677f", + "\u6797", + "\u679c", + "\u679d", + "\u67ab", + "\u67c4", + "\u67cf", + "\u67d2", + "\u67e0", + "\u6807", + "\u6811", + "\u6837", + "\u6839", + "\u6843", + "\u6846", + "\u6865", + "\u6881", + "\u6885", + "\u68a6", + "\u68cd", + "\u6930", + "\u697c", + "\u6986", + "\u6a21", + "\u6a31", + "\u6a58", + "\u6a59", + "\u6aac", + "\u6b21", + "\u6b23", + "\u6b3e", + "\u6b63", + "\u6b65", + "\u6bb7", + "\u6bcf", + "\u6bd4", + "\u6bd5", + "\u6bdb", + "\u6beb", + "\u6c34", + "\u6c49", + "\u6c5f", + "\u6c7d", + "\u6c90", + "\u6ca1", + "\u6cb3", + "\u6cb9", + "\u6cbb", + "\u6cc9", + "\u6ce1", + "\u6ce2", + "\u6ce5", + "\u6ce8", + "\u6d01", + "\u6d17", + "\u6d25", + "\u6d2a", + "\u6d3b", + "\u6d41", + "\u6d46", + "\u6d59", + "\u6d69", + "\u6d6e", + "\u6d74", + "\u6d77", + "\u6da6", + "\u6dd8", + "\u6df1", + "\u6e05", + "\u6e14", + "\u6e21", + "\u6e56", + "\u6e90", + "\u6eaa", + "\u6f47", + "\u6fa1", + "\u706b", + "\u706f", + "\u7070", + "\u7075", + "\u70ad", + "\u70ae", + "\u70b9", + "\u70eb", + "\u70ed", + "\u710a", + "\u7136", + "\u7167", + "\u718a", + "\u71d5", + "\u7231", + "\u7247", + "\u7248", + "\u724c", + "\u7259", + "\u725b", + "\u7269", + "\u7279", + "\u72d7", + "\u72ec", + "\u7315", + "\u732a", + "\u732b", + "\u7334", + "\u7389", + "\u738b", + "\u73af", + "\u73b0", + "\u73b2", + "\u73bb", + "\u73e0", + "\u73ed", + "\u7426", + "\u742a", + "\u742e", + "\u7433", + "\u7483", + "\u74dc", + "\u74f7", + "\u7518", + "\u751f", + "\u7528", + "\u752c", + "\u7532", + "\u753b", + "\u754c", + "\u7586", + "\u75c5", + "\u75d5", + "\u766b", + "\u767b", + "\u767d", + "\u767e", + "\u7684", + "\u76ae", + "\u76df", + "\u76f4", + "\u76f8", + "\u7701", + "\u771f", + "\u777f", + "\u77e5", + "\u77f3", + "\u7801", + "\u7834", + "\u786e", + "\u78b3", + "\u795e", + "\u7965", + "\u798f", + "\u79b9", + "\u79c0", + "\u79cd", + "\u79d1", + "\u79d8", + "\u79e6", + "\u7a7a", + "\u7a7f", + "\u7acb", + "\u7ae0", + "\u7aef", + "\u7b11", + "\u7b2c", + "\u7b49", + "\u7b52", + "\u7b71", + "\u7b7e", + "\u7b94", + "\u7bb1", + "\u7c73", + "\u7c7b", + "\u7c98", + "\u7ca5", + "\u7cbe", + "\u7cd6", + "\u7cfb", + "\u7d20", + "\u7d2b", + "\u7ea2", + "\u7ea7", + "\u7eac", + "\u7eaf", + "\u7eb1", + "\u7eb5", + "\u7eb8", + "\u7eb9", + "\u7ebf", + "\u7ec4", + "\u7edd", + "\u7edf", + "\u7eee", + "\u7ef3", + "\u7ef4", + "\u7f0e", + "\u7f18", + "\u7f1d", + "\u7f29", + "\u7f3a", + "\u7f57", + "\u7f8a", + "\u7f8e", + "\u7fa4", + "\u7fca", + "\u8001", + "\u8033", + "\u8054", + "\u805a", + "\u8083", + "\u8089", + "\u808c", + "\u80a0", + "\u80f6", + "\u80fd", + "\u819c", + "\u81ea", + "\u81f4", + "\u8272", + "\u827a", + "\u827e", + "\u8299", + "\u82a6", + "\u82ad", + "\u82af", + "\u82b1", + "\u82b3", + "\u82cf", + "\u82e5", + "\u82f1", + "\u8303", + "\u8304", + "\u8309", + "\u831c", + "\u8336", + "\u8349", + "\u8363", + "\u8389", + "\u838e", + "\u8393", + "\u83b1", + "\u83b2", + "\u83b9", + "\u83c7", + "\u83ca", + "\u83e9", + "\u840c", + "\u840d", + "\u8425", + "\u8431", + "\u843d", + "\u846b", + "\u8499", + "\u84c9", + "\u84dd", + "\u852b", + "\u8537", + "\u854a", + "\u8587", + "\u85cf", + "\u866b", + "\u86cb", + "\u86d9", + "\u8717", + "\u87f9", + "\u884c", + "\u8857", + "\u8865", + "\u888b", + "\u88c1", + "\u88c5", + "\u88d4", + "\u88f9", + "\u897f", + "\u8981", + "\u8986", + "\u89c1", + "\u89d2", + "\u8ba1", + "\u8ba4", + "\u8bb0", + "\u8bb8", + "\u8bbe", + "\u8bd5", + "\u8bda", + "\u8bed", + "\u8bf7", + "\u8bfa", + "\u8c01", + "\u8c03", + "\u8c46", + "\u8c5a", + "\u8d1d", + "\u8d21", + "\u8d22", + "\u8d27", + "\u8d28", + "\u8d2d", + "\u8d34", + "\u8d35", + "\u8d38", + "\u8d39", + "\u8d5e", + "\u8d77", + "\u8d85", + "\u8ddf", + "\u8def", + "\u8eab", + "\u8f66", + "\u8f69", + "\u8f6c", + "\u8fb0", + "\u8fb9", + "\u8fbd", + "\u8fbe", + "\u8fce", + "\u8fd0", + "\u8fd9", + "\u8fdc", + "\u8fea", + "\u8ff7", + "\u8ff9", + "\u9000", + "\u9001", + "\u900f", + "\u9047", + "\u9053", + "\u90b5", + "\u90d1", + "\u90dd", + "\u90e8", + "\u90ed", + "\u90fd", + "\u914d", + "\u9152", + "\u9171", + "\u9192", + "\u91c7", + "\u91ca", + "\u91cc", + "\u91cd", + "\u91cf", + "\u91d1", + "\u946b", + "\u9488", + "\u94a9", + "\u94b0", + "\u94b1", + "\u94c3", + "\u94dc", + "\u94dd", + "\u94f6", + "\u94fa", + "\u9501", + "\u950c", + "\u9526", + "\u9542", + "\u9547", + "\u956d", + "\u957f", + "\u95e8", + "\u95f4", + "\u9601", + "\u961f", + "\u9633", + "\u963f", + "\u9648", + "\u9655", + "\u9675", + "\u9676", + "\u969c", + "\u96c5", + "\u96c6", + "\u96e8", + "\u96ea", + "\u96f6", + "\u96fe", + "\u9700", + "\u9706", + "\u972d", + "\u9732", + "\u9752", + "\u9759", + "\u9762", + "\u9879", + "\u987a", + "\u987e", + "\u9897", + "\u989c", + "\u98ce", + "\u98de", + "\u9965", + "\u996d", + "\u9970", + "\u9971", + "\u9988", + "\u9999", + "\u9a70", + "\u9a91", + "\u9ad8", + "\u9c7c", + "\u9e1f", + "\u9e2d", + "\u9e4f", + "\u9ea6", + "\u9ebb", + "\u9ec4", + "\u9ed1", + "\u9f50", + "\u9f99", + "\u9f9f", + "\uff01", + "\uff08", + "\uff09", + "\uff0c", + "\uff1a", + "\uff1b" +] \ No newline at end of file diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/vectors b/app/resources/models/订单尺寸识别/best_model/vocab/vectors new file mode 100644 index 0000000..ebadaa5 Binary files /dev/null and b/app/resources/models/订单尺寸识别/best_model/vocab/vectors differ diff --git a/app/resources/models/订单尺寸识别/best_model/vocab/vectors.cfg b/app/resources/models/订单尺寸识别/best_model/vocab/vectors.cfg new file mode 100644 index 0000000..32c800a --- /dev/null +++ b/app/resources/models/订单尺寸识别/best_model/vocab/vectors.cfg @@ -0,0 +1,3 @@ +{ + "mode":"default" +} \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..9b5ed21 --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1 @@ +from .base import * diff --git a/app/schemas/apis.py b/app/schemas/apis.py new file mode 100644 index 0000000..6c6d4f4 --- /dev/null +++ b/app/schemas/apis.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Field + +from app.models.enums import MethodType + + +class BaseApi(BaseModel): + path: str = Field(..., description="API路径", example="/api/v1/user/list") + summary: str = Field("", description="API简介", example="查看用户列表") + method: MethodType = Field(..., description="API方法", example="GET") + tags: str = Field(..., description="API标签", example="User") + + +class ApiCreate(BaseApi): ... + + +class ApiUpdate(BaseApi): + id: int + + +class Paginate(BaseModel): + page: int = Field(1, alias="pageNo", description="页码", example=1) + page_size: int = Field(20, alias="pageSize", description="每页数量", example=20) + \ No newline at end of file diff --git a/app/schemas/automation.py b/app/schemas/automation.py new file mode 100644 index 0000000..8a7f8f1 --- /dev/null +++ b/app/schemas/automation.py @@ -0,0 +1,164 @@ +# app/schemas/automation.py + +from datetime import datetime +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field +from app.models.enums import TaskStatus, ScenarioScope, ActionType + +# ---------------------------- +# Scenario Schemas +# ---------------------------- + +class ScenarioBase(BaseModel): + title: str = Field(..., max_length=255, description="场景标题") + trigger: Dict[str, Any] = Field(..., description="触发条件(事件/时间)") + actions: List[Dict[str, Any]] = Field(default_factory=list, description="执行动作模板列表") + notes: Optional[str] = Field(None, description="备注") + is_global: bool = Field(True, description="是否全局场景") + owner_user_id: Optional[str] = Field(None, max_length=64, description="归属用户ID(非全局时有效)") + enabled: bool = Field(True, description="是否启用") + scope: ScenarioScope = Field(ScenarioScope.ALL, description="作用角色范围") + + +class ScenarioCreate(ScenarioBase): + pass + + +class ScenarioUpdate(BaseModel): + id: int = Field(..., description="场景ID") + title: Optional[str] = Field(None, max_length=255) + trigger: Optional[Dict[str, Any]] = None + actions: Optional[List[Dict[str, Any]]] = None + notes: Optional[str] = None + is_global: Optional[bool] = None + owner_user_id: Optional[str] = Field(None, max_length=64) + enabled: Optional[bool] = None + scope: Optional[ScenarioScope] = None + + # model_config = {"extra": "forbid"} # 禁止传入未定义字段 + + +class ScenarioCopy(BaseModel): + id: int = Field(..., description="场景ID") + # model_config = {"extra": "forbid"} # 禁止传入未定义字段 + + +class ScenarioRead(ScenarioBase): + id: int + created_at: datetime + updated_at: datetime + + model_config = {"from_attributes": True} + + +# ---------------------------- +# Task Schemas +# ---------------------------- + +class TaskBase(BaseModel): + title: str = Field(..., max_length=255, description="待办标题") + # actions: List[Dict[str, Any]] = Field(default_factory=list, description="动作列表(动态组件渲染用)") + event_data: Optional[Dict[str, Any]] = Field(default_factory=dict, description="事件数据") + notes: Optional[str] = Field(None, description="备注") + reason: Optional[str] = Field(None, description="触发原因") + + # 关联字段 + source_scenario_id: Optional[int] = Field(None, description="来源场景ID") + owner_user_id: Optional[str] = Field(None, max_length=64, description="归属用户ID") + + # 分配与状态 + assignee_user_id: Optional[str] = Field(None, max_length=64, description="指派人用户ID") + assignee_username: Optional[str] = Field(None, max_length=64, description="指派人用户名") + status: TaskStatus = Field(TaskStatus.PENDING, description="任务状态") + + # 自动化控制 + due_at: Optional[datetime] = Field(None, description="截止时间") + auto_closeable: bool = Field(True, description="是否允许自动消除") + closed_by: Optional[str] = Field(None, max_length=32, description="关闭方式: manual/auto/expired") + + +class TaskCreate(TaskBase): + pass + + +class TaskUpdate(BaseModel): + title: Optional[str] = Field(None, max_length=255) + content: Optional[List[Dict[str, Any]]] = None + notes: Optional[str] = None + source_scenario_id: Optional[int] = None + related_order_id: Optional[str] = Field(None, max_length=64) + related_customer_id: Optional[str] = Field(None, max_length=64) + group_id: Optional[str] = Field(None, max_length=64) + assignee_user_id: Optional[str] = Field(None, max_length=64) + assignee_username: Optional[str] = Field(None, max_length=64) + status: Optional[TaskStatus] = None + due_at: Optional[datetime] = None + auto_closeable: Optional[bool] = None + closed_by: Optional[str] = Field(None, max_length=32) + completed_at: Optional[datetime] = None + + # model_config = {"extra": "forbid"} + + +class TaskRead(TaskBase): + id: int + created_at: datetime + updated_at: datetime + completed_at: Optional[datetime] = None + + model_config = {"from_attributes": True} + + +# ---------------------------- +# Action Schemas +# ---------------------------- + +class ActionBase(BaseModel): + type: Optional[ActionType] = Field(None, description="操作类型") + detail: Dict[str, Any] = Field(default_factory=dict, description="操作详情") + done: bool = Field(False, description="是否已完成") + done_at: Optional[datetime] = Field(None, description="完成时间") + result: Dict[str, Any] = Field(default_factory=dict, description="操作结果") + userid: Optional[str] = Field(None, max_length=64, description="操作用户ID") + username: Optional[str] = Field(None, max_length=64, description="操作用户名称") + notes: Optional[str] = Field(None, description="操作备注") + task_id: int = Field(..., description="所属任务ID") + action_template_id: Optional[str] = Field(None, max_length=64, description="操作模板ID") + + +class ActionCreate(ActionBase): + pass + + +class ActionUpdate(BaseModel): + type: Optional[ActionType] = None + detail: Optional[Dict[str, Any]] = None + done: Optional[bool] = None + done_at: Optional[datetime] = None + result: Optional[Dict[str, Any]] = None + userid: Optional[str] = Field(None, max_length=64) + username: Optional[str] = Field(None, max_length=64) + notes: Optional[str] = None + task_id: Optional[int] = None + action_template_id: Optional[str] = Field(None, max_length=64) + + model_config = {"extra": "forbid"} + + +class ActionRead(ActionBase): + id: int + created_at: datetime + updated_at: datetime + + model_config = {"from_attributes": True} + +class NotifyAction(BaseModel): + platform: str = Field(..., description="平台类型") + webhook_url: str = Field(..., description="Webhook URL") + template: str = Field(..., description="模板内容") + need_at: bool = Field(False, description="是否需要@用户") + + __meta_mapping__ = { + "template": {"type": "text"} + } + diff --git a/app/schemas/base.py b/app/schemas/base.py new file mode 100644 index 0000000..bbb69b8 --- /dev/null +++ b/app/schemas/base.py @@ -0,0 +1,52 @@ +from typing import Any, Optional + +from fastapi.responses import JSONResponse + + +class Success(JSONResponse): + def __init__( + self, + code: int = 200, + msg: Optional[str] = "OK", + data: Optional[Any] = None, + **kwargs, + ): + content = {"code": code, "msg": msg, "data": data} + content.update(kwargs) + super().__init__(content=content, status_code=code) + + +class Fail(JSONResponse): + def __init__( + self, + code: int = 400, + msg: Optional[str] = None, + data: Optional[Any] = None, + **kwargs, + ): + content = {"code": code, "msg": msg, "data": data} + content.update(kwargs) + super().__init__(content=content, status_code=code) + + +class SuccessExtra(JSONResponse): + def __init__( + self, + code: int = 200, + msg: Optional[str] = None, + data: Optional[Any] = None, + total: int = 0, + page: int = 1, + page_size: int = 20, + **kwargs, + ): + content = { + "code": code, + "msg": msg, + "data": data, + "total": total, + "page": page, + "page_size": page_size, + } + content.update(kwargs) + super().__init__(content=content, status_code=code) diff --git a/app/schemas/codegen.py b/app/schemas/codegen.py new file mode 100644 index 0000000..050612b --- /dev/null +++ b/app/schemas/codegen.py @@ -0,0 +1,48 @@ +from datetime import datetime +from typing import Optional, Union + +from pydantic import BaseModel, Field + + +class BaseCodegen(BaseModel): + name: Optional[str] = Field(description="名称") + author: Optional[str] = Field(description="作者", default='') + description: Optional[str] = Field(description="描述", default='') + version: Optional[str] = Field(description="版本", default='') + fields: Optional[Union[str, dict]] = Field(description="字段", default='{}') + relations: Optional[Union[str, dict]] = Field(description="关系", default='{}') + api: Optional[Union[str, dict]] = Field(description="API", default='{}') + menu: Optional[Union[str, dict]] = Field(description="菜单", default='{}') + + +class CodegenCreate(BaseCodegen): + name: str = Field(description="名称") + fields: str = Field(description="字段") + +class CodegenUpdate(BaseModel): + id: int = Field(example=1) + name: str = Field(example="管理员") + desc: str = Field("", example="管理员角色") + + +class CodegenUpdateMenusApis(BaseModel): + id: int + menu_ids: list[int] = [] + api_infos: list[dict] = [] + + +class DatasourceInfo(BaseModel): + id: int = Field(..., description="ID") + tableName: Optional[str] = Field(..., description="名称") + tableComment: Optional[str] = Field(..., description="注释") + created_at: Optional[datetime] + updated_at: Optional[datetime] + + +class DatasourceCreate(DatasourceInfo): + tableName: str = Field(..., description="名称") + tableComment: str = Field(..., description="注释") + + +class DatasourceUpdate(DatasourceInfo): + id: int = Field(..., description="ID") diff --git a/app/schemas/crm.py b/app/schemas/crm.py new file mode 100644 index 0000000..dd13e64 --- /dev/null +++ b/app/schemas/crm.py @@ -0,0 +1,170 @@ +from pydantic import BaseModel, Field, field_validator +from typing import Optional, Any +from datetime import datetime # 如果需要 datetime 对象 + +# 如果你有 TimestampMixin,需要在这里导入或定义 +# class TimestampMixin(BaseModel): +# created_at: Optional[datetime] = None +# updated_at: Optional[datetime] = None + +class CrmCustomerBase(BaseModel): + # 保留 JSON 中存在的字段,并添加 alias + customer_name: Optional[str] = Field(default=None, alias="customerName") + province: Optional[str] = Field(default=None, alias="province") + city: Optional[str] = Field(default=None, alias="city") + area: Optional[str] = Field(default=None, alias="area") + clinch_order: Optional[str] = Field(default=None, alias="clinchOrder") + clinch_price: Optional[str] = Field(default=None, alias="clinchPrice") + first_clinch_time: Optional[str] = Field(default=None, alias="firstClinchTime") + last_clinch_time: Optional[str] = Field(default=None, alias="lastClinchTime") + refund_order_times: Optional[str] = Field(default=None, alias="refundOrderTimes") + refund_amount: Optional[str] = Field(default=None, alias="refundAmount") + private_flag: Optional[int] = Field(default=None, alias="privateFlag") # JSON 中是 int + touch_flag: Optional[int] = Field(default=None, alias="touchFlag") # JSON 中是 int + open_id: Optional[str] = Field(default=None, alias="openId") + xy_id: Optional[int] = Field(default=None, alias="xyId") # JSON 中是 int + last_touch_time: Optional[str] = Field(default=None, alias="lastTouchTime") + add_wechat_time: Optional[str] = Field(default=None, alias="addWeChatTime") + mobile: Optional[str] = Field(default=None, alias="mobile") + source: Optional[str] = Field(default=None, alias="source") + we_chat_image: Optional[str] = Field(default=None, alias="weChatImage") + cid: Optional[int] = Field(default=None, alias="cid") # JSON 中是 int + last_trade_time: Optional[str] = Field(default=None, alias="lastTradeTime") + private_jump: Optional[int] = Field(default=None, alias="privateJump") # JSON 中是 int + lst_pay_ord_amt: Optional[float] = Field(default=None, alias="lstPayOrdAmt") # JSON 中是 float + pay_avg_ord_amt_td: Optional[float] = Field(default=None, alias="payAvgOrdAmtTd") # JSON 中是 float + refund_ord_rate_td: Optional[str] = Field(default=None, alias="refundOrdRateTd") # JSON 中是 string like '0.00%' + touch_cnt_td: Optional[int] = Field(default=None, alias="touchCntTd") # JSON 中是 int + pay_ord_day_cnt_1y: Optional[int] = Field(default=None, alias="payOrdDayCnt1y") # JSON 中是 int + pay_same_item_max_cnt_td: Optional[int] = Field(default=None, alias="paySameItemMaxCntTd") # JSON 中是 int + pay_diff_item_cnt_td: Optional[int] = Field(default=None, alias="payDiffItemCntTd") # JSON 中是 int + pay_item_max_cnt_td: Optional[int] = Field(default=None, alias="payItemMaxCntTd") # JSON 中是 int + user_type_90d: Optional[int] = Field(default=None, alias="userType90d") # JSON 中是 int + # shop_id 和 shop_name 不在 JSON 中,但根据你的 Tortoise 模型定义,它们是 CrmCustomer 特有的,所以不加 alias + shop_id: Optional[int] = Field(default=None) # 没有 alias,因为它在 JSON 中不存在 + shop_name: Optional[str] = Field(default=None) # 没有 alias,因为它在 JSON 中不存在 + + class Config: + populate_by_name = True # 允许使用别名或字段名创建实例 + +class CrmCustomerCreate(CrmCustomerBase): + pass + +class CrmCustomerUpdate(BaseModel): + # 这个模型的字段与 JSON 结构无关,保留原有定义 + customer_name: Optional[str] = None + customer_remark: Optional[str] = None + customer_desc: Optional[str] = None + mobiles: Optional[list[str]] = None + labels: Optional[list[dict[str, Any]]] = None + follow_member_name: Optional[str] = None + follow_member_mobile: Optional[str] = None + follow_member_dept: Optional[str] = None + source_tips: Optional[str] = None + cus_channel: Optional[str] = None + +class CrmCustomer(CrmCustomerBase): + id: int = Field(alias="id") # JSON 中的 'id' 字段 + data_source: Optional[str] = None + last_updated_from: Optional[str] = None + # updated_at: Optional[datetime] = Field(default=None, alias="updatedAt") # 如果需要 datetime 对象 + updated_at: Optional[str] = Field(default=None, alias="updatedAt") # 使用 str 匹配 JSON 格式 + + class Config: + from_attributes = True + populate_by_name = True + +def convert_to_str(v) -> Optional[str]: + """将输入值转换为字符串""" + if v is None: + return v + return str(v) + +class CrmCustomerBase(BaseModel): + # 对应 Tortoise 模型字段,并使用 JSON 中的键作为 alias + cid: Optional[int] = Field(default=None, alias="cid", description="CID") + customer_name: Optional[str] = Field(default=None, max_length=64, alias="customerName", description="客户名称") + clinch_order: Optional[str] = Field(default=None, max_length=64, alias="clinchOrder", description="成交订单数") + clinch_price: Optional[str] = Field(default=None, max_length=64, alias="clinchPrice", description="成交金额") + first_clinch_time: Optional[datetime] = Field(default=None, alias="firstClinchTime", description="首次成交时间") + last_clinch_time: Optional[datetime] = Field(default=None, alias="lastClinchTime", description="最后成交时间") + refund_order_times: Optional[str] = Field(default=None, max_length=64, alias="refundOrderTimes", description="退款订单数") + refund_amount: Optional[str] = Field(default=None, max_length=64, alias="refundAmount", description="退款金额") + private_flag: Optional[bool] = Field(default=False, alias="privateFlag", description="是否私有") + touch_flag: Optional[bool] = Field(default=False, alias="touchFlag", description="是否触达") + open_id: Optional[str] = Field(default=None, max_length=64, alias="openId", description="微信openid") + xy_id: Optional[int] = Field(default=None, alias="xyId", description="星云有客中的用户ID", index=True) + last_touch_time: Optional[datetime] = Field(default=None, alias="lastTouchTime", description="最后触达时间") + add_wechat_time: Optional[datetime] = Field(default=None, alias="addWeChatTime", description="添加微信时间") + mobile: Optional[str] = Field(default=None, max_length=64, alias="mobile", description="手机号") + source: Optional[str] = Field(default=None, max_length=64, alias="source", description="来源") + we_chat_image: Optional[str] = Field(default=None, max_length=512, alias="weChatImage", description="微信头像") + last_trade_time: Optional[datetime] = Field(default=None, alias="lastTradeTime", description="最后交易时间") + private_jump: Optional[bool] = Field(default=False, alias="privateJump", description="是否跳转") + lst_pay_ord_amt: Optional[str] = Field(default=None, alias="lstPayOrdAmt", description="最近一次支付金额") + pay_avg_ord_amt_td: Optional[str] = Field(default=None, alias="payAvgOrdAmtTd", description="最近30天平均支付金额") + refund_ord_rate_td: Optional[str] = Field(default=None, alias="refundOrdRateTd", description="最近30天退款订单数") + touch_cnt_td: Optional[int] = Field(default=None, alias="touchCntTd", description="最近30天触达次数") + pay_ord_day_cnt_1y: Optional[int] = Field(default=None, alias="payOrdDayCnt1y", description="最近一年支付订单数") + pay_same_item_max_cnt_td: Optional[int] = Field(default=None, alias="paySameItemMaxCntTd", description="最近30天相同商品最大购买数量") + pay_diff_item_cnt_td: Optional[int] = Field(default=None, alias="payDiffItemCntTd", description="最近30天不同商品购买数量") + pay_item_max_cnt_td: Optional[int] = Field(default=None, alias="payItemMaxCntTd", description="最近30天最大购买商品数量") + user_type_90d: Optional[int] = Field(default=None, alias="userType90d", description="最近90天用户类型") + # shop_id 和 shop_name 在 JSON 中不存在,因此不设置 alias + shop_id: Optional[int] = Field(default=None, description="店铺ID", index=True) + shop_name: Optional[str] = Field(default=None, max_length=64, description="店铺名称") + + @field_validator('lst_pay_ord_amt', 'pay_avg_ord_amt_td', 'refund_ord_rate_td', mode='before') + def validate_and_convert_to_str(cls, v): + return convert_to_str(v) + + class Config: + populate_by_name = True # 允许使用别名或字段名创建实例 + +class CrmCustomerCreate(CrmCustomerBase): + pass + +class CrmCustomerUpdate(BaseModel): + # 保留原始定义(如果需要) + customer_name: Optional[str] = None + customer_remark: Optional[str] = None + customer_desc: Optional[str] = None + mobiles: Optional[list[str]] = None + labels: Optional[list[dict[str, Any]]] = None + follow_member_name: Optional[str] = None + follow_member_mobile: Optional[str] = None + follow_member_dept: Optional[str] = None + source_tips: Optional[str] = None + cus_channel: Optional[str] = None + + + +class BaseCrmBindInfo(BaseModel): + platform: Optional[str]= None + customer_id: Optional[int]= None + orderId: Optional[str]= None + buyer_id: Optional[str]= None + userid: Optional[str]= None + name: Optional[str]= None + nick_name: Optional[str]= None + avatar: Optional[str]= None + platform_id: Optional[str]= None + need_confirm: Optional[bool]= None + tenant_name: Optional[str]= None + tenant_id: Optional[str]= None + remark: Optional[str]= None + extra: Optional[str]= None + + +class CrmBindInfoCreate(BaseCrmBindInfo): + id: Optional[int] = None + pass + + +class CrmBindInfoUpdate(BaseCrmBindInfo): + id: int = Field(..., description="绑定信息ID") + orderId: str = Field(..., description="订单ID") + customer_id: int = Field(..., description="客户ID") + name: str = Field(..., description="客户名称") + + diff --git a/app/schemas/customer.py b/app/schemas/customer.py new file mode 100644 index 0000000..2362031 --- /dev/null +++ b/app/schemas/customer.py @@ -0,0 +1,61 @@ +from pydantic import BaseModel +from typing import Optional, List, Dict, Any +from datetime import datetime + +class CustomerBase(BaseModel): + customer_id: str + customer_name: Optional[str] = None + nick_name: Optional[str] = None + head_url: Optional[str] = None + gender: Optional[int] = None + customer_remark: Optional[str] = None + customer_desc: Optional[str] = None + mobiles: Optional[List[str]] = None + labels: Optional[List[Dict[str, Any]]] = None + source_type: Optional[int] = None + apply_mode: Optional[int] = None + source_tips: Optional[str] = None + source_room_id: Optional[str] = None + source_room_name: Optional[str] = None + source_room_image: Optional[str] = None + follow_member_vid: str + follow_member_name: Optional[str] = None + follow_member_mobile: Optional[str] = None + follow_member_dept: Optional[str] = None + follow_member_avatar: Optional[str] = None + create_time: Optional[int] = None + update_time: Optional[int] = None + tag_time: Optional[int] = None + add_time: Optional[int] = None + friend_flag: Optional[int] = None + relation_flag: Optional[int] = None + cus_channel: Optional[str] = None + seq: Optional[str] = None + kh_type: Optional[int] = None + room_count: Optional[int] = 0 + user_count: Optional[int] = 0 + data_version: Optional[str] = None + +class CustomerCreate(CustomerBase): + pass + +class CustomerUpdate(BaseModel): + customer_name: Optional[str] = None + customer_remark: Optional[str] = None + customer_desc: Optional[str] = None + mobiles: Optional[List[str]] = None + labels: Optional[List[Dict[str, Any]]] = None + follow_member_name: Optional[str] = None + follow_member_mobile: Optional[str] = None + follow_member_dept: Optional[str] = None + source_tips: Optional[str] = None + cus_channel: Optional[str] = None + +class Customer(CustomerBase): + id: int + data_source: Optional[str] = None + last_updated_from: Optional[str] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True \ No newline at end of file diff --git a/app/schemas/depts.py b/app/schemas/depts.py new file mode 100644 index 0000000..48b6c39 --- /dev/null +++ b/app/schemas/depts.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel, Field + + +class BaseDept(BaseModel): + name: str = Field(..., description="部门名称", example="研发中心") + desc: str = Field("", description="备注", example="研发中心") + order: int = Field(0, description="排序") + parent_id: int = Field(0, description="父部门ID") + + +class DeptCreate(BaseDept): ... + + +class DeptUpdate(BaseDept): + id: int + + def update_dict(self): + return self.model_dump(exclude_unset=True, exclude={"id"}) diff --git a/app/schemas/login.py b/app/schemas/login.py new file mode 100644 index 0000000..67cd483 --- /dev/null +++ b/app/schemas/login.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class CredentialsSchema(BaseModel): + username: str = Field(..., description="用户名称", example="admin") + password: str = Field(..., description="密码", example="123456") + + +class JWTOut(BaseModel): + access_token: str + username: str + + +class JWTPayload(BaseModel): + user_id: int + username: str + is_superuser: bool + exp: datetime diff --git a/app/schemas/menus.py b/app/schemas/menus.py new file mode 100644 index 0000000..28c6dd5 --- /dev/null +++ b/app/schemas/menus.py @@ -0,0 +1,52 @@ +from enum import StrEnum +from typing import Optional + +from pydantic import BaseModel, Field + + +class MenuType(StrEnum): + CATALOG = "catalog" # 目录 + MENU = "menu" # 菜单 + + +class BaseMenu(BaseModel): + id: int + name: str + path: str + remark: Optional[dict] + menu_type: Optional[MenuType] + icon: Optional[str] + order: int + parent_id: int + is_hidden: bool + component: str + keepalive: bool + redirect: Optional[str] + children: Optional[list["BaseMenu"]] + + +class MenuCreate(BaseModel): + menu_type: MenuType = Field(default=MenuType.CATALOG.value) + name: str = Field(example="用户管理") + icon: Optional[str] = "ph:user-list-bold" + path: str = Field(example="/system/user") + order: Optional[int] = Field(example=1) + parent_id: Optional[int] = Field(example=0, default=0) + is_hidden: Optional[bool] = False + component: str = Field(default="Layout", example="/system/user") + keepalive: Optional[bool] = True + redirect: Optional[str] = "" + + +class MenuUpdate(BaseModel): + id: int + menu_type: Optional[MenuType] = Field(example=MenuType.CATALOG.value) + name: Optional[str] = Field(example="用户管理") + icon: Optional[str] = "ph:user-list-bold" + path: Optional[str] = Field(example="/system/user") + order: Optional[int] = Field(example=1) + parent_id: Optional[int] = Field(example=0) + is_hidden: Optional[bool] = False + component: str = Field(example="/system/user") + keepalive: Optional[bool] = False + redirect: Optional[str] = "" diff --git a/app/schemas/msg.py b/app/schemas/msg.py new file mode 100644 index 0000000..89091c0 --- /dev/null +++ b/app/schemas/msg.py @@ -0,0 +1,99 @@ +from typing import Optional +from datetime import datetime + +from pydantic import BaseModel, Field +from app.models.enums import MsgType, ActionType + + +class BaseMsg(BaseModel): + hash_id: Optional[str] = Field(description="消息哈希ID") + title: Optional[str] = Field(description="消息标题") + content: Optional[str] = Field(description="消息内容") + detail: Optional[str] = Field(default={}, description="消息详情") + is_read: Optional[bool] = Field(default=False, description="是否已读") + read_at: Optional[datetime] = Field(default=None, description="已读时间") + is_delete: Optional[bool] = Field(default=False, description="是否已删除") + delete_at: Optional[datetime] = Field(default=None, description="删除时间") + is_send: Optional[bool] = Field(default=False, description="是否已发送") + send_at: Optional[datetime] = Field(default=None, description="发送时间") + type: Optional[MsgType] = Field(default=MsgType.SYSTEM, description="消息类型") + owner_id: Optional[str] = Field(description="消息所有者ID") + owner_name: Optional[str] = Field(description="消息所有者名称") + + +class MsgCreate(BaseMsg): + hash_id: str = Field(description="消息哈希ID") + title: str = Field(description="消息标题") + content: str = Field(description="消息内容") + type: MsgType = Field(default=MsgType.SYSTEM, description="消息类型") + owner_id: str = Field(description="消息所有者ID") + owner_name: str = Field(description="消息所有者名称") + + +class MsgUpdate(BaseModel): + id: int + is_read: Optional[bool] = Field(default=None, description="是否已读") + is_delete: Optional[bool] = Field(default=None, description="是否已删除") + is_send: Optional[bool] = Field(default=None, description="是否已发送") + +class MsgNewOrder(BaseModel): + type: Optional[MsgType] = Field(default=MsgType.NEW_ORDER, description="消息类型") + order_id: str = Field(description="订单ID") + order_status: str = Field(description="订单状态") + is_refund: Optional[bool] = Field(default=False, alias="order_type", description="是否退款: 0-普通订单,1-退款订单") + buyer_id: str = Field(description="买家ID") + buyer_nick: str = Field(description="买家名称") + + def __str__(self): + return f'订单:{self.order_id} {self.order_status},买家:{self.buyer_nick or self.buyer_id} {"退款" if self.is_refund else ""}' + +class MsgFilter(BaseModel): + type: Optional[MsgType] = Field(default=None, description="消息类型") + is_read: Optional[bool] = Field(default=None, description="是否已读") + is_refund: Optional[bool] = Field(default=None, description="是否退款订单") + +class BaseAction(BaseModel): + type: Optional[ActionType] = Field(default=None, description="操作类型") + detail: Optional[dict] = Field(default={}, description="操作详情") + user_id: Optional[str] = Field(default=None, description="操作用户ID") + user_name: Optional[str] = Field(default=None, description="操作用户名称") + +class ActionCreate(BaseAction): + done: Optional[bool] = Field(default=False, description="是否完成") + type: ActionType = Field(description="操作类型") + detail: dict = Field(description="操作详情") + userid: str = Field(description="操作用户ID") + username: str = Field(description="操作用户名称") + +class ActionUpdate(BaseAction): + id: int = Field(description="操作ID") + +class FollowFormErp(BaseModel): + order_id: str = Field(description="订单ID", alias="trade_no") + order_title: str = Field(description="订单标题", alias="title") + pay_time: str = Field(description="支付时间", alias="pay_time") + pay_price: str = Field(description="支付金额", alias="price") + remark: str = Field(description="erp额外备注", alias="remark") + + shop_name: str = Field(description="店铺名称", alias="shop_name") + + """ + customer_id = fields.CharField(max_length=64, null=True, description="客户ID", index=True) + customer_name = fields.CharField(max_length=64, null=True, description="客户名称", index=True) + customer_taobao_id = fields.CharField(max_length=64, null=True, description="客户淘宝ID", index=True) + + staff_id = fields.CharField(max_length=64, null=True, description="企微员工ID", index=True) + staff_name = fields.CharField(max_length=64, null=True, description="企微员工名称", index=True) + staff_time = fields.DatetimeField(null=True, description="添加时间", index=True) + + designer_id = fields.CharField(max_length=64, null=True, description="设计师ID", index=True) + designer_name = fields.CharField(max_length=64, null=True, description="设计师名称", index=True) + + kefu_id = fields.CharField(max_length=64, null=True, description="淘宝客服ID", index=True) + kefu_name = fields.CharField(max_length=64, null=True, description="淘宝客服名称", index=True) + + """ + +class FeishuEvent(BaseModel): + challenge: str = Field(description="验证challenge") + diff --git a/app/schemas/query.py b/app/schemas/query.py new file mode 100644 index 0000000..6882bee --- /dev/null +++ b/app/schemas/query.py @@ -0,0 +1,27 @@ +from fastapi import Query, Request +from tortoise.expressions import Q + + +class QueryParams: + def __init__(self, + request: Request, + page: int = Query(1, description="页码"), + page_size: int = Query(10, description="每页数量"), + ): + self.page = page + self.page_size = page_size + self.query_kwargs = dict(request.query_params) + + self.q = Q() + for key, value in self.query_kwargs.items(): + if key not in ['page', 'page_size']: + # 使用 contains 方法实现包含查询 + self.q &= Q(**{f"{key}__contains": value}) + + def get_offset(self): + return (self.page - 1) * self.page_size + + def search_query(self, model, order=('-created_at',)): + query = model.filter(self.q) + return query, query.offset((self.page - 1) * self.page_size).limit(self.page_size).order_by(*order) + \ No newline at end of file diff --git a/app/schemas/roles.py b/app/schemas/roles.py new file mode 100644 index 0000000..908ef48 --- /dev/null +++ b/app/schemas/roles.py @@ -0,0 +1,32 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +class BaseRole(BaseModel): + id: int + name: str + desc: str = "" + users: Optional[list] = [] + menus: Optional[list] = [] + apis: Optional[list] = [] + created_at: Optional[datetime] + updated_at: Optional[datetime] + + +class RoleCreate(BaseModel): + name: str = Field(example="管理员") + desc: str = Field("", example="管理员角色") + + +class RoleUpdate(BaseModel): + id: int = Field(example=1) + name: str = Field(example="管理员") + desc: str = Field("", example="管理员角色") + + +class RoleUpdateMenusApis(BaseModel): + id: int + menu_ids: list[int] = [] + api_infos: list[dict] = [] diff --git a/app/schemas/task.py b/app/schemas/task.py new file mode 100644 index 0000000..b68382f --- /dev/null +++ b/app/schemas/task.py @@ -0,0 +1,75 @@ +from typing import Optional +from typing import Dict, Any, Union +from datetime import datetime + +from pydantic import BaseModel, Field, ConfigDict +from app.models.enums import TaskStatus, TaskType + +class DecodeTaskParams(BaseModel): + filename: str = Field(..., description="文件名") + sheet_name: str = Field(..., description="工作表名称") + decode_type: str = Field(..., description="解码类型") + header: str = Field(..., description="解析列") + model: Optional[str] = Field(None, description="模型名称") + +class DecodeTaskResult(BaseModel): + filename: str = Field(..., description="结果保存文件名") + spend: float = Field(..., description="任务耗时") + rows: int = Field(..., description="解析行数") + +class TaskResponse(BaseModel): + id: int + name: str + type: TaskType # ← Pydantic 会自动转为字符串 + params: dict + status: TaskStatus # ← 同上 + result: Optional[dict] + + class Config: + from_attributes=True + use_enum_values = True # ← 关键!将 Enum 转为值(如 'pending') + # 或者在 Pydantic v2 中: + # model_config = ConfigDict(use_enum_values=True) + +class TaskModel(BaseModel): + name: str + type: TaskType # Enum: e.g., "decode", "upload", etc. + params: Dict[str, Any] # 存储原始参数字典 + status: TaskStatus + result: Optional[Dict[str, Any]] = None + + @classmethod + def create_decode_task( + cls, + name: str, + params: DecodeTaskParams, + status: TaskStatus = TaskStatus.PENDING + ) -> "TaskModel": + """创建解码任务的工厂方法""" + return cls( + name=name, + type=TaskType.DECODE_ORDER, + params=params.dict(), + status=status + ) + + def get_params(self) -> BaseModel: + """反序列化 params 为具体模型""" + if self.type == TaskType.DECODE_ORDER: + return DecodeTaskParams(**self.params) + raise ValueError(f"Unknown task type: {self.type}") + + def set_result(self, result: Union[DecodeTaskResult]) -> None: + """设置结果(自动序列化)""" + if self.type == TaskType.DECODE_ORDER: + self.result = result.dict() + else: + raise ValueError(f"Unsupported result for task type: {self.type}") + + def get_result(self) -> Optional[BaseModel]: + """反序列化 result 为具体模型""" + if self.result is None: + return None + if self.type == TaskType.DECODE_ORDER: + return DecodeTaskResult(**self.result) + raise ValueError(f"Unknown task type: {self.type}") diff --git a/app/schemas/users.py b/app/schemas/users.py new file mode 100644 index 0000000..1f26e9c --- /dev/null +++ b/app/schemas/users.py @@ -0,0 +1,49 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, EmailStr, Field + + +class BaseUser(BaseModel): + id: int + email: Optional[EmailStr] = None + username: Optional[str] = None + is_active: Optional[bool] = True + is_superuser: Optional[bool] = False + created_at: Optional[datetime] + updated_at: Optional[datetime] + last_login: Optional[datetime] + roles: Optional[list] = [] + + +class UserCreate(BaseModel): + email: EmailStr = Field(example="admin@qq.com") + username: str = Field(example="admin") + password: str = Field(example="123456") + is_active: Optional[bool] = True + is_superuser: Optional[bool] = False + role_ids: Optional[List[int]] = [] + dept_id: Optional[int] = Field(0, description="部门ID") + + def create_dict(self): + return self.model_dump(exclude_unset=True, exclude={"role_ids"}) + + +class UserUpdate(BaseModel): + id: int + email: EmailStr + username: str + is_active: Optional[bool] = True + is_superuser: Optional[bool] = False + role_ids: Optional[List[int]] = [] + dept_id: Optional[int] = 0 + +class UserUpdateOnline(BaseModel): + id: int + email: EmailStr + username: str + + +class UpdatePassword(BaseModel): + old_password: str = Field(description="旧密码") + new_password: str = Field(description="新密码") diff --git a/app/schemas/weixin.py b/app/schemas/weixin.py new file mode 100644 index 0000000..548f634 --- /dev/null +++ b/app/schemas/weixin.py @@ -0,0 +1,220 @@ +from datetime import datetime +from typing import List, Optional, Union + +from pydantic import BaseModel, EmailStr, Field + +class BaseWeixinUser(BaseModel): + userid: Optional[str] = None + corp_id: Optional[str] = None + vid: Optional[str] = None + wx_id_hash: Optional[str] = None + is_quit: Optional[bool] = None + crmid: Optional[int] = None + depart_ids: Optional[List[int]] = None + position: Optional[str] = None + role: Optional[str] = None + realname: Optional[str] = None + username: Optional[str] = None + english_name: Optional[str] = None + name: Optional[str] = None + # 将所有属性映射为可选属性 + alias: Optional[str] = None + avatar: Optional[str] = None + department: Optional[int] = None + mobile: Optional[str] = None + email: Optional[EmailStr] = None + gender: Optional[int] = None + has_external_user_permit: Optional[bool] = None + erpid: Optional[int] = None + erp_name: Optional[str] = None + crmid: Optional[int] = None + crm_name: Optional[str] = None + +class WeixinUserCreate(BaseWeixinUser): + userid: str = Field(example="julia") + name: str = Field(example='用户姓名') + role: str = Field(example='用户角色') + + +class WeixinUserUpdate(BaseModel): + id: int + crmid: Optional[int] = None + erpid: Optional[int] = None + is_quit: Optional[bool] = None + + +class Member(BaseModel): + userid: Optional[str] = Field(description="用户ID") + name: Optional[str] = Field(description="用户姓名") + type: Optional[int] = Field(description="用户类型: 1-外部成员, 2-内部成员") + join_time: Optional[int] = Field(description="加入时间") + # join_scene: Optional[int] = Field(description="加入场景") + # unionid: Optional[str] = Field(description="微信UnionID") + # invitor: Optional[dict] = Field(description="邀请人") + group_nickname: Optional[str] = Field(description="群内昵称") + +class BaseWeixinGroupChat(BaseModel): + chat_id: Optional[str] = Field(description="企业微信中的群聊ID") + name: Optional[str] = Field(description="群聊名称") + admin_list: Optional[List[Member]] = Field(description="群聊管理员列表") + create_time: Optional[int] = Field(description="群聊创建时间") + owner: Optional[str] = Field(description="群聊群主") + member_version: Optional[str] = Field(description="群聊成员版本") + external_member_list: Optional[List[Member]] = Field(description="群聊外部成员列表") + internal_member_list: Optional[List[Member]] = Field(description="群聊内部成员列表") + +class WeixinGroupChatEvent(BaseWeixinGroupChat): + external_user_count: Optional[int] = Field(description="群聊外部成员数量") + internal_member_count: Optional[int] = Field(description="群聊内部成员数量") + + __exclude_filter__ = ["admin_list", "member_version", "id"] + + +class TriggerWeixinGroupChat(BaseModel): + group_name: str = Field(alias="groupName", description="群聊名称") + order_id: Optional[str] = Field(alias="orderId", description="订单ID", default=None) + user_ids: Optional[List[str]] = Field(alias="userIds", description="群聊成员列表") + external_user_ids: Optional[List[str]] = Field(alias="externalUserIds", description="群聊外部成员列表") + +class WeixinGroupChatCreate(BaseWeixinGroupChat): + pass + +class WeixinGroupChatUpdate(BaseWeixinGroupChat): + pass + +class BaseWeixinGroupChatXingyun(BaseModel): + xingyun_chat_id: Optional[int] = Field(alias="chatId") + avatars: Optional[List[Union[str, None]]] = Field(alias="avatars") + chat_id: Optional[str] = Field(alias="groupChatId") + name: Optional[str] = Field(alias="chatName") + admin_list: Optional[List[str]] = Field(alias="admins") + create_time: Optional[int] = Field(alias="createTime") + owner: Optional[str] = Field(alias="ownerName") + member_version: Optional[str] = Field(default=None) + external_member_list: Optional[List[str]] = Field(default=None) + internal_member_list: Optional[List[str]] = Field(default=None) + +class WeixinGroupChatXingyunCreate(BaseWeixinGroupChatXingyun): + chat_id: str = Field(alias="groupChatId") + name: str = Field(alias="chatName") + xingyun_chat_id: int = Field(alias="chatId") + + +class BaseWeixinCustomer(BaseModel): + order_id: Optional[str] = Field(alias="orderId", description="订单ID", default=None) + shop_name: Optional[str] = Field(alias="shopName", description="店铺名称", default=None) + weixin_id: Optional[str] = Field(alias="weixinId", description="微信用户ID", default=None) + weixin_name: Optional[str] = Field(alias="weixinName", description="微信用户名", default=None) + weixin_unionid: Optional[str] = Field(alias="weixinUnionid", description="微信用户UnionID", default=None) + weixin_avatar: Optional[str] = Field(alias="weixinAvatar", description="微信用户头像", default=None) + xingyun_id: Optional[int] = Field(alias="xingyunId", description="星云用户ID", default=None) + xingyun_sex: Optional[int] = Field(alias="xingyunSex", description="星云用户性别", default=None) + xingyun_name: Optional[str] = Field(alias="xingyunName", description="星云用户名", default=None) + xingyun_avatar: Optional[str] = Field(alias="xingyunAvatar", description="星云用户头像", default=None) + xingyun_tags: Optional[list] = Field(alias="xingyunTags", description="星云用户标签", default=None) + xingyun_external_userid: Optional[str] = Field(alias="xingyunExternalUserid", description="星云用户外部ID", default=None) + xingyun_sync: Optional[bool] = Field(alias="xingyunSync", description="是否已同步到星云", default=False) + taobao_id: Optional[str] = Field(alias="taobaoId", description="淘宝用户ID", default=None) + taobao_name: Optional[str] = Field(alias="taobaoName", description="淘宝用户名", default=None) + taobao_avatar: Optional[str] = Field(alias="taobaoAvatar", description="淘宝用户头像", default=None) + need_confirm: Optional[bool] = Field(alias="needConfirm", description="是否需要确认", default=None) + extra: Optional[dict] = Field(alias="extra", description="额外信息", default=None) + +class WeixinCustomerCreate(BaseWeixinCustomer): + pass + +class WeixinCustomerUpdate(BaseWeixinCustomer): + pass + +class WeixinCustomerFilter(BaseModel): + order_id: Optional[str] = Field(description="订单ID", default=None) + has_order: Optional[bool] = Field(description="是否有订单", default=None) + shop_name: Optional[str] = Field(description="店铺名称", default=None) + # weixin_id: Optional[str] = Field(description="微信用户ID", default=None) + # weixin_unionid: Optional[str] = Field(description="微信用户UnionID", default=None) + # xingyun_id: Optional[int] = Field(description="星云用户ID", default=None) + # xingyun_name: Optional[str] = Field(description="星云用户名", default=None) + xingyun_tags: Optional[str] = Field(description="星云用户标签", default=None) + xingyun_sync: Optional[bool] = Field(description="是否已同步到星云", default=False) + # taobao_id: Optional[str] = Field(description="淘宝用户ID", default=None) + # taobao_name: Optional[str] = Field(description="淘宝用户名", default=None) + union_id: Optional[str] = Field(description="联合用户ID", default=None) + union_name: Optional[str] = Field(description="联合用户名", default=None) + + +class BaseWeixinCustomerXingyun(BaseModel): + weixin_id: Optional[str] = Field(alias="external_userid", description="微信用户ID", default=None) + weixin_name: Optional[str] = Field(alias="weixin_name", description="微信用户名", default=None) + xingyun_id: Optional[int] = Field(alias="cid", description="星云用户ID") + xingyun_name: Optional[str] = Field(alias="name", description="星云用户名") + xingyun_avatar: Optional[str] = Field(alias="avatar", description="星云用户头像") + xingyun_external_userid: Optional[str] = Field(alias="externalUserid", description="星云用户外部ID") + xingyun_sex: Optional[int] = Field(alias="sex", description="星云用户性别", default=None) + xingyun_tags: Optional[list] = Field(alias="tags", description="星云用户标签", default=None) + # 移除这些字段,因为它们将被存储在 extra 中 + exclusive_member_info: Optional[dict] = Field(alias="exclusiveMemberInfo", description=" exclusiveMemberInfo", default=None) + extra: Optional[dict] = Field(alias="extra", description="额外信息", default=None) + + def transform_to_extra(self): + new_obj = self.model_dump(exclude_unset=True, by_alias=True) + extra = { + 'exclusive_member_info': new_obj.pop('exclusiveMemberInfo', None), + } + new_name = new_obj.get('name', None) + if new_name: new_obj['weixin_name'] = new_name + return self.__class__(**new_obj, extra=extra) + +class WeixinCustomerXingyunCreate(BaseWeixinCustomerXingyun): + pass + + +class WeixinUserBindInfo(BaseModel): + userid: str = Field(description="微信用户ID") + username: Optional[str] = Field(description="微信用户名", alias="name") + role: Optional[str] = Field(description="用户角色", default=None) + erp_id: Optional[int] = Field(description="ERP用户ID", default=None) + avatar: Optional[str] = Field(description="微信用户头像", default=None) + taobao_id: Optional[str] = None + + +class WeixinOrderBindInfo(BaseModel): + userid: str = Field(description="微信用户ID") + order_id: str = Field(description="订单ID") + remark: Optional[str] = Field(description="企业微信向ERP同步订单时的备注", default=None) + +class WeixinOrderRemark(BaseModel): + order_id: str = Field(description="订单ID") + remark: Optional[str] = Field(description="企业微信向ERP同步订单时的备注", default=None) + + +class OrderUser(BaseModel): + role: Optional[str] = Field(description="用户角色", default=None) + taobao_id: Optional[str] = Field(description="旺旺名称", alias="id", default=None) + taobao_name: Optional[str] = Field(description="旺旺名称", alias="name", default=None) + userid: Optional[str] = Field(description="微信用户ID", default=None) + +class BindOrderResultEvent(BaseModel): + shop_name: Optional[str] = Field(description="店铺名称") + price: Optional[int] = Field(description="订单金额") + discount_fee: Optional[int] = Field(description="折扣金额") + real_payment: Optional[int] = Field(description="实付金额") + remark: Optional[str] = Field(description="企业微信向ERP同步订单时的备注", default=None) + status_string: Optional[str] = Field(description="订单状态") + pay_time: Optional[str] = Field(description="支付时间") + users: Optional[List[OrderUser]] = Field(description="绑定的用户信息") + + # __exclude_filter__ = ["member_version", "id"] + + +class CustomerRepeatPurchaseEvent(BindOrderResultEvent): + has_service_group: Optional[bool] = Field(description="是否有服务群", default=None) + __exclude_filter__ = ["remark", "status_string", "pay_time"] + +# CUSTOMER_SERVICE_ASSIGN_ORDER +class CustomerAssignOrderEvent(BindOrderResultEvent): + __exclude_filter__ = ["remark", "status_string"] + +# CUSTOMER_SERVICE_ASSIGN_ORDER +class CustomerRefundOrderEvent(BindOrderResultEvent): + __exclude_filter__ = ["status_string"] + diff --git a/app/settings/__init__.py b/app/settings/__init__.py new file mode 100644 index 0000000..ff19664 --- /dev/null +++ b/app/settings/__init__.py @@ -0,0 +1,3 @@ +from .config import settings as settings + +TORTOISE_ORM = settings.TORTOISE_ORM diff --git a/app/settings/config.py b/app/settings/config.py new file mode 100644 index 0000000..b4d8f1e --- /dev/null +++ b/app/settings/config.py @@ -0,0 +1,106 @@ +import os +import typing + +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + VERSION: str = "0.1.0" + APP_TITLE: str = "Vue FastAPI Admin" + PROJECT_NAME: str = "Vue FastAPI Admin" + APP_DESCRIPTION: str = "Description" + + CORS_ORIGINS: typing.List = ["*"] + CORS_ALLOW_CREDENTIALS: bool = True + CORS_ALLOW_METHODS: typing.List = ["*"] + CORS_ALLOW_HEADERS: typing.List = ["*"] + + DEBUG: bool = True + + PROJECT_ROOT: str = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) + BASE_DIR: str = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir)) + LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs") + SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32 + JWT_ALGORITHM: str = "HS256" + JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 day + TORTOISE_ORM: dict = { + "connections": { + # SQLite configuration + "sqlite": { + "engine": "tortoise.backends.sqlite", + "credentials": {"file_path": f"{BASE_DIR}/db_new.sqlite3"}, # Path to SQLite database file + }, + # MySQL/MariaDB configuration + # Install with: tortoise-orm[asyncmy] + "mysql": { + "engine": "tortoise.backends.mysql", + "credentials": { + "host": os.getenv("DB_HOST", "localhost"), # Database host address + "port": int(os.getenv("DB_PORT", 3306)), # Database port + "user": os.getenv("DB_USER", "root"), # Database username + "password": os.getenv("DB_PASSWORD", "db_password"), # Database password + "database": os.getenv("DB_NAME", "db_name"), # Database name + }, + }, + # PostgreSQL configuration + # Install with: tortoise-orm[asyncpg] + # "postgres": { + # "engine": "tortoise.backends.asyncpg", + # "credentials": { + # "host": os.getenv("DB_HOST", "localhost"), # Database host address + # "port": int(os.getenv("DB_PORT", 5432)), # Database port + # "user": os.getenv("DB_USER", "yourusername"), # Database username + # "password": os.getenv("DB_PASSWORD", "yourpassword"), # Database password + # "database": os.getenv("DB_NAME", "yourdatabase"), # Database name + # }, + # }, + # MSSQL/Oracle configuration + # Install with: tortoise-orm[asyncodbc] + # "oracle": { + # "engine": "tortoise.backends.asyncodbc", + # "credentials": { + # "host": os.getenv("DB_HOST", "localhost"), # Database host address + # "port": int(os.getenv("DB_PORT", 1433)), # Database port + # "user": os.getenv("DB_USER", "yourusername"), # Database username + # "password": os.getenv("DB_PASSWORD", "yourpassword"), # Database password + # "database": os.getenv("DB_NAME", "yourdatabase"), # Database name + # }, + # }, + # SQLServer configuration + # Install with: tortoise-orm[asyncodbc] + # "sqlserver": { + # "engine": "tortoise.backends.asyncodbc", + # "credentials": { + # "host": os.getenv("DB_HOST", "localhost"), # Database host address + # "port": int(os.getenv("DB_PORT", 1433)), # Database port + # "user": os.getenv("DB_USER", "yourusername"), # Database username + # "password": os.getenv("DB_PASSWORD", "yourpassword"), # Database password + # "database": os.getenv("DB_NAME", "yourdatabase"), # Database name + # }, + # }, + }, + "apps": { + "models": { + "models": ["app.models", "aerich.models"], + "default_connection": os.getenv("DB_CONNECTION", "sqlite"), + }, + }, + "use_tz": False, # Whether to use timezone-aware datetimes + "timezone": "Asia/Shanghai", # Timezone setting + } + DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S" + REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/2") + + LOG_FORMAT: str = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" + LOG_FILE_MAX_SIZE: int = 1024 * 1024 * 10 # 100 MB + LOG_FILE_BACKUP_COUNT: int = 5 # 保留5个备份文件 + + +settings = Settings() + +TORTOISE_ORM = settings.TORTOISE_ORM + +connections = settings.TORTOISE_ORM['connections'] +default_connection = settings.TORTOISE_ORM['apps']["models"]["default_connection"] + +TORTOISE_ORM['connections'] = {default_connection: connections[default_connection]} diff --git a/app/utils/codegen.py b/app/utils/codegen.py new file mode 100644 index 0000000..619be5f --- /dev/null +++ b/app/utils/codegen.py @@ -0,0 +1,272 @@ +import os +import zipfile +from io import BytesIO +from pathlib import Path +from mako.lookup import TemplateLookup +import black + +class CodeGenerator: + def __init__(self, template_root='templates'): + self.template_root = template_root + self.lookup = TemplateLookup(directories=[template_root], input_encoding='utf-8') + + def generate_files(self, entity_config, prj_template_dir, params, language='python'): + """ + 生成所有匹配的模板文件 + :param entity_config: 实体配置 + :param prj_template_dir: 项目对应的文件路径 + :param params: 生成参数 + :return: 生成的文件字典 {相对路径: 文件内容} + """ + entity_config = type("EntityConfig", (object,), entity_config) + generated_files = [] + template_dir = os.path.join(self.template_root, prj_template_dir) + + # 遍历模板目录 + for root, _, files in os.walk(template_dir): + for file in files: + if not file.endswith('.mako'): continue + template_path = os.path.join(root, file) + relative_path = os.path.relpath(template_path, self.template_root) + + # 渲染模板 + template = self.lookup.get_template(relative_path) + content = template.render(entity=entity_config, params=params, generator=self) + + if relative_path.endswith('.py.mako'): + content = self.post_process(content) + + # 计算输出路径 + output_path = self._get_output_path(relative_path, entity_config, params) + # generated_files[output_path] = content + generated_files.append(dict( + path=output_path, + content=content + )) + + return generated_files + + def post_process(self, content): + try: + # 尝试使用 Black 格式化代码 + formatted_code = black.format_str(content, mode=black.Mode(line_length=100)) + return formatted_code + except Exception as e: + print(f"Black 格式化失败: {e}") + return content + + def _get_output_path(self, template_path, entity_config, params): + """ + 根据模板路径计算输出路径 + :param template_path: 模板相对路径 + :return: 输出文件相对路径(无.mako后缀) + """ + # 移除.mako后缀 + if template_path.endswith('.mako'): + output_path = template_path[:-5] + else: + output_path = template_path + + # 替换实体名称占位符 + entity_name = entity_config.name + output_path = output_path.replace('Entity', entity_name.capitalize()) + output_path = output_path.replace('entity', entity_name.lower()) + + return output_path + + def _process_relations(self, entity): + """处理实体关系并返回处理后的结果""" + if not hasattr(entity, 'relations') or not entity.relations: + return [] + + processed = [] + for rel in entity.relations: + # 确保关系有必要的字段 + rel.setdefault('through', None) + rel.setdefault('description', '') + + # 计算反向引用名称 + rel['reverse_name'] = self._get_reverse_relation_name(entity.name, rel) + processed.append(rel) + return processed + + def _get_reverse_relation_name(self, entity_name, relation): + """生成反向引用名称""" + if relation['type'] == 'one-to-many': + return entity_name.lower() + elif relation['type'] == 'many-to-one': + return relation['name'] + elif relation['type'] == 'many-to-many': + return f"{entity_name.lower()}s" + return None + + def create_zip(self, generated_files): + """ + 将生成的文件打包为zip + :param generated_files: {路径: 内容} 字典 + :return: zip文件字节流 + """ + zip_buffer = BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for item in generated_files: + path, content = item.get('path'), item.get('content') + zip_file.writestr(path, content) + zip_buffer.seek(0) + return zip_buffer + + def create_local_code(self, generated_files, output_dir): + save_dir = Path(output_dir) + + for item in generated_files: + # 确保输出目录存在 + path, content = item.get('path'), item.get('content') + output_file = save_dir / path + output_file.parent.mkdir(parents=True, exist_ok=True) + + with open(output_file, 'w', encoding='utf-8', newline='') as f: + f.write(content) + + +if __name__ == "__main__": + # 使用示例 + generator = CodeGenerator() + + default_frontend_config = { + "editable": True, + "listable": True, + "detailable": True, + "sortable": True, + "filterable": True, + "filter_operator": 'equal', # equal | contains 默认过滤操作符为包含 + "display_type": 'text', # 默认显示类型为文本 + } + + entity_config = { + "name": "user", # 实体名称(小写) + "author": "Ly997", # 实体名称(小写) + "description": "系统用户管理", # 实体描述 + "version": "1.0.0", # 版本号 + "fields": [ + { + "name": "id", + "type": "Long", + "primary_key": True, + "description": "用户ID", + "required": True + }, + { + "name": "username", + "type": "String", + "length": 50, + "description": "用户名", + "required": True, + "unique": True, + "validations": [ + {"type": "min_length", "value": 4}, + {"type": "max_length", "value": 20} + ] + }, + { + "name": "email", + "type": "String", + "description": "电子邮箱", + "required": True, + "validations": [ + {"type": "email"} + ] + }, + { + "name": "password_hash", + "type": "String", + "description": "密码哈希", + "required": True, + "secret": True # 标记为敏感字段 + }, + { + "name": "is_active", + "type": "Boolean", + "description": "是否激活", + "default": True + }, + { + "name": "created_at", + "type": "DateTime", + "description": "创建时间", + "auto_now_add": True + }, + { + "name": "updated_at", + "type": "DateTime", + "description": "更新时间", + "auto_now": True + } + ], + "relations": [ + { + "name": "roles", + "type": "many-to-many", + "target": "role", + "through": "user_roles", + "description": "用户角色关联" + }, + { + "name": "posts", + "type": "one-to-many", + "target": "post", + "description": "用户发表的文章" + } + ], + "api": { + "operations": ["create", "read", "update", "delete", "list"], + "base_path": "/users", + "auth_required": True, + "permissions": { + "create": ["admin", "manager"], + "delete": ["admin"] + } + }, + "menu": { + "label": "用户管理", + "icon": "user", + "order": 10, + "submenu": [ + { + "label": "用户列表", + "path": "/users", + "icon": "list" + }, + { + "label": "角色管理", + "path": "/roles", + } + ] + } + } + params = { + "package": "com.example", + "author": "John Doe" + } + + # 在代码中合并默认配置和字段的前端配置 + for field in entity_config["fields"]: + frontend = {**default_frontend_config, **field} + field.update(frontend) + + import json + with open('tmp.json', 'w', encoding='utf-8') as f: + json.dump(entity_config, f, ensure_ascii=False, indent=4) + + # 生成所有文件 + generated_files = generator.generate_files(entity_config, 'relation-demo', params) + # generated_files = generator.generate_files(entity_config, 'vue-fastapi-admin', params) + generator.create_local_code(generated_files, 'tmp') + # 打包为zip + zip_data = generator.create_zip(generated_files) + + file_path = './tmp.zip' + with open(file_path, 'wb') as f: + f.write(zip_data.getvalue()) + print(f"文件已保存到 {file_path}") + + # 在Flask中返回zip文件示例 + # return send_file(zip_data, mimetype='application/zip', as_attachment=True, download_name='generated_code.zip') diff --git a/app/utils/common.py b/app/utils/common.py new file mode 100644 index 0000000..b95e03d --- /dev/null +++ b/app/utils/common.py @@ -0,0 +1,133 @@ + +import random +import string +from hashlib import md5 +from typing import Any, Dict, List, Union, Optional, get_origin, get_args, Type +from pydantic import BaseModel + +def gen_random_str(length=10, prefix="", suffix=""): + """ + 生成随机字符串,默认长度为10,可添加前缀和后缀 + """ + random_str = prefix + ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + suffix + return md5(random_str.encode()).hexdigest() + + +def split_generator(iterable, chunk_size=50): + """ + 将可迭代对象拆分为多个子列表 + """ + chunk = [] + for item in iterable: + chunk.append(item) + if len(chunk) == chunk_size: + yield chunk + chunk = [] + if chunk: + yield chunk + +async def async_split_generator(iterable, chunk_size=50): + """ + 将可迭代对象拆分为多个子列表 + """ + chunk = [] + async for item in iterable: + chunk.append(item) + if len(chunk) == chunk_size: + yield chunk + chunk = [] + if chunk: + yield chunk + +async def async_generator_to_list(iterable): + """ + 将可迭代对象转换为列表 + """ + return [item async for item in iterable] + +TYPE_MAPPING = { + "str": "string", + "int": "number", + "float": "number", + "bool": "boolean", + "list": "array", + "dict": "object", + "NoneType": "null", # 注意:None 的类型名是 'NoneType' +} + +def transform_pydantic_to_list( + cls: Type[BaseModel], + prefix: str = "" # 新增:当前路径前缀 +) -> List[Dict[str, Any]]: + if not issubclass(cls, BaseModel): + raise TypeError(f"{cls} is not a Pydantic BaseModel") + + result = [] + exclude_filter = getattr(cls, "__exclude_filter__", []) + meta_mapping = getattr(cls, "__meta_mapping__", {}) + + for field_name, field_info in cls.model_fields.items(): + if field_name in exclude_filter: + continue + + # 构建当前字段的完整路径 + current_path = f"{prefix}.{field_name}" if prefix else field_name + + field_meta = meta_mapping.get(field_name, {}) or {} + + raw_type = field_info.annotation + children = [] + display_type_name = "any" + + origin = get_origin(raw_type) + args = get_args(raw_type) + + # 处理 Optional[T](即 Union[T, None]) + if origin is Union and type(None) in args: + # 提取非 None 的类型 + non_none_types = [arg for arg in args if arg is not type(None)] + if len(non_none_types) == 1: + inner_type = non_none_types[0] + else: + inner_type = raw_type # 多类型 Union,暂不深入处理 + else: + inner_type = raw_type + + # 判断是否为 List[BaseModel] + list_origin = get_origin(inner_type) + if list_origin is list: + item_type = get_args(inner_type)[0] if get_args(inner_type) else Any + if isinstance(item_type, type) and issubclass(item_type, BaseModel): + children = transform_pydantic_to_list(item_type, prefix=current_path) + display_type_name = "list" + elif isinstance(inner_type, type) and issubclass(inner_type, BaseModel): + children = transform_pydantic_to_list(inner_type, prefix=current_path) + display_type_name = "object" + else: + # 基础类型:获取类型名 + if inner_type is type(None): + display_type_name = "NoneType" + elif hasattr(inner_type, '__name__'): + display_type_name = inner_type.__name__ + else: + display_type_name = str(inner_type) + + frontend_type = TYPE_MAPPING.get(display_type_name, "string") + + item = { + "value": current_path, # ✅ 使用层级路径 + "label": field_info.description or field_name, + "type": frontend_type, + **field_meta, + } + if children: + item["children"] = children + result.append(item) + + return result + +if __name__ == "__main__": + print(gen_random_str()) + print(gen_random_str()) + print(gen_random_str()) + print(gen_random_str()) diff --git a/app/utils/db.py b/app/utils/db.py new file mode 100644 index 0000000..66627a3 --- /dev/null +++ b/app/utils/db.py @@ -0,0 +1,134 @@ +import pymysql + + +class DatabaseInfo: + def __init__(self, host, user, port, password, database): + self.host = host + self.port = port + self.user = user + self.password = password + self.database = database + self.connection = None + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + + def connect(self): + try: + self.connection = pymysql.connect( + host=self.host, + port=self.port, + user=self.user, + password=self.password, + database=self.database + ) + except pymysql.Error as e: + print(f"数据库连接失败: {e}") + + def disconnect(self): + if self.connection: + self.connection.close() + + def get_all_tables(self): + if not self.connection: + self.connect() + try: + with self.connection.cursor() as cursor: + cursor.execute("SHOW TABLE STATUS") + tables = cursor.fetchall() + table_info_list = [] + for table in tables: + table_info = { + "tableName": table[0], + "tableComment": table[17] # 表注释信息在第 19 列(索引为 18) + } + table_info_list.append(table_info) + return table_info_list + except pymysql.Error as e: + print(f"获取表名失败: {e}") + return [] + + def get_table_info(self, table_name): + if not self.connection: + self.connect() + try: + with self.connection.cursor() as cursor: + cursor.execute("SHOW TABLE STATUS LIKE %s", (table_name,)) + table = cursor.fetchone() + table_info = { + "tableName": table[0], + "tableComment": table[17] # 表注释信息在第 19 列(索引为 18) + } + return table_info + except pymysql.Error as e: + print(f"获取表名失败: {e}") + return {} + + def parse_type_and_validation(self, column_type): + # 简单的类型解析,实际可能需要更复杂的逻辑 + if "int" in column_type.lower(): + return dict(type="int", validation="int") + elif "varchar" in column_type.lower(): + number = column_type.split("(")[1].split(")")[0].strip() + return dict(type="str", validations=[ + dict(type="str", min=1, max=int(number)) + ]) + # elif "datetime" in column_type.lower(): + # return dict(type="str", validations=[ + # dict(type="str", format="%Y-%m-%d %H:%M:%S") + # ]) + else: + return dict(type=column_type) + + def get_table_structure(self, table_name): + if not self.connection: + self.connect() + try: + table_info = self.get_table_info(table_name) + with self.connection.cursor() as cursor: + # 使用 SHOW FULL COLUMNS FROM 获取包含注释的列信息 + cursor.execute(f"SHOW FULL COLUMNS FROM {table_name}") + columns = cursor.fetchall() + structure = [] + for column in columns: + column_info = { + "name": column[0], + "type": column[1], + "required": column[2] is not None, + "primary_key": column[4] == 'PRI', + # "default": column[4], + "description": column[8] # 注释信息在第 9 列(索引为 8) + } + column_info.update(self.parse_type_and_validation(column_info["type"])) # 添加解析后的类型和验证信息 + structure.append(column_info) + table_info['fields'] = structure + return table_info + except pymysql.Error as e: + print(f"获取表 {table_name} 结构失败: {e}") + return {'fields': []} + + +if __name__ == "__main__": + # 请根据实际情况修改数据库连接信息 + db_info = DatabaseInfo( + host="lt.330770.xyz", + port=3307, + user="root", + password="rap_sky", + database="rpa" + ) + tables = db_info.get_all_tables() + print("所有表名:") + for table in tables: + print(table) + tableName = table.get('TableName') + structure = db_info.get_table_structure(tableName) + print(f"表 {tableName} 的结构信息:") + for column in structure: + print(column) + db_info.disconnect() + \ No newline at end of file diff --git a/app/utils/event_task.py b/app/utils/event_task.py new file mode 100644 index 0000000..ae24057 --- /dev/null +++ b/app/utils/event_task.py @@ -0,0 +1,237 @@ +from typing import Dict, List, Callable, Any +from fastapi import BackgroundTasks + +import asyncio +import logging +from enum import Enum +import time + +logger = logging.getLogger(__name__) + +class EventPriority(Enum): + HIGH = 1 + MEDIUM = 2 + LOW = 3 + +class EventType: + # 同步星云联系人信息 + SYNC_XINGYUN_CONTACT_INFO = "sync_xingyun_contact_info" + # 绑定订单给用户 + BIND_ORDER_FOR_USER = "bind_order_for_user" + # 打开个人聊天会话 + OPEN_PERSONAL_CHAT = "open_personal_chat" + # 群聊变更 + GROUP_CHAT_UPDATED = "group_chat_updated" + # 检查创建群聊 + CHECK_CREATE_GROUP_CHAT = "check_create_group_chat" + # 客服领单 + CUSTOMER_SERVICE_ASSIGN_ORDER = "customer_service_assign_order" + # 分配设计师 + DESIGNER_ASSIGN_ORDER = "designer_assign_order" + # 客户复购 + OLD_CUSTOMER_REPEAT_PURCHASE = "old_customer_repeat_purchase" + # 客户退货 + CUSTOMER_RETURN_ORDER = "customer_return_order" + # 设计稿上传 + DESIGNER_UPLOAD_DESIGN = "designer_upload_design" + # 客户在新店铺下单 + CUSTOMER_ORDER_IN_NEW_SHOP = "customer_order_in_new_shop" + + +EventList = [ + { + "value": EventType.SYNC_XINGYUN_CONTACT_INFO, + "label": "同步星云联系人信息", + "description": "当星云联系人信息发生变化时触发", + "automation_event": False + }, + { + "value": EventType.BIND_ORDER_FOR_USER, + "label": "绑定订单", + "description": "当用户创建订单时触发", + "automation_event": True + }, + { + "value": EventType.OPEN_PERSONAL_CHAT, + "label": "打开个人聊天会话", + "description": "当用户创建个人聊天时触发", + "automation_event": False + }, + { + "value": EventType.GROUP_CHAT_UPDATED, + "label": "群聊变更", + "description": "当群聊信息发生变化时触发", + "automation_event": True + }, + { + "value": EventType.CHECK_CREATE_GROUP_CHAT, + "label": "群聊创建", + "description": "当用户请求创建群聊时触发", + "automation_event": True + }, + { + "value": EventType.CUSTOMER_SERVICE_ASSIGN_ORDER, + "label": "客服领单", + "description": "当客服领单时触发", + "automation_event": True + }, + { + "value": EventType.DESIGNER_ASSIGN_ORDER, + "label": "分配设计师", + "description": "当分配设计师时触发", + "automation_event": True + }, + { + "value": EventType.OLD_CUSTOMER_REPEAT_PURCHASE, + "label": "客户复购", + "description": "当老客复购时触发", + "automation_event": True + }, + { + "value": EventType.CUSTOMER_RETURN_ORDER, + "label": "客户退货", + "description": "当客户退货时触发", + "automation_event": True + }, + { + "value": EventType.DESIGNER_UPLOAD_DESIGN, + "label": "设计稿上传", + "description": "当设计师上传设计稿时触发", + "automation_event": True + }, + { + "value": EventType.CUSTOMER_ORDER_IN_NEW_SHOP, + "label": "客户在新店铺下单", + "description": "当客户在新店铺下单时触发", + "automation_event": True + }, +] + +class NonBlockingEventManager: + def __init__(self): + self.handlers: Dict[str, List[Callable]] = {} + self.priority_handlers: Dict[EventPriority, List[Callable]] = { + EventPriority.HIGH: [], + EventPriority.MEDIUM: [], + EventPriority.LOW: [] + } + self.pretasks: List[Callable] = [] + self.automation_event_handlers: Dict[str, Any] = [] + + def subscribe(self, event_type: str, handler: Callable, priority: EventPriority = EventPriority.MEDIUM, input_model: Any = None): + if event_type not in self.handlers: + self.handlers[event_type] = [] + + for event in EventList: + if event["value"] == event_type: + event["field_tree"] = input_model + self.automation_event_handlers.append(event) + + logger.info(f"Subscribe {event_type} with priority {priority}") + # print(f"Subscribe {event_type} with priority {priority}", flush=True) + self.handlers[event_type].append(handler) + + def subscribe_priority(self, handler: Callable, priority: EventPriority = EventPriority.MEDIUM): + """订阅所有事件类型的处理器,按优先级""" + self.priority_handlers[priority].append(handler) + + async def publish_async(self, event_type: str, data: Any, background_tasks: BackgroundTasks): + """异步发布事件,不阻塞主线程""" + # 添加到后台任务,立即返回 + background_tasks.add_task( + self._process_event_async, + event_type, + data + ) + + def register_pretask(self, pretask: Callable): + """注册预任务""" + self.pretasks.append(pretask) + + async def trigger_event(self, event_type: str, event_data: Any): + """解析事件数据""" + for pretask in self.pretasks: + await pretask(event_type, event_data) + + async def _process_event_async(self, event_type: str, data: Any): + """异步处理事件""" + start_time = time.time() + + # 触发事件系统 + try: + await self.trigger_event(event_type, data) + except Exception as e: + logger.exception(f'处理事件失败,{e}') + + # 处理优先级处理器(高优先级先执行) + for priority in [EventPriority.HIGH, EventPriority.MEDIUM, EventPriority.LOW]: + priority_tasks = [] + for handler in self.priority_handlers[priority]: + try: + if asyncio.iscoroutinefunction(handler): + task = handler(event_type, data) + else: + task = asyncio.to_thread(handler, event_type, data) + priority_tasks.append(task) + except Exception as e: + logger.error(f"Error in priority handler: {e}") + + if priority_tasks: + await asyncio.gather(*priority_tasks, return_exceptions=True) + + # 处理特定事件类型的处理器 + if event_type in self.handlers: + specific_tasks = [] + for handler in self.handlers[event_type]: + try: + if asyncio.iscoroutinefunction(handler): + task = handler(event_type, data) + else: + task = asyncio.to_thread(handler, event_type, data) + specific_tasks.append(task) + except Exception as e: + logging.error(f"Error in specific handler: {e}") + + if specific_tasks: + await asyncio.gather(*specific_tasks, return_exceptions=True) + + logging.info(f"Event {event_type} processed in {time.time() - start_time:.2f}s") + +# 创建全局事件管理器 +event_manager = NonBlockingEventManager() + +# # 定义事件处理器 +# async def log_event_handler(event_type: str, data: Any): +# await asyncio.sleep(0.1) # 模拟异步日志记录 +# logging.info(f"Logged event: {event_type}") + +# def update_cache_handler( Any): +# import time +# time.sleep(0.5) # 模拟缓存更新 +# logging.info(f"Cache updated with: {data}") + +# # 注册处理器 +# event_manager.subscribe_priority(log_event_handler, EventPriority.HIGH) +# event_manager.subscribe("order_created", update_cache_handler, EventPriority.MEDIUM) + +# @app.post("/orders") +# async def create_order_complete(order_ dict, background_tasks: BackgroundTasks): +# # 执行主要业务逻辑 +# order_id = f"order_{hash(str(order_data))}" +# order_data["order_id"] = order_id + +# # 保存到数据库 +# # await save_order_to_db(order_data) + +# # 立即返回响应 +# response = {"order_id": order_id, "status": "created"} + +# # 异步发布事件,不阻塞响应 +# await event_manager.publish_async("order_created", order_data, background_tasks) + +# return response + +# # 启动时配置 +# @app.on_event("startup") +# async def startup_event(): +# logging.basicConfig(level=logging.INFO) \ No newline at end of file diff --git a/app/utils/excel_utils.py b/app/utils/excel_utils.py new file mode 100644 index 0000000..40afaab --- /dev/null +++ b/app/utils/excel_utils.py @@ -0,0 +1,34 @@ +# excel_utils.py +import pandas as pd +from typing import Dict, List, Tuple + +def get_sheets_and_headers( + file_path: str, + header: int = 0, + skiprows: int = 0, + **kwargs +) -> Dict[str, List[str]]: + """ + 读取 Excel 文件,返回每个 sheet 的名称及其表头(列名)。 + + 参数: + file_path (str): Excel 文件路径(支持 .xlsx / .xls) + header (int): 表头所在行索引(默认 0,即第一行) + skiprows (int): 跳过的行数(在表头之前) + **kwargs: 透传给 pd.read_excel 的其他参数(如 engine 等) + + 返回: + Dict[str, List[str]]: {sheet_name: [col1, col2, ...]} + """ + # 使用 nrows=0 只读取表头,不加载数据,性能高 + all_sheets = pd.read_excel( + file_path, + sheet_name=None, # 读取所有 sheet + header=header, + skiprows=skiprows, + nrows=0, # ⚡ 关键:只读表头 + **kwargs + ) + return {name: df.columns.tolist() for name, df in all_sheets.items()} + + diff --git a/app/utils/jwt.py b/app/utils/jwt.py new file mode 100644 index 0000000..06b883d --- /dev/null +++ b/app/utils/jwt.py @@ -0,0 +1,10 @@ +import jwt + +from app.schemas.login import JWTPayload +from app.settings.config import settings + + +def create_access_token(*, data: JWTPayload): + payload = data.model_dump().copy() + encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM) + return encoded_jwt diff --git a/app/utils/password.py b/app/utils/password.py new file mode 100644 index 0000000..48f5dc8 --- /dev/null +++ b/app/utils/password.py @@ -0,0 +1,16 @@ +from passlib import pwd +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def generate_password() -> str: + return pwd.genword() diff --git a/app/utils/some_sdk b/app/utils/some_sdk new file mode 160000 index 0000000..2e8b24e --- /dev/null +++ b/app/utils/some_sdk @@ -0,0 +1 @@ +Subproject commit 2e8b24e3120638d92028a9fcd14e7761345a83c0 diff --git a/codegen/.env.development b/codegen/.env.development new file mode 100644 index 0000000..517d977 --- /dev/null +++ b/codegen/.env.development @@ -0,0 +1,9 @@ +# 资源公共路径,需要以 /开头和结尾 +VITE_PUBLIC_PATH = './' + +# 是否启用代理 +VITE_USE_PROXY = true + +# base api +VITE_BASE_API = '/api/v1' + diff --git a/codegen/.env.production b/codegen/.env.production new file mode 100644 index 0000000..30e61ef --- /dev/null +++ b/codegen/.env.production @@ -0,0 +1 @@ +VITE_BASE_API=. diff --git a/codegen/.eslintignore b/codegen/.eslintignore new file mode 100644 index 0000000..c40a313 --- /dev/null +++ b/codegen/.eslintignore @@ -0,0 +1,22 @@ +logs +*.log +*.sh +node_modules +.DS_Store +lib +*.md +*.scss +*.woff +*.ttf +.vscode +.idea +dist +mock +public +bin +build +config +index.html +components.d.ts +element.d.ts +src/assets diff --git a/codegen/.eslintrc.cjs b/codegen/.eslintrc.cjs new file mode 100644 index 0000000..6d6d9e0 --- /dev/null +++ b/codegen/.eslintrc.cjs @@ -0,0 +1,19 @@ +module.exports = { + root: true, + env: { + node: true + }, + extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'], + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + ecmaVersion: 2022, + ecmaFeatures: { + jsx: true + } + }, + rules: { + curly: 'error', // 控制语句需要大括号 + 'vue/multi-word-component-names': 'off' + } +} diff --git a/codegen/.gitignore b/codegen/.gitignore new file mode 100644 index 0000000..c6d8f03 --- /dev/null +++ b/codegen/.gitignore @@ -0,0 +1,31 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*-lock.yaml +*.lock +*-lock.json diff --git a/codegen/.npmrc b/codegen/.npmrc new file mode 100644 index 0000000..8418a0b --- /dev/null +++ b/codegen/.npmrc @@ -0,0 +1,2 @@ +# 使用pnpm,则需要 +shamefully-hoist=true diff --git a/codegen/.prettierrc.cjs b/codegen/.prettierrc.cjs new file mode 100644 index 0000000..39e530c --- /dev/null +++ b/codegen/.prettierrc.cjs @@ -0,0 +1,29 @@ +// 参考:https://prettier.io/docs/en/options.html +module.exports = { + // 超过指定值换行 + printWidth: 100, + // 制表符宽度 + tabWidth: 2, + // 使用制表符缩进 + // useTabs: true, + // 句尾不使用分号 + semi: false, + // 使用单引号 + singleQuote: true, + // 在 JSX 文件中使用单引号代替双引号 + jsxSingleQuote: true, + // 在对象或数组最后一个元素后面是否加逗号,可选值"[none:不追加 | es5:追加] + trailingComma: 'none', + // 在对象,数组括号与代码之间加空格 "{ foo: bar }" + bracketSpacing: true, + // 将多行 JSX 元素的 > 放置于最后一行的末尾,而非换行 + jsxBracketSameLine: true, + //(x) => {} 箭头函数参数只有一个时是否要有小括号,可选值"[always:(x) => x | avoid:x => x] + arrowParens: 'avoid', + // 使用默认的折行标准,可选值"[always|never|preserve]" + proseWrap: 'preserve', + // 指定HTML文件的全局空格敏感度,可选值"[css|strict|ignore]" + htmlWhitespaceSensitivity: 'css', + // 结尾时换行符,默认值lf,可选值"[auto|lf|crlf|cr]" + endOfLine: 'lf' +} diff --git a/codegen/README.md b/codegen/README.md new file mode 100644 index 0000000..e618e05 --- /dev/null +++ b/codegen/README.md @@ -0,0 +1,19 @@ +# maku-generator-ui + +## Project setup + +```bash +npm install +``` + +### Compiles and hot-reloads for development + +```bash +npm run dev +``` + +### Compiles and minifies for production + +```bash +npm run build +``` diff --git a/codegen/build/config/define.js b/codegen/build/config/define.js new file mode 100644 index 0000000..4b10543 --- /dev/null +++ b/codegen/build/config/define.js @@ -0,0 +1,13 @@ +import dayjs from 'dayjs' + +/** + * * 此处定义的是全局常量,启动或打包后将添加到window中 + * https://vitejs.cn/config/#define + */ + +// 项目构建时间 +const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss')) + +export const viteDefine = { + _BUILD_TIME_, +} diff --git a/codegen/build/config/index.js b/codegen/build/config/index.js new file mode 100644 index 0000000..066d06d --- /dev/null +++ b/codegen/build/config/index.js @@ -0,0 +1 @@ +export * from './define' diff --git a/codegen/build/constant.js b/codegen/build/constant.js new file mode 100644 index 0000000..cf58a73 --- /dev/null +++ b/codegen/build/constant.js @@ -0,0 +1,23 @@ +export const OUTPUT_DIR = 'dist' + +export const PROXY_CONFIG = { + // /** + // * @desc 替换匹配值 + // * @请求路径 http://localhost:3100/api/user + // * @转发路径 http://localhost:9999/api/v1 +/user + // */ + // '/api': { + // target: 'http://localhost:9999/api/v1', + // changeOrigin: true, + // rewrite: (path) => path.replace(new RegExp('^/api'), ''), + // }, + /** + * @desc 不替换匹配值 + * @请求路径 http://localhost:3100/api/v1/user + * @转发路径 http://localhost:9999/api/v1/user + */ + '/api/v1': { + target: 'http://127.0.0.1:9999', + changeOrigin: true, + }, +} diff --git a/codegen/build/plugin/html.js b/codegen/build/plugin/html.js new file mode 100644 index 0000000..7ed7f76 --- /dev/null +++ b/codegen/build/plugin/html.js @@ -0,0 +1,15 @@ +import { createHtmlPlugin } from 'vite-plugin-html' + +export function configHtmlPlugin(viteEnv, isBuild) { + const { VITE_TITLE } = viteEnv + + const htmlPlugin = createHtmlPlugin({ + minify: isBuild, + inject: { + data: { + title: VITE_TITLE, + }, + }, + }) + return htmlPlugin +} diff --git a/codegen/build/plugin/index.js b/codegen/build/plugin/index.js new file mode 100644 index 0000000..a80b886 --- /dev/null +++ b/codegen/build/plugin/index.js @@ -0,0 +1,41 @@ +import vue from '@vitejs/plugin-vue' + +/** + * * unocss插件,原子css + * https://github.com/antfu/unocss + */ +// import Unocss from 'unocss/vite' + +// // rollup打包分析插件 +// import visualizer from 'rollup-plugin-visualizer' +// // 压缩 +// import viteCompression from 'vite-plugin-compression' + +// import { configHtmlPlugin } from './html' +// import unplugin from './unplugin' + +export function createVitePlugins(viteEnv, isBuild) { + const plugins = [ + vue(), + createSvgIconsPlugin({ + iconDirs: [resolve(__dirname, 'src/icons/svg')], + symbolId: 'icon-[dir]-[name]' + }) + ] + + if (viteEnv.VITE_USE_COMPRESS) { + plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' })) + } + + if (isBuild) { + plugins.push( + visualizer({ + open: true, + gzipSize: true, + brotliSize: true + }) + ) + } + + return plugins +} diff --git a/codegen/build/plugin/unplugin.js b/codegen/build/plugin/unplugin.js new file mode 100644 index 0000000..3277682 --- /dev/null +++ b/codegen/build/plugin/unplugin.js @@ -0,0 +1,46 @@ +import { resolve } from 'path' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' +import { FileSystemIconLoader } from 'unplugin-icons/loaders' +import IconsResolver from 'unplugin-icons/resolver' + +/** + * * unplugin-icons插件,自动引入iconify图标 + * usage: https://github.com/antfu/unplugin-icons + * 图标库: https://icones.js.org/ + */ +import Icons from 'unplugin-icons/vite' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' + +import { getSrcPath } from '../utils' + +const customIconPath = resolve(getSrcPath(), 'assets/svg') + +export default [ + AutoImport({ + imports: ['vue', 'vue-router'], + dts: false, + }), + Icons({ + compiler: 'vue3', + customCollections: { + custom: FileSystemIconLoader(customIconPath), + }, + scale: 1, + defaultClass: 'inline-block', + }), + Components({ + resolvers: [ + NaiveUiResolver(), + IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }), + ], + dts: false, + }), + createSvgIconsPlugin({ + iconDirs: [customIconPath], + symbolId: 'icon-custom-[dir]-[name]', + inject: 'body-last', + customDomId: '__CUSTOM_SVG_ICON__', + }), +] diff --git a/codegen/build/script/build-cname.js b/codegen/build/script/build-cname.js new file mode 100644 index 0000000..7658c98 --- /dev/null +++ b/codegen/build/script/build-cname.js @@ -0,0 +1,15 @@ +import { resolve } from 'path' +import chalk from 'chalk' +import { writeFileSync } from 'fs-extra' +import { OUTPUT_DIR } from '../constant' +import { getEnvConfig, getRootPath } from '../utils' + +export function runBuildCNAME() { + const { VITE_CNAME } = getEnvConfig() + if (!VITE_CNAME) return + try { + writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME) + } catch (error) { + console.log(chalk.red('CNAME file failed to package:\n' + error)) + } +} diff --git a/codegen/build/script/index.js b/codegen/build/script/index.js new file mode 100644 index 0000000..e0efeb0 --- /dev/null +++ b/codegen/build/script/index.js @@ -0,0 +1,14 @@ +import chalk from 'chalk' +import { runBuildCNAME } from './build-cname' + +export const runBuild = async () => { + try { + runBuildCNAME() + console.log(`✨ ${chalk.cyan('build successfully!')}`) + } catch (error) { + console.log(chalk.red('vite build error:\n' + error)) + process.exit(1) + } +} + +runBuild() diff --git a/codegen/build/utils.js b/codegen/build/utils.js new file mode 100644 index 0000000..fe43a2b --- /dev/null +++ b/codegen/build/utils.js @@ -0,0 +1,70 @@ +import fs from 'fs' +import path from 'path' +import dotenv from 'dotenv' + +/** + * * 项目根路径 + * @descrition 结尾不带/ + */ +export function getRootPath() { + return path.resolve(process.cwd()) +} + +/** + * * 项目src路径 + * @param srcName src目录名称(默认: "src") + * @descrition 结尾不带斜杠 + */ +export function getSrcPath(srcName = 'src') { + return path.resolve(getRootPath(), srcName) +} + +export function convertEnv(envOptions) { + const result = {} + if (!envOptions) return result + + for (const envKey in envOptions) { + let envVal = envOptions[envKey] + if (['true', 'false'].includes(envVal)) envVal = envVal === 'true' + + if (['VITE_PORT'].includes(envKey)) envVal = +envVal + + result[envKey] = envVal + } + return result +} + +/** + * 获取当前环境下生效的配置文件名 + */ +function getConfFiles() { + const script = process.env.npm_lifecycle_script + const reg = new RegExp('--mode ([a-z_\\d]+)') + const result = reg.exec(script) + if (result) { + const mode = result[1] + return ['.env', '.env.local', `.env.${mode}`] + } + return ['.env', '.env.local', '.env.production'] +} + +export function getEnvConfig(match = 'VITE_', confFiles = getConfFiles()) { + let envConfig = {} + confFiles.forEach((item) => { + try { + if (fs.existsSync(path.resolve(process.cwd(), item))) { + const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item))) + envConfig = { ...envConfig, ...env } + } + } catch (e) { + console.error(`Error in parsing ${item}`, e) + } + }) + const reg = new RegExp(`^(${match})`) + Object.keys(envConfig).forEach((key) => { + if (!reg.test(key)) { + Reflect.deleteProperty(envConfig, key) + } + }) + return envConfig +} diff --git a/codegen/components.d.ts b/codegen/components.d.ts new file mode 100644 index 0000000..25dce96 --- /dev/null +++ b/codegen/components.d.ts @@ -0,0 +1,35 @@ +// generated by unplugin-vue-components +// We suggest you to commit this file into source control +// Read more: https://github.com/vuejs/core/pull/3399 +import '@vue/runtime-core' + +declare module '@vue/runtime-core' { + export interface GlobalComponents { + ElAside: (typeof import('element-plus/es'))['ElAside'] + ElAvatar: (typeof import('element-plus/es'))['ElAvatar'] + ElButton: (typeof import('element-plus/es'))['ElButton'] + ElCard: (typeof import('element-plus/es'))['ElCard'] + ElConfigProvider: (typeof import('element-plus/es'))['ElConfigProvider'] + ElContainer: (typeof import('element-plus/es'))['ElContainer'] + ElDropdown: (typeof import('element-plus/es'))['ElDropdown'] + ElDropdownItem: (typeof import('element-plus/es'))['ElDropdownItem'] + ElDropdownMenu: (typeof import('element-plus/es'))['ElDropdownMenu'] + ElHeader: (typeof import('element-plus/es'))['ElHeader'] + ElIcon: (typeof import('element-plus/es'))['ElIcon'] + ElImage: (typeof import('element-plus/es'))['ElImage'] + ElLink: (typeof import('element-plus/es'))['ElLink'] + ElMain: (typeof import('element-plus/es'))['ElMain'] + ElMenu: (typeof import('element-plus/es'))['ElMenu'] + ElMenuItem: (typeof import('element-plus/es'))['ElMenuItem'] + ElScrollbar: (typeof import('element-plus/es'))['ElScrollbar'] + ElSubMenu: (typeof import('element-plus/es'))['ElSubMenu'] + ElTabPane: (typeof import('element-plus/es'))['ElTabPane'] + ElTabs: (typeof import('element-plus/es'))['ElTabs'] + Lang: (typeof import('./src/components/Lang/index.vue'))['default'] + RouterLink: (typeof import('vue-router'))['RouterLink'] + RouterView: (typeof import('vue-router'))['RouterView'] + SvgIcon: (typeof import('./src/components/SvgIcon/index.vue'))['default'] + } +} + +export {} diff --git a/codegen/index.html b/codegen/index.html new file mode 100644 index 0000000..25a8129 --- /dev/null +++ b/codegen/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + +
+ + + diff --git a/codegen/package.json b/codegen/package.json new file mode 100644 index 0000000..fa17aa0 --- /dev/null +++ b/codegen/package.json @@ -0,0 +1,60 @@ +{ + "name": "maku-generator-ui", + "version": "3.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint --fix --ext .vue,.jsx,.ts,.tsx ." + }, + "dependencies": { + "@codemirror/lang-javascript": "^6.2.1", + "@element-plus/icons-vue": "2.3.1", + "@vueuse/core": "9.1.1", + "axios": "1.6.0", + "codemirror": "^6.0.1", + "crypto-js": "^4.1.1", + "element-plus": "2.7.6", + "qs": "6.10.3", + "sortablejs": "^1.15.0", + "vue": "3.4.30", + "vue-codemirror": "^6.1.1", + "vue-router": "4.2.5", + "vxe-table": "^4.3.5", + "xe-utils": "^3.5.4" + }, + "devDependencies": { + "@babel/types": "7.24.0", + "@types/node": "20.11.28", + "@types/nprogress": "0.2.3", + "@types/qs": "6.9.12", + "@types/sm-crypto": "0.3.4", + "@types/sortablejs": "1.15.8", + "@vitejs/plugin-vue": "5.0.5", + "@vue/compiler-sfc": "3.4.30", + "@vue/eslint-config-prettier": "9.0.0", + "@vue/eslint-config-typescript": "13.0.0", + "@vue/tsconfig": "0.5.1", + "dotenv": "^16.5.0", + "eslint": "8.57.0", + "eslint-plugin-vue": "9.23.0", + "prettier": "3.2.5", + "sass": "1.72.0", + "typescript": "5.4.2", + "vite": "5.3.1", + "vite-plugin-svg-icons": "2.0.1", + "vite-plugin-vue-setup-extend": "0.4.0", + "vue-tsc": "2.0.22" + }, + "keywords": [ + "vue", + "vue3", + "vuejs", + "vite", + "element-plus", + "generator", + "maku-generator" + ], + "license": "MIT" +} diff --git a/codegen/public/favicon.ico b/codegen/public/favicon.ico new file mode 100644 index 0000000..8d5d381 Binary files /dev/null and b/codegen/public/favicon.ico differ diff --git a/codegen/src/App.vue b/codegen/src/App.vue new file mode 100644 index 0000000..2a6089c --- /dev/null +++ b/codegen/src/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/codegen/src/api/baseClass.ts b/codegen/src/api/baseClass.ts new file mode 100644 index 0000000..ff4abd8 --- /dev/null +++ b/codegen/src/api/baseClass.ts @@ -0,0 +1,17 @@ +import service from '@/utils/request' + +export const useBaseClassApi = (id: Number) => { + return service.get('/gen/baseclass/' + id) +} + +export const useBaseClassListApi = () => { + return service.get('/gen/baseclass/list') +} + +export const useBaseClassSubmitApi = (dataForm: any) => { + if (dataForm.id) { + return service.put('/gen/baseclass', dataForm) + } else { + return service.post('/gen/baseclass', dataForm) + } +} diff --git a/codegen/src/api/datasource.ts b/codegen/src/api/datasource.ts new file mode 100644 index 0000000..4612d6b --- /dev/null +++ b/codegen/src/api/datasource.ts @@ -0,0 +1,25 @@ +import service from '@/utils/request' + +export const useDataSourceTestApi = (id: Number) => { + return service.get('/gen/datasource/test/' + id) +} + +export const useDataSourceApi = (id: Number) => { + return service.get('/gen/datasource/' + id) +} + +export const useDataSourceListApi = () => { + return service.get('/gen/datasource/list') +} + +export const useDataSourceSubmitApi = (dataForm: any) => { + if (dataForm.id) { + return service.put('/gen/datasource', dataForm) + } else { + return service.post('/gen/datasource', dataForm) + } +} + +export const useDataSourceTableListApi = (id: string, tableName: string) => { + return service.get(`/gen/datasource/get/${id}?tableName=${tableName}`) +} diff --git a/codegen/src/api/fieldType.ts b/codegen/src/api/fieldType.ts new file mode 100644 index 0000000..415752b --- /dev/null +++ b/codegen/src/api/fieldType.ts @@ -0,0 +1,17 @@ +import service from '@/utils/request' + +export const useFieldTypeApi = (id: Number) => { + return service.get('/gen/fieldtype/' + id) +} + +export const useFieldTypeListApi = () => { + return service.get('/gen/fieldtype/list') +} + +export const useFieldTypeSubmitApi = (dataForm: any) => { + if (dataForm.id) { + return service.put('/gen/fieldtype', dataForm) + } else { + return service.post('/gen/fieldtype', dataForm) + } +} diff --git a/codegen/src/api/generator.ts b/codegen/src/api/generator.ts new file mode 100644 index 0000000..be1b4b8 --- /dev/null +++ b/codegen/src/api/generator.ts @@ -0,0 +1,17 @@ +import service from '@/utils/request' + +// 生成代码(zip压缩包) +export const useDownloadApi = (tableIds: any[]) => { + location.href = + import.meta.env.VITE_BASE_API + '/gen/generator/download?tableIds=' + tableIds.join(',') +} + +// 生成代码(自定义目录) +export const useGeneratorApi = (tableIds: any[]) => { + return service.post(`/gen/generator/code`, tableIds) +} + +//代码预览 +export const usePreviewApi = (tableId: any) => { + return service.get(`/gen/table/preview/${tableId}`) +} diff --git a/codegen/src/api/project.ts b/codegen/src/api/project.ts new file mode 100644 index 0000000..3f3907f --- /dev/null +++ b/codegen/src/api/project.ts @@ -0,0 +1,18 @@ +import service from '@/utils/request' + +export const useProjectApi = (id: number) => { + return service.get('/gen/project/' + id) +} + +export const useProjectSubmitApi = (dataForm: any) => { + if (dataForm.id) { + return service.put('/gen/project', dataForm) + } else { + return service.post('/gen/project', dataForm) + } +} + +// 源码下载 +export const useSourceDownloadApi = (id: string) => { + location.href = import.meta.env.VITE_BASE_API + '/gen/project/download/' + id +} diff --git a/codegen/src/api/table.ts b/codegen/src/api/table.ts new file mode 100644 index 0000000..b560cdf --- /dev/null +++ b/codegen/src/api/table.ts @@ -0,0 +1,21 @@ +import service from '@/utils/request' + +export const useTableApi = (id: number) => { + return service.get('/gen/table/' + id) +} + +export const useTableSubmitApi = (dataForm: any) => { + return service.put('/gen/table', dataForm) +} + +export const useTableImportSubmitApi = (datasourceId: string, tableNameList: any) => { + return service.post('/gen/table/import/' + datasourceId, tableNameList) +} + +export const useTableFieldSubmitApi = (tableId: number, fieldList: any) => { + return service.put('/gen/table/field/' + tableId, fieldList) +} + +export const useTableSyncApi = (id: number) => { + return service.post('/gen/table/sync/' + id) +} diff --git a/codegen/src/assets/404.png b/codegen/src/assets/404.png new file mode 100644 index 0000000..520dcc1 Binary files /dev/null and b/codegen/src/assets/404.png differ diff --git a/codegen/src/components/codemirror/CodeMirror.vue b/codegen/src/components/codemirror/CodeMirror.vue new file mode 100644 index 0000000..ccea363 --- /dev/null +++ b/codegen/src/components/codemirror/CodeMirror.vue @@ -0,0 +1,56 @@ + + + diff --git a/codegen/src/components/svg-icon/index.ts b/codegen/src/components/svg-icon/index.ts new file mode 100644 index 0000000..4bef962 --- /dev/null +++ b/codegen/src/components/svg-icon/index.ts @@ -0,0 +1,4 @@ +import { withInstall } from '@/utils/tool' +import SvgIcon from './src/svg-icon.vue' + +export default withInstall(SvgIcon) diff --git a/codegen/src/components/svg-icon/src/svg-icon.vue b/codegen/src/components/svg-icon/src/svg-icon.vue new file mode 100644 index 0000000..ba62e1d --- /dev/null +++ b/codegen/src/components/svg-icon/src/svg-icon.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/codegen/src/hooks/index.ts b/codegen/src/hooks/index.ts new file mode 100644 index 0000000..27b5ba0 --- /dev/null +++ b/codegen/src/hooks/index.ts @@ -0,0 +1,164 @@ +import { IHooksOptions } from '@/hooks/interface' +import service from '@/utils/request' +import { onMounted } from 'vue' +import { ElMessage, ElMessageBox } from 'element-plus' + +export const useCrud = (options: IHooksOptions) => { + const defaultOptions: IHooksOptions = { + createdIsNeed: true, + dataListUrl: '', + isPage: true, + deleteUrl: '', + primaryKey: 'id', + exportUrl: '', + queryForm: {}, + dataList: [], + order: '', + asc: false, + page: 1, + limit: 10, + total: 0, + pageSizes: [1, 10, 20, 50, 100, 200], + dataListLoading: false, + dataListSelections: [] + } + + const mergeDefaultOptions = (options: any, props: any): IHooksOptions => { + for (const key in options) { + if (!Object.getOwnPropertyDescriptor(props, key)) { + props[key] = options[key] + } + } + return props + } + + // 覆盖默认值 + const state = mergeDefaultOptions(defaultOptions, options) + + onMounted(() => { + if (state.createdIsNeed) { + query() + } + }) + + const query = () => { + if (!state.dataListUrl) { + return + } + + state.dataListLoading = true + + service + .get(state.dataListUrl, { + params: { + order: state.order, + asc: state.asc, + page: state.isPage ? state.page : null, + limit: state.isPage ? state.limit : null, + ...state.queryForm + } + }) + .then((res: any) => { + state.dataList = state.isPage ? res.data.list : res.data + state.total = state.isPage ? res.data.total : 0 + }) + + state.dataListLoading = false + } + + const getDataList = () => { + state.page = 1 + query() + } + + const sizeChangeHandle = (val: number) => { + state.page = 1 + state.limit = val + query() + } + + const currentChangeHandle = (val: number) => { + state.page = val + query() + } + + // 多选 + const selectionChangeHandle = (selections: any[]) => { + state.dataListSelections = selections.map( + (item: any) => state.primaryKey && item[state.primaryKey] + ) + } + + // 排序 + const sortChangeHandle = (data: any) => { + const { prop, order } = data + + if (prop && order) { + state.order = prop + state.asc = order === 'ascending' + } else { + state.order = '' + } + + query() + } + + const deleteHandle = (key: number | string) => { + if (!state.deleteUrl) { + return + } + + ElMessageBox.confirm('确定进行删除操作?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }) + .then(() => { + service.delete(state.deleteUrl + '/' + key).then(() => { + ElMessage.success('删除成功') + + query() + }) + }) + .catch(() => {}) + } + + const deleteBatchHandle = (key?: number | string) => { + let data: any[] = [] + if (key) { + data = [key] + } else { + data = state.dataListSelections ? state.dataListSelections : [] + + if (data.length === 0) { + ElMessage.warning('请选择删除记录') + return + } + } + + ElMessageBox.confirm('确定进行删除操作?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }) + .then(() => { + if (state.deleteUrl) { + service.delete(state.deleteUrl, { data: { data } }).then(() => { + ElMessage.success('删除成功') + query() + }) + } + }) + .catch(() => {}) + } + + return { + getDataList, + sizeChangeHandle, + currentChangeHandle, + selectionChangeHandle, + sortChangeHandle, + deleteHandle, + deleteBatchHandle + } +} diff --git a/codegen/src/hooks/interface.ts b/codegen/src/hooks/interface.ts new file mode 100644 index 0000000..7ecd539 --- /dev/null +++ b/codegen/src/hooks/interface.ts @@ -0,0 +1,33 @@ +export interface IHooksOptions { + // 否在创建页面时,调用数据列表接口 + createdIsNeed?: boolean + // 数据列表 Url + dataListUrl?: string + // 是否需要分页 + isPage?: boolean + // 删除 Url + deleteUrl?: string + // 主键key,用于删除场景 + primaryKey?: string + // 导出 Url + exportUrl?: string + // 查询条件 + queryForm?: any + // 数据列表 + dataList?: any[] + // 排序字段 + order?: string + // 是否升序 + asc?: boolean + // 当前页码 + page?: number + // 每页数 + limit?: number + // 总条数 + total?: number + pageSizes?: any[] + // 数据列表,loading状态 + dataListLoading?: boolean + // 数据列表,多选项 + dataListSelections?: any[] +} diff --git a/codegen/src/icons/iconfont/iconfont.js b/codegen/src/icons/iconfont/iconfont.js new file mode 100644 index 0000000..8e4f347 --- /dev/null +++ b/codegen/src/icons/iconfont/iconfont.js @@ -0,0 +1,63 @@ +!(function (c) { + var h, + l, + v, + o, + i, + a = + '', + t = (t = document.getElementsByTagName('script'))[t.length - 1].getAttribute('data-injectcss'), + z = function (c, h) { + h.parentNode.insertBefore(c, h) + } + if (t && !c.__iconfont__svg__cssinject__) { + c.__iconfont__svg__cssinject__ = !0 + try { + document.write( + '' + ) + } catch (c) { + console && console.log(c) + } + } + function s() { + i || ((i = !0), v()) + } + function p() { + try { + o.documentElement.doScroll('left') + } catch (c) { + return void setTimeout(p, 50) + } + s() + } + ;(h = function () { + var c, + h = document.createElement('div') + ;(h.innerHTML = a), + (a = null), + (h = h.getElementsByTagName('svg')[0]) && + (h.setAttribute('aria-hidden', 'true'), + (h.style.position = 'absolute'), + (h.style.width = 0), + (h.style.height = 0), + (h.style.overflow = 'hidden'), + (h = h), + (c = document.body).firstChild ? z(h, c.firstChild) : c.appendChild(h)) + }), + document.addEventListener + ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState) + ? setTimeout(h, 0) + : ((l = function () { + document.removeEventListener('DOMContentLoaded', l, !1), h() + }), + document.addEventListener('DOMContentLoaded', l, !1)) + : document.attachEvent && + ((v = h), + (o = c.document), + (i = !1), + p(), + (o.onreadystatechange = function () { + 'complete' == o.readyState && ((o.onreadystatechange = null), s()) + })) +})(window) diff --git a/codegen/src/icons/svg/404.svg b/codegen/src/icons/svg/404.svg new file mode 100644 index 0000000..6df5019 --- /dev/null +++ b/codegen/src/icons/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/codegen/src/icons/svg/gitee.svg b/codegen/src/icons/svg/gitee.svg new file mode 100644 index 0000000..7d3e6e1 --- /dev/null +++ b/codegen/src/icons/svg/gitee.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/codegen/src/layout/components/Navbar/components/Logo.vue b/codegen/src/layout/components/Navbar/components/Logo.vue new file mode 100644 index 0000000..c7cbf2f --- /dev/null +++ b/codegen/src/layout/components/Navbar/components/Logo.vue @@ -0,0 +1,27 @@ + + + diff --git a/codegen/src/layout/components/Navbar/index.vue b/codegen/src/layout/components/Navbar/index.vue new file mode 100644 index 0000000..dbee1b7 --- /dev/null +++ b/codegen/src/layout/components/Navbar/index.vue @@ -0,0 +1,64 @@ + + + + diff --git a/codegen/src/layout/components/Router/Redirect.vue b/codegen/src/layout/components/Router/Redirect.vue new file mode 100644 index 0000000..9aa6c8b --- /dev/null +++ b/codegen/src/layout/components/Router/Redirect.vue @@ -0,0 +1,18 @@ + diff --git a/codegen/src/layout/components/Sidebar/components/Menu.vue b/codegen/src/layout/components/Sidebar/components/Menu.vue new file mode 100644 index 0000000..5e50882 --- /dev/null +++ b/codegen/src/layout/components/Sidebar/components/Menu.vue @@ -0,0 +1,27 @@ + + + diff --git a/codegen/src/layout/components/Sidebar/components/MenuItem.vue b/codegen/src/layout/components/Sidebar/components/MenuItem.vue new file mode 100644 index 0000000..6ccc64d --- /dev/null +++ b/codegen/src/layout/components/Sidebar/components/MenuItem.vue @@ -0,0 +1,24 @@ + + + diff --git a/codegen/src/layout/components/Sidebar/index.vue b/codegen/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..ba6e875 --- /dev/null +++ b/codegen/src/layout/components/Sidebar/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/codegen/src/layout/index.vue b/codegen/src/layout/index.vue new file mode 100644 index 0000000..73c2205 --- /dev/null +++ b/codegen/src/layout/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/codegen/src/main.ts b/codegen/src/main.ts new file mode 100644 index 0000000..d08c5d4 --- /dev/null +++ b/codegen/src/main.ts @@ -0,0 +1,28 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import App from './App.vue' +import { router } from './router' +import 'virtual:svg-icons-register' +import 'xe-utils' +import VXETable from 'vxe-table' +import SvgIcon from '@/components/svg-icon' +import 'vxe-table/lib/style.css' + +import '@/icons/iconfont/iconfont' +import 'element-plus/dist/index.css' +import '@/styles/index.scss' + +VXETable.setup({ + zIndex: 3000, + select: { + transfer: true + } +}) + +const app = createApp(App) + +app.use(router) +app.use(SvgIcon) +app.use(ElementPlus) +app.use(VXETable) +app.mount('#app') diff --git a/codegen/src/router/index.ts b/codegen/src/router/index.ts new file mode 100644 index 0000000..72d5ed8 --- /dev/null +++ b/codegen/src/router/index.ts @@ -0,0 +1,90 @@ +import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' + +export const menuRoutes: RouteRecordRaw[] = [ + { + path: '/p/gen', + meta: { + title: '代码生成器', + icon: 'icon-appstore' + }, + children: [ + { + path: '/gen/generator', + name: 'Generator', + component: () => import('../views/generator/index.vue'), + meta: { + title: '代码生成', + icon: 'icon-fire' + } + }, + { + path: '/gen/datasource', + name: 'DataSource', + component: () => import('../views/datasource/index.vue'), + meta: { + title: '数据源管理', + icon: 'icon-database-fill' + } + }, + { + path: '/gen/field-type', + name: 'FieldType', + component: () => import('../views/field-type/index.vue'), + meta: { + title: '字段类型映射', + icon: 'icon-menu' + } + }, + { + path: '/gen/base-class', + name: 'BaseClass', + component: () => import('../views/base-class/index.vue'), + meta: { + title: '基类管理', + icon: 'icon-cluster' + } + }, + { + path: '/gen/project', + name: 'ProjectIndex', + component: () => import('../views/project/index.vue'), + meta: { + title: '项目名变更', + icon: 'icon-edit-square' + } + } + ] + } +] + +export const constantRoutes: RouteRecordRaw[] = [ + { + path: '/redirect', + component: () => import('../layout/index.vue'), + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('../layout/components/Router/Redirect.vue') + } + ] + }, + { + path: '/', + component: () => import('../layout/index.vue'), + redirect: '/gen/generator', + children: [...menuRoutes] + }, + { + path: '/404', + component: () => import('../views/404.vue') + }, + { + path: '/:pathMatch(.*)', + redirect: '/404' + } +] + +export const router = createRouter({ + history: createWebHashHistory(), + routes: constantRoutes +}) diff --git a/codegen/src/styles/element.scss b/codegen/src/styles/element.scss new file mode 100644 index 0000000..734eb8b --- /dev/null +++ b/codegen/src/styles/element.scss @@ -0,0 +1,96 @@ +// cover some element-plus styles + +.el-menu { + border-right: none !important; + &:not(.el-menu--collapse) { + width: var(--theme-aside-width); + } +} + +.el-menu-item, +.el-sub-menu__title { + color: var(--theme-menu-text-color) !important; + height: 48px !important; + overflow: hidden; +} + +.el-menu.el-menu--horizontal { + border-bottom: none !important; +} + +.el-menu-item { + height: 45px !important; + line-height: 45px !important; +} + +.el-menu-item a, +.el-menu-item a:hover, +.el-menu-item i, +.el-sub-menu__title i { + color: inherit; + text-decoration: none; +} + +.el-menu-item .svg-icon, +.el-sub-menu .svg-icon { + font-size: 16px !important; + display: inline-block; + vertical-align: middle; + margin-right: 5px; + width: 24px; + text-align: center; +} + +.el-sub-menu.is-active > :first-child { + overflow: hidden; + color: var(--theme-menu-hover-color) !important; +} + +.el-menu-item.is-active { + border-right: 2px solid var(--theme-menu-border-color); + position: relative; + right: 1px; + overflow: hidden; + color: var(--theme-menu-hover-color) !important; + background-color: var(--theme-menu-hover-bg-color) !important; + &:hover { + background-color: var(--theme-menu-hover-bg-color) !important; + } +} + +.el-sub-menu.is-active.is-opened { + .el-sub-menu__title { + background-color: unset !important; + color: var(--theme-menu-hover-color); + } +} + +// 鼠标 hover 时 +.el-menu-item:hover, +.el-sub-menu__title:hover { + background-color: unset !important; + color: var(--theme-menu-hover-color) !important; +} + +.el-pagination { + margin-top: 15px; + justify-content: flex-end; +} + +.el-form--inline .el-form-item { + margin-right: 16px !important; +} + +.el-drawer .el-drawer__header { + display: flex; + align-items: center; + height: var(--theme-header-height); + padding: 0 15px; + margin-bottom: 0 !important; + font-size: 15px; + border-bottom: 1px solid var(--el-border-color); + color: var(--el-text-color-primary); +} +.el-card.is-always-shadow { + box-shadow: none !important; +} diff --git a/codegen/src/styles/index.scss b/codegen/src/styles/index.scss new file mode 100644 index 0000000..ac57ebe --- /dev/null +++ b/codegen/src/styles/index.scss @@ -0,0 +1,3 @@ +@import './variables.scss'; +@import './layout.scss'; +@import './element.scss'; diff --git a/codegen/src/styles/layout.scss b/codegen/src/styles/layout.scss new file mode 100644 index 0000000..ea5bce9 --- /dev/null +++ b/codegen/src/styles/layout.scss @@ -0,0 +1,68 @@ +* { + margin: 0; + padding: 0; + outline: none !important; +} + +html, +body, +#app { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', + '微软雅黑', Arial, sans-serif; + font-weight: 400; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: transparent; + font-size: 14px; + overflow: hidden; + position: relative; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +.layout-container { + width: 100%; + height: 100%; + .layout-sidebar { + background: var(--theme-menu-bg-color); + border-right: var(--theme-border-color-light) 1px solid; + height: inherit; + width: var(--theme-aside-width) !important; + position: relative; + display: flex; + flex-direction: column; + overflow-x: hidden !important; + .el-scrollbar__view { + overflow: hidden; + } + } + .layout-header { + padding: 0 !important; + display: flex; + flex-direction: column; + width: 100%; + height: var(--theme-header-height); + } + .layout-main { + padding: 0 !important; + overflow: hidden; + width: 100%; + background-color: var(--theme-main-bg-color); + .layout-card { + min-height: calc(100vh - 30px - var(--theme-header-height)); + } + } + .layout-scrollbar { + height: calc(100vh - 30px - var(--theme-header-height)); + padding: 15px; + } +} diff --git a/codegen/src/styles/variables.scss b/codegen/src/styles/variables.scss new file mode 100644 index 0000000..1dc218d --- /dev/null +++ b/codegen/src/styles/variables.scss @@ -0,0 +1,20 @@ +:root { + --el-color-primary: #1e97f8; + + --theme-aside-width: 200px; + --theme-header-height: 50px; + --theme-border-color-light: #f1f2f3; + --theme-logo-text-color: #ffffff; + + --theme-menu-text-color: #333333; + --theme-menu-bg-color: #ffffff; + --theme-menu-border-color: var(--el-color-primary); + --theme-menu-hover-color: var(--el-color-primary); + --theme-menu-hover-bg-color: var(--el-color-primary-light-9); + + --theme-header-bg-color: var(--el-color-primary); + --theme-header-text-color: #ffffff; + --theme-header-hover-color: rgba(0, 0, 0, 0.1); + + --theme-main-bg-color: #f0f2f5; +} diff --git a/codegen/src/types/shims.d.ts b/codegen/src/types/shims.d.ts new file mode 100644 index 0000000..1e8df5d --- /dev/null +++ b/codegen/src/types/shims.d.ts @@ -0,0 +1,13 @@ +declare module '*.svg' +declare module '*.png' +declare module '*.jpg' +declare module '*.gif' +declare module '*.scss' +declare module '*.ts' +declare module '*.js' + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/codegen/src/utils/request.ts b/codegen/src/utils/request.ts new file mode 100644 index 0000000..1aea9c5 --- /dev/null +++ b/codegen/src/utils/request.ts @@ -0,0 +1,59 @@ +import axios from 'axios' +import qs from 'qs' +import { ElMessage } from 'element-plus' +import { getToken } from './token' + +// axios实例 +const service = axios.create({ + baseURL: import.meta.env.VITE_BASE_API as any, + timeout: 60000, + headers: { 'Content-Type': 'application/json;charset=UTF-8' } +}) + +// 请求拦截器 +service.interceptors.request.use( + (config: any) => { + // 追加时间戳,防止GET请求缓存 + if (config.method?.toUpperCase() === 'GET') { + config.params = { ...config.params, t: new Date().getTime() } + } + + config.headers.token = config.headers.token || getToken() + + if (Object.values(config.headers).includes('application/x-www-form-urlencoded')) { + config.data = qs.stringify(config.data) + } + + return config + }, + error => { + return Promise.reject(error) + } +) + +// 响应拦截器 +service.interceptors.response.use( + response => { + if (response.status !== 200) { + return Promise.reject(new Error(response.statusText || 'Error')) + } + + const res = response.data + // 响应成功 + if (res.code === 200) { + return res + } + + // 错误提示 + ElMessage.error(res.msg) + + return Promise.reject(new Error(res.msg || 'Error')) + }, + error => { + ElMessage.error(error.message) + return Promise.reject(error) + } +) + +// 导出 axios 实例 +export default service diff --git a/codegen/src/utils/token.js b/codegen/src/utils/token.js new file mode 100644 index 0000000..7f85fef --- /dev/null +++ b/codegen/src/utils/token.js @@ -0,0 +1,30 @@ +const TOKEN_CODE = 'access_token' + +export function getToken() { + return 'dev' + return localStorage.getItem(TOKEN_CODE) +} + +export function setToken(token) { + localStorage.setItem(TOKEN_CODE, token) +} + +export function removeToken() { + localStorage.removeItem(TOKEN_CODE) +} + +// export async function refreshAccessToken() { +// const tokenItem = lStorage.getItem(TOKEN_CODE) +// if (!tokenItem) { +// return +// } +// const { time } = tokenItem +// // token生成或者刷新后30分钟内不执行刷新 +// if (new Date().getTime() - time <= 1000 * 60 * 30) return +// try { +// const res = await api.refreshToken() +// setToken(res.data.token) +// } catch (error) { +// console.error(error) +// } +// } diff --git a/codegen/src/utils/tool.ts b/codegen/src/utils/tool.ts new file mode 100644 index 0000000..59e2e2f --- /dev/null +++ b/codegen/src/utils/tool.ts @@ -0,0 +1,45 @@ +import type { App, Plugin } from 'vue' +import { AES, lib, enc, mode, pad } from 'crypto-js' +// 全局组件安装 +export const withInstall = (component: T, alias?: string) => { + const comp = component as any + comp.install = (app: App) => { + app.component(comp.__name || comp.displayName, component) + if (alias) { + app.config.globalProperties[alias] = component + } + } + return component as T & Plugin +} + +// 密钥 +const ENCRYPT_KEY = 'makulowcodeyyds1' + +export const decrypt = (ciphertext: string): string => { + // 将密文转换为CipherParams对象 + const cipherParams = lib.CipherParams.create({ + ciphertext: enc.Base64.parse(ciphertext) + }) + + // 使用密钥解密CipherParams对象 + const decrypted = AES.decrypt(cipherParams, enc.Utf8.parse(ENCRYPT_KEY), { + mode: mode.ECB, + padding: pad.Pkcs7 + }) + + // 获取明文 + return decrypted.toString(enc.Utf8) +} + +export const encrypt = (plaintext: string): string => { + // 将明文转换为要加密的格式 + const message = enc.Utf8.parse(plaintext) + + // 使用密钥加密明文 + const encrypted = AES.encrypt(message, enc.Utf8.parse(ENCRYPT_KEY), { + mode: mode.ECB, + padding: pad.Pkcs7 + }) + + return encrypted.toString() +} diff --git a/codegen/src/views/404.vue b/codegen/src/views/404.vue new file mode 100644 index 0000000..3800164 --- /dev/null +++ b/codegen/src/views/404.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/codegen/src/views/base-class/add-or-update.vue b/codegen/src/views/base-class/add-or-update.vue new file mode 100644 index 0000000..69d38f8 --- /dev/null +++ b/codegen/src/views/base-class/add-or-update.vue @@ -0,0 +1,105 @@ + + + diff --git a/codegen/src/views/base-class/index.vue b/codegen/src/views/base-class/index.vue new file mode 100644 index 0000000..a83b6a3 --- /dev/null +++ b/codegen/src/views/base-class/index.vue @@ -0,0 +1,108 @@ + + + diff --git a/codegen/src/views/datasource/add-or-update.vue b/codegen/src/views/datasource/add-or-update.vue new file mode 100644 index 0000000..24383f4 --- /dev/null +++ b/codegen/src/views/datasource/add-or-update.vue @@ -0,0 +1,123 @@ + + + diff --git a/codegen/src/views/datasource/index.vue b/codegen/src/views/datasource/index.vue new file mode 100644 index 0000000..94a2a30 --- /dev/null +++ b/codegen/src/views/datasource/index.vue @@ -0,0 +1,133 @@ + + + diff --git a/codegen/src/views/field-type/add-or-update.vue b/codegen/src/views/field-type/add-or-update.vue new file mode 100644 index 0000000..5933d6d --- /dev/null +++ b/codegen/src/views/field-type/add-or-update.vue @@ -0,0 +1,98 @@ + + + diff --git a/codegen/src/views/field-type/index.vue b/codegen/src/views/field-type/index.vue new file mode 100644 index 0000000..a92194f --- /dev/null +++ b/codegen/src/views/field-type/index.vue @@ -0,0 +1,104 @@ + + + diff --git a/codegen/src/views/generator/edit.vue b/codegen/src/views/generator/edit.vue new file mode 100644 index 0000000..f289293 --- /dev/null +++ b/codegen/src/views/generator/edit.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/codegen/src/views/generator/generator.vue b/codegen/src/views/generator/generator.vue new file mode 100644 index 0000000..08e1f32 --- /dev/null +++ b/codegen/src/views/generator/generator.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/codegen/src/views/generator/import.vue b/codegen/src/views/generator/import.vue new file mode 100644 index 0000000..e2c1e1c --- /dev/null +++ b/codegen/src/views/generator/import.vue @@ -0,0 +1,145 @@ + + + diff --git a/codegen/src/views/generator/index.vue b/codegen/src/views/generator/index.vue new file mode 100644 index 0000000..4a7aaa1 --- /dev/null +++ b/codegen/src/views/generator/index.vue @@ -0,0 +1,167 @@ + + + diff --git a/codegen/src/views/generator/preview.vue b/codegen/src/views/generator/preview.vue new file mode 100644 index 0000000..b53000f --- /dev/null +++ b/codegen/src/views/generator/preview.vue @@ -0,0 +1,189 @@ + + + diff --git a/codegen/src/views/project/add-or-update.vue b/codegen/src/views/project/add-or-update.vue new file mode 100644 index 0000000..ad40a7a --- /dev/null +++ b/codegen/src/views/project/add-or-update.vue @@ -0,0 +1,102 @@ + + + diff --git a/codegen/src/views/project/download.vue b/codegen/src/views/project/download.vue new file mode 100644 index 0000000..9d22f66 --- /dev/null +++ b/codegen/src/views/project/download.vue @@ -0,0 +1,139 @@ + + + diff --git a/codegen/src/views/project/index.vue b/codegen/src/views/project/index.vue new file mode 100644 index 0000000..167a3f4 --- /dev/null +++ b/codegen/src/views/project/index.vue @@ -0,0 +1,114 @@ + + + diff --git a/codegen/tsconfig.json b/codegen/tsconfig.json new file mode 100644 index 0000000..4bb2137 --- /dev/null +++ b/codegen/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "skipLibCheck": true, + "allowJs": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "lib": ["esnext", "dom"], + "types": ["web-bluetooth", "vite/client"], + "paths": { + "@/*": ["./src/*"] + }, + "compilerOptions": { + "types": ["vite-plugin-svg-icons/client"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules"] +} diff --git a/codegen/vite.config.ts b/codegen/vite.config.ts new file mode 100644 index 0000000..8dfa8fd --- /dev/null +++ b/codegen/vite.config.ts @@ -0,0 +1,54 @@ +import { resolve } from 'path' +import vue from '@vitejs/plugin-vue' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' + +import { defineConfig, loadEnv } from 'vite' + +import { convertEnv, getSrcPath, getRootPath } from './build/utils' +import { viteDefine } from './build/config' +import { OUTPUT_DIR, PROXY_CONFIG } from './build/constant' + +export default defineConfig(({ command, mode }) => { + const srcPath = getSrcPath() + const rootPath = getRootPath() + const isBuild = command === 'build' + + const env = loadEnv(mode, process.cwd()) + const viteEnv = convertEnv(env) + const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_BASE_API } = viteEnv + + return { + base: VITE_PUBLIC_PATH || '/', + resolve: { + alias: { + '~': rootPath, + '@': srcPath, + 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js' + } + }, + define: viteDefine, + plugins: [ + vue(), + createSvgIconsPlugin({ + iconDirs: [resolve(__dirname, 'src/icons/svg')], + symbolId: 'icon-[dir]-[name]' + }) + ], + server: { + host: '0.0.0.0', + port: VITE_PORT, + open: true, + proxy: VITE_USE_PROXY + ? { + [VITE_BASE_API]: PROXY_CONFIG[VITE_BASE_API] + } + : undefined + }, + build: { + target: 'es2015', + outDir: OUTPUT_DIR || 'dist', + reportCompressedSize: false, // 启用/禁用 gzip 压缩大小报告 + chunkSizeWarningLimit: 1024 // chunk 大小警告的限制(单位kb) + } + } +}) diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100644 index 0000000..3aec06e --- /dev/null +++ b/deploy/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +nginx +python run.py \ No newline at end of file diff --git a/deploy/sample-picture/1.jpg b/deploy/sample-picture/1.jpg new file mode 100644 index 0000000..d8e4f08 Binary files /dev/null and b/deploy/sample-picture/1.jpg differ diff --git a/deploy/sample-picture/2.jpg b/deploy/sample-picture/2.jpg new file mode 100644 index 0000000..e9961f3 Binary files /dev/null and b/deploy/sample-picture/2.jpg differ diff --git a/deploy/sample-picture/3.jpg b/deploy/sample-picture/3.jpg new file mode 100644 index 0000000..6a6bf83 Binary files /dev/null and b/deploy/sample-picture/3.jpg differ diff --git a/deploy/sample-picture/api.jpg b/deploy/sample-picture/api.jpg new file mode 100644 index 0000000..e5f0638 Binary files /dev/null and b/deploy/sample-picture/api.jpg differ diff --git a/deploy/sample-picture/group.jpg b/deploy/sample-picture/group.jpg new file mode 100644 index 0000000..1c75525 Binary files /dev/null and b/deploy/sample-picture/group.jpg differ diff --git a/deploy/sample-picture/login.jpg b/deploy/sample-picture/login.jpg new file mode 100644 index 0000000..196cc59 Binary files /dev/null and b/deploy/sample-picture/login.jpg differ diff --git a/deploy/sample-picture/logo.svg b/deploy/sample-picture/logo.svg new file mode 100644 index 0000000..6b8705f --- /dev/null +++ b/deploy/sample-picture/logo.svg @@ -0,0 +1 @@ + diff --git a/deploy/sample-picture/menu.jpg b/deploy/sample-picture/menu.jpg new file mode 100644 index 0000000..3c8aef1 Binary files /dev/null and b/deploy/sample-picture/menu.jpg differ diff --git a/deploy/sample-picture/role.jpg b/deploy/sample-picture/role.jpg new file mode 100644 index 0000000..8ea5411 Binary files /dev/null and b/deploy/sample-picture/role.jpg differ diff --git a/deploy/sample-picture/user.jpg b/deploy/sample-picture/user.jpg new file mode 100644 index 0000000..62bdc3b Binary files /dev/null and b/deploy/sample-picture/user.jpg differ diff --git a/deploy/sample-picture/workbench.jpg b/deploy/sample-picture/workbench.jpg new file mode 100644 index 0000000..a1d229b Binary files /dev/null and b/deploy/sample-picture/workbench.jpg differ diff --git a/deploy/web.conf b/deploy/web.conf new file mode 100644 index 0000000..3cfcda6 --- /dev/null +++ b/deploy/web.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name localhost; + location / { + root /opt/backend-server/web/dist; + index index.html index.htm; + try_files $uri /index.html; + } + location ^~ /api/ { + proxy_pass http://127.0.0.1:9999; + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c7b99b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +services: + fastapi-admin: + build: + context: ./ + dockerfile: ./Dockerfile + args: + # 可以在这里添加构建参数,例如版本号等 + APP_VERSION: 1.1 + tags: fastapi:1.1 + container_name: weixin-admin + restart: always + ports: + - "8091:80" + volumes: + - "./app:/opt/backend-server/app" # 项目代码 + - "./logs/server:/opt/backend-server/app/logs" # 日志目录 + - "./logs/nginx:/var/log/nginx" # nginx日志目录 + - "./cache:/opt/backend-server/cache_dir" # 缓存目录 + extra_hosts: + - "host.docker.internal:host-gateway" + working_dir: /opt/backend-server + # stdin_open: true # equivalent to -i + # tty: true # equivalent to -t + environment: + - APP_ENV=prod + - CACHE_DIR=/opt/backend-server/cache_dir + - TZ=Asia/Shanghai + - LANG=en_US.UTF-8 + # 👇 数据库或其他配置按需添加(如果项目需要) + - DB_CONNECTION=mysql + - DB_HOST=host.docker.internal + - DB_PORT=3306 + - DB_NAME=crm_info + - DB_USER=root + - DB_PASSWORD=^tY%ESo8TfcjSeT% + - REDIS_URL=redis://:Zmht%40170901%21%40%23%24.%3B@host.docker.internal:6379/2 + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..301fdbf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "vue-fastapi-admin" +version = "0.1.0" +description = "Vue Fastapi admin" +authors = [ + {name = "mizhexiaoxiao", email = "mizhexiaoxiao@gmail.com"}, +] +requires-python = ">=3.11" +dependencies = [ + "fastapi==0.111.0", + "tortoise-orm==0.23.0", + "pydantic==2.10.5", + "email-validator==2.2.0", + "passlib==1.7.4", + "pyjwt==2.10.1", + "black==24.10.0", + "isort==5.13.2", + "ruff==0.9.1", + "loguru==0.7.3", + "pydantic-settings==2.7.1", + "argon2-cffi==23.1.0", + "pydantic-core==2.27.2", + "annotated-types==0.7.0", + "setuptools==75.8.0", + "uvicorn==0.34.0", + "h11==0.14.0", + "aerich==0.8.1", + "aiosqlite==0.20.0", + "anyio==4.8.0", + "argon2-cffi-bindings==21.2.0", + "asyncclick==8.1.8", + "certifi==2024.12.14", + "cffi==1.17.1", + "click==8.1.8", + "dictdiffer==0.9.0", + "dnspython==2.7.0", + "fastapi-cli==0.0.7", + "httpcore==1.0.7", + "httptools==0.6.4", + "httpx==0.28.1", + "idna==3.10", + "iso8601==2.1.0", + "jinja2==3.1.5", + "markdown-it-py==3.0.0", + "markupsafe==3.0.2", + "mdurl==0.1.2", + "mypy-extensions==1.0.0", + "orjson==3.10.14", + "packaging==24.2", + "pathspec==0.12.1", + "platformdirs==4.3.6", + "pycparser==2.22", + "pygments==2.19.1", + "pypika-tortoise==0.3.2", + "python-dotenv==1.0.1", + "python-multipart==0.0.20", + "pytz==2024.2", + "pyyaml==6.0.2", + "rich==13.9.4", + "rich-toolkit==0.13.2", + "shellingham==1.5.4", + "sniffio==1.3.1", + "starlette==0.37.2", + "typer==0.15.1", + "typing-extensions==4.12.2", + "ujson==5.10.0", + "uvloop==0.21.0", + "watchfiles==1.0.4", + "websockets==14.1", + "pyproject-toml>=0.1.0", + "uvloop==0.21.0 ; sys_platform != 'win32'", +] + +[tool.black] +line-length = 120 +target-version = ["py310", "py311"] + +[tool.ruff] +line-length = 120 +lint.extend-select = [] +lint.ignore = [ + "F403", + "F405", +] + +[tool.aerich] +tortoise_orm = "app.settings.TORTOISE_ORM" +location = "./migrations" +src_folder = "./." diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ce426f1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,63 @@ +aerich==0.8.1 +aiosqlite==0.20.0 +annotated-types==0.7.0 +anyio==4.8.0 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +asyncclick==8.1.8 +black==24.10.0 +certifi==2024.12.14 +cffi==1.17.1 +click==8.1.8 +dictdiffer==0.9.0 +dnspython==2.7.0 +email-validator==2.2.0 +fastapi==0.111.0 +fastapi-cli==0.0.7 +h11==0.14.0 +httpcore==1.0.7 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +iso8601==2.1.0 +isort==5.13.2 +jinja2==3.1.5 +loguru==0.7.3 +markdown-it-py==3.0.0 +markupsafe==3.0.2 +mdurl==0.1.2 +mypy-extensions==1.0.0 +orjson==3.10.14 +packaging==24.2 +passlib==1.7.4 +pathspec==0.12.1 +platformdirs==4.3.6 +pycparser==2.22 +pydantic==2.10.5 +pydantic-core==2.27.2 +pydantic-settings==2.7.1 +pygments==2.19.1 +pyjwt==2.10.1 +pypika-tortoise==0.3.2 +python-dotenv==1.0.1 +python-multipart==0.0.20 +pytz==2024.2 +pyyaml==6.0.2 +rich==13.9.4 +rich-toolkit==0.13.2 +ruff==0.9.1 +setuptools==75.8.0 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.37.2 +tortoise-orm==0.23.0 +typer==0.15.1 +typing-extensions==4.12.2 +ujson==5.10.0 +uvicorn==0.34.0 +uvloop==0.21.0; sys_platform != 'win32' +watchfiles==1.0.4 +websockets==14.1 +colorlog==6.10.1 +apscheduler==3.11.1 +redis==7.1.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..8dc2338 --- /dev/null +++ b/run.py @@ -0,0 +1,13 @@ +import uvicorn +from uvicorn.config import LOGGING_CONFIG + +if __name__ == "__main__": + # 修改默认日志配置 + LOGGING_CONFIG["formatters"]["default"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s" + LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S" + LOGGING_CONFIG["formatters"]["access"][ + "fmt" + ] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s' + LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S" + + uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config=LOGGING_CONFIG) diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..ebfecba --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,20 @@ +python -m nuitka \ + --standalone \ + --onefile \ + --follow-imports \ + --lto=yes \ + --jobs=8 \ + --remove-output \ + --output-dir=dist \ + --enable-plugin=numpy \ + --include-package=pymysql \ + --include-module=sqlalchemy.dialects.mysql \ + --include-module=sqlalchemy.engine \ + --include-module=sqlalchemy.sql \ + --include-module=cv2 \ + --noinclude-custom-mode=cv2.gui:warning \ + --nofollow-import-to=matplotlib,scipy,pandas,tkinter,test \ + --noinclude-default-mode=error \ + --python-flag=no_docstrings \ + --noinclude-data-files=*.png,*.mp4,*.xls* \ + main.py \ No newline at end of file diff --git a/scripts/build_and_sign.py b/scripts/build_and_sign.py new file mode 100644 index 0000000..267c1d5 --- /dev/null +++ b/scripts/build_and_sign.py @@ -0,0 +1,367 @@ +import configparser +import subprocess +import shutil +import argparse +import requests +from pathlib import Path +import time +import os +import sys +sys.path.append(str(Path(__file__).resolve().parent.parent)) +from build import get_settings + +def download_file(url, dest): + """下载文件到指定路径""" + try: + r = requests.get(url, stream=True) + r.raise_for_status() + with open(dest, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + return True + except Exception as e: + print(f"下载失败: {e}") + return False + +def load_config(config_path): + """加载配置文件""" + return get_settings() + config = configparser.ConfigParser() + try: + # 指定 UTF-8 编码读取配置文件 + config.read(config_path, encoding='utf-8') + if not config.sections(): # 检查是否成功读取到配置项 + raise ValueError(f"配置文件 {config_path} 为空或格式错误") + return config + except FileNotFoundError: + raise FileNotFoundError(f"配置文件 {config_path} 不存在") + except UnicodeDecodeError: + # 尝试其他常见编码 + try: + config.read(config_path, encoding='gbk') + if not config.sections(): + raise ValueError(f"配置文件 {config_path} 为空或格式错误") + return config + except Exception as e: + raise ValueError(f"配置文件 {config_path} 编码错误: {str(e)}") + except Exception as e: + raise ValueError(f"加载配置文件 {config_path} 失败: {str(e)}") + +def build_nuitka_command(config, args, app_name): + """构造Nuitka编译命令""" + cmd = [] + + # 基本选项 + + if compiler := args.compiler or config.nuitka.compiler: + cmd.append(f"--{compiler}") + if args.show_progress or config.nuitka.show_progress: + cmd.append("--show-progress") + if args.standalone or config.nuitka.standalone: + cmd.append("--standalone") + if args.onefile or config.nuitka.onefile: + cmd.append("--onefile") + if config.nuitka.remove_output: + cmd.append("--remove-output") + if args.disable_console or config.nuitka.disable_console: + cmd.append("--windows-disable-console") + + # 版本信息 + if product_version := config.version.version: + cmd.append(f"--product-version={product_version}") + if file_version := config.version.version_str: + cmd.append(f"--file-version={file_version}") + if company_name := config.version.author: + cmd.append(f"--company-name={company_name}") + if product_name := (config.version.alias or config.version.name): + cmd.append(f"--product-name={product_name}") + cmd.append(f"--output-filename={app_name}") + + # 添加新的编译选项 + if config.nuitka.get('follow_imports', True): + cmd.append("--follow-imports") + + if lto := config.nuitka.lto: + cmd.append(f"--lto={lto}") + + if jobs := args.jobs or config.nuitka.jobs: + cmd.append(f"--jobs={jobs}") + + # 插件处理 + if plugins := config.nuitka.enable_plugin: + for plugin in plugins.split(','): + cmd.append(f"--enable-plugin={plugin.strip()}") + + # 包和模块包含 + if packages := config.nuitka.include_package: + for package in packages.split(','): + cmd.append(f"--include-package={package.strip()}") + + if modules := config.nuitka.include_module: + for module in modules.split(','): + cmd.append(f"--include-module={module.strip()}") + + # 不跟随的导入 + if nofollow := config.nuitka.nofollow_import_to: + for item in nofollow.split(','): + cmd.append(f"--nofollow-import-to={item.strip()}") + + # 数据文件排除 + if noinclude_data := config.nuitka.noinclude_data_files: + for pattern in noinclude_data.split(','): + cmd.append(f"--noinclude-data-files={pattern.strip()}") + + # Python标志 + if python_flags := config.nuitka.python_flag: + for flag in python_flags.split(','): + cmd.append(f"--python-flag={flag.strip()}") + + # 图标处理 + icon_source = args.icon or config.build.icon_file + if icon_source: + ico_path = 'favicon.ico' + if 'ico' not in icon_source: + from tranform_to_ico import convert_png_to_ico + convert_png_to_ico(icon_source, ico_path, sizes=[(256, 256)], sharpen=False) + else: + ico_path = icon_source + + if ico_path.startswith(('http://', 'https://')): + local_icon = Path("download.ico") + if download_file(ico_path, local_icon): + cmd.append(f"--windows-icon-from-ico={local_icon}") + else: + cmd.append(f"--windows-icon-from-ico={ico_path}") + + # 额外选项 + if extra_options := config.nuitka.extra_options: + if isinstance(extra_options, str): + extra_options = extra_options.split(',') + cmd.extend(extra_options) + + # 输出目录 + output_dir = args.output or config.build.output_dir or 'dist' + cmd.append(f"--output-dir={output_dir}") + + python_exe = sys.executable + cmd = [python_exe, "-m", "nuitka"] + list(set(cmd)) + + # 源文件 + source_script = args.script or config.build.source_script or 'source_script' + cmd.append(source_script) + + return cmd + +def sign_executable(config, args): + """签名可执行文件""" + if not args.sign: + print("跳过签名步骤") + return True + + sign_tool = config.signing.sign_tool + if not sign_tool or not Path(sign_tool).exists(): + print("警告: signtool.exe 未找到,跳过签名") + return False + + cert_file = args.cert or config.signing.cert_file + if not cert_file or not Path(cert_file).exists(): + print("警告: 证书文件不存在,跳过签名") + return False + + cert_password = args.password or config.signing.cert_password or '' + timestamp_server = args.timestamp or config.signing.timestamp_server or 'http://timestamp.digicert.com' + + output_dir = args.output or config.build.output_dirt + source_script = args.script or config.build.source_script + exe_name = Path(source_script).stem + ".exe" + exe_path = Path(output_dir) / exe_name + + sign_cmd = [ + sign_tool, + "sign", + "/tr", timestamp_server, + "/td", "sha256", + "/fd", "sha256", + "/f", cert_file, + "/p", cert_password, + str(exe_path), + ] + + try: + subprocess.run(sign_cmd, check=True) + print(f"✓ 签名成功: {exe_path}") + + # 验证签名 + verify_cmd = [sign_tool, "verify", "/pa", str(exe_path)] + subprocess.run(verify_cmd, check=True) + return True + except subprocess.CalledProcessError as e: + print(f"✗ 签名失败: {e}") + return False + +def parse_args(): + """解析命令行参数""" + parser = argparse.ArgumentParser(description="Python打包签名工具") + parser.add_argument('--config', default='build_config.ini', + help="配置文件路径 (默认: build_config.ini)") + parser.add_argument('--script', help="覆盖配置中的源脚本") + parser.add_argument('--output', help="覆盖输出目录") + parser.add_argument('--icon', help="本地或在线ICO图标路径 (http/https)") + parser.add_argument('--jobs', help="并行进程数") + parser.add_argument('--compiler', default="mingw64", help="编译器类型") + parser.add_argument('--show-progress', default="store_true", help="显示编译进度") + parser.add_argument('--standalone', action='store_true', help="强制启用standalone模式") + parser.add_argument('--onefile', action='store_true', help="强制启用单文件模式") + parser.add_argument('--disable-console', action='store_true', + help="禁用控制台窗口 (GUI程序)") + parser.add_argument('--no-sign', dest='sign', action='store_false', + help="跳过签名步骤") + parser.add_argument('--cert', help="覆盖证书路径") + parser.add_argument('--password', help="证书密码") + parser.add_argument('--timestamp', help="覆盖时间戳服务器URL") + return parser.parse_args() + +def format_file_size(size_bytes): + """将文件大小转换为易读格式""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024.0: + return f"{size_bytes:.2f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.2f} TB" + +def get_exe_path(config, args): + """获取生成的exe文件路径""" + output_dir = args.output or config.build.output_dirt or 'dist' + source_script = args.script or config.build.source_script + exe_name = Path(source_script).stem + (".exe" if os.name == 'nt' else "") + return Path(output_dir) / exe_name + +def main(): + args = parse_args() + print("=== 开始构建 ===") + start_time = time.time() + build_success = True + sign_success = False + + try: + config = load_config(args.config) + except FileNotFoundError as e: + print(f"错误: {e}") + return + + # import json; print(json.dumps(config, indent=4, ensure_ascii=False)) + # import json; print(json.dumps(get_settings(), indent=4, ensure_ascii=False)) + # return + + # 清理旧构建 + output_dir = args.output or config.build.output_dir or 'dist' + if config.build.clean and Path(output_dir).exists(): + try: + shutil.rmtree(output_dir) + Path(output_dir).mkdir(parents=True, exist_ok=True) + print(f"已清理输出目录: {output_dir}") + except Exception as e: + print(f"清理目录时出错: {e}") + build_success = False + + product_name = (config.version.alias or config.version.name) + product_version = config.version.version + app_name = f"{product_name}{'v'+product_version if product_version else ''}" + print(f"应用名称: {app_name}") + build_success = True + + if build_success: + # 编译 + print("\n[1/3] 正在使用Nuitka编译...") + try: + nuitka_cmd = build_nuitka_command(config, args, app_name) + print("执行命令:", " ".join(nuitka_cmd)) + + compile_start = time.time() + subprocess.run(nuitka_cmd, check=True) + print(f"编译完成,耗时: {time.time() - compile_start:.2f}秒") + except subprocess.CalledProcessError as e: + print(f"编译失败: {e}") + build_success = False + + if build_success and args.sign: + # 签名 + print("\n[2/3] 正在处理签名...") + try: + sign_start = time.time() + sign_success = sign_executable(config, args) + if sign_success: + print(f"签名完成,耗时: {time.time() - sign_start:.2f}秒") + except Exception as e: + print(f"签名过程中出错: {e}") + sign_success = False + + if build_success: + # 压缩文件 + print("\n[3/3] 正在压缩文件...") + import zipfile + from datetime import datetime + zip_path = f"{output_dir}/{app_name}.zip" + try: + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file in [f'{output_dir}/{app_name}.exe', *(config.build.resource_files or [])]: + if not file: continue + filename = Path(file).name + zipf.write(file, arcname=filename) + print(f"压缩完成: {zip_path}") + except Exception as e: + print(f"压缩文件时出错: {e}") + build_success = False + + zip_app_name = f"{config.version.name}{'v'+product_version if product_version else ''}.zip" + version_data = config.version.to_dict() + save_url = config.build.save_url.strip('/') + access_url = config.version.access_url.strip('/') + version_data['url'] = access_url + '/' + zip_app_name + + import json + with open(f'{output_dir}/version.json', 'w', encoding='utf-8') as f: + json.dump(dict( + data=version_data, + code=200, + success='true', + message=f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {app_name} 打包完成', + ), f, ensure_ascii=False, indent=4) + print(f"版本信息已保存到: {output_dir}/version.json") + + if not os.system(f'curl -T {zip_path} {save_url}/{zip_app_name}'): + print(f"上传完成: {version_data['url']}") + os.system(f'curl -T {output_dir}/version.json {save_url}/latest') + + # 构建总结 + print("\n=== 构建总结 ===") + total_time = time.time() - start_time + status = "成功" if build_success else "失败" + + summary = [ + f"构建状态: {status}", + f"总耗时: {total_time:.2f}秒", + f"输出目录: {output_dir}", + ] + + if build_success: + exe_path = get_exe_path(config, args) + if exe_path.exists(): + file_time = time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime(exe_path.stat().st_mtime)) + summary.extend([ + f"生成文件: {exe_path}", + f"文件大小: {format_file_size(exe_path.stat().st_size)}", + f"修改时间: {file_time}", + ]) + + if args.sign: + summary.append(f"签名状态: {'成功' if sign_success else '失败'}") + + print("\n".join(summary)) + + if not build_success: + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/gen.sh b/scripts/gen.sh new file mode 100644 index 0000000..6607990 --- /dev/null +++ b/scripts/gen.sh @@ -0,0 +1 @@ +python scripts/generate_pyi.py image_loader.py \ No newline at end of file diff --git a/scripts/generate_pyi.py b/scripts/generate_pyi.py new file mode 100644 index 0000000..325c734 --- /dev/null +++ b/scripts/generate_pyi.py @@ -0,0 +1,125 @@ +# generate_pyi.py + +import re +import os +import argparse +from pathlib import Path + +def sanitize_name(name: str) -> str: + name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + return re.sub(r'^[^a-zA-Z_]+', '', name) + +def pascal_case(s: str) -> str: + """将字符串转为 PascalCase""" + return ''.join(word.capitalize() for word in re.split(r'[\W_]+', s) if word) + +def generate_pyi(base_dir: str, output_file: str): + base_path = Path(base_dir) + if not base_path.exists(): + raise FileNotFoundError(f"资源目录不存在: {base_dir}") + + # 构建目录树结构 + tree = {} + + for root, dirs, files in os.walk(str(base_path)): + relative_path = Path(root).relative_to(base_path) + parts = list(relative_path.parts) if relative_path != Path('.') else [] + + # 插入当前路径到树中 + current = tree + for part in parts: + safe_part = sanitize_name(part) + if safe_part not in current: + current[safe_part] = {} + current = current[safe_part] + + # 添加图像文件作为叶子节点 + for file in files: + if file.startswith('.'): + continue + if file.split('.')[-1].lower() in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}: + stem = sanitize_name(Path(file).stem) + current[stem] = None # 表示这是一个图像资源(str 类型) + + # 构建类定义 + class_defs = [] + written_classes = set() + + def build_class(class_name: str, data: dict): + if not data or class_name in written_classes: + return + written_classes.add(class_name) + + lines = [f"class {class_name}:\n"] + has_content = False + + for key in sorted(data.keys()): + value = data[key] + safe_key = key + if value is None: + # 图像文件:直接定义为 str 属性 + lines.append(f" {safe_key}: str\n") + has_content = True + else: + # 子目录:引用一个类 + nested_class_name = pascal_case(safe_key) + lines.append(f" {safe_key}: \"{nested_class_name}\"\n") + build_class(nested_class_name, value) + has_content = True + + if has_content: + class_defs.append("".join(lines)) + else: + # 空类不写入 + written_classes.remove(class_name) + + # 构建主类 ImageResourceLoader + loader_lines = [] + for key in sorted(tree.keys()): + value = tree[key] + if not value: # 跳过空的子目录 + loader_lines.append(f" {key}: str\n") + continue + class_name = pascal_case(key) + loader_lines.append(f" {key}: {class_name}\n") + build_class(class_name, value) + + # 写入 .pyi 文件 + with open(output_file, 'w', encoding='utf-8') as f: + f.write("from typing import Optional, Any\n\n") + + # 写入所有子类定义 + for line in class_defs: + f.write(line + "\n") + + # 写入主类 + f.write("class ImageResourceLoader:\n") + f.write(" def __init__(self, base_dir: str): ...\n") + f.write(" def list_resources(self) -> None: ...\n") + f.write(" def get(self, path: str, default: Optional[str] = ...) -> Optional[str]: ...\n") + f.write(" def __getattr__(self, name: str) -> Any: ...\n") + f.write("\n # 自动生成的资源类属性(仅用于 IDE 提示)\n") + f.writelines(loader_lines) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="为 ImageResourceLoader 类生成 .pyi 存根文件。") + parser.add_argument("source_file", help="Python 源文件路径(例如:image_loader.py)") + parser.add_argument("--base_dir", default=".automation", + help="图像资源目录,默认值:.automation") + args = parser.parse_args() + + source_path = Path(args.source_file) + if not source_path.exists(): + raise FileNotFoundError(f"源文件不存在: {args.source_file}") + + # 输出 .pyi 文件路径 + pyi_path = source_path.with_suffix(".pyi") + + print(f"生成存根文件: {pyi_path}") + print(f"使用资源目录: {args.base_dir}") + + generate_pyi(args.base_dir, str(pyi_path)) + """ + python scripts/generate_pyi.py image_loader.py --base_dir assets/images + + """ \ No newline at end of file diff --git a/scripts/setup_example.sh b/scripts/setup_example.sh new file mode 100644 index 0000000..7fa950d --- /dev/null +++ b/scripts/setup_example.sh @@ -0,0 +1,15 @@ +source scripts/venv.sh + +cd app/utils/some_sdk +git pull origin main + +cd ../../../ + +export DB_CONNECTION=mysql +export DB_HOST=127.0.0.1 +export DB_PORT=3306 +export DB_NAME=my_db +export DB_USER=root +export DB_PASSWORD=xxxxxx + +python run.py \ No newline at end of file diff --git a/scripts/tranform_to_ico.py b/scripts/tranform_to_ico.py new file mode 100644 index 0000000..2c6607b --- /dev/null +++ b/scripts/tranform_to_ico.py @@ -0,0 +1,74 @@ +from PIL import Image, ImageEnhance, ImageFilter + +def convert_png_to_ico(png_path, ico_path, sizes=None, resample=Image.LANCZOS, sharpen=False): + """ + 将PNG图片转换为ICO图标文件,同时优化圆角效果 + + 参数: + - png_path: 输入PNG文件路径 + - ico_path: 输出ICO文件路径 + - sizes: 图标尺寸列表 + - resample: 重采样方法,默认为Image.LANCZOS + - sharpen: 是否应用锐化滤镜 + """ + if sizes is None: + sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (256, 256)] + + try: + with Image.open(png_path) as img: + # 确保图片为RGBA模式 + if img.mode != 'RGBA': + img = img.convert('RGBA') + + # 检查是否有透明通道 + has_transparency = False + if img.mode == 'RGBA': + alpha = img.getchannel('A') + if alpha.getextrema()[0] < 255: + has_transparency = True + + # 为每个尺寸创建单独的图像 + icon_sizes = [] + for size in sizes: + # 调整图片尺寸 + if size[0] < img.size[0] or size[1] < img.size[1]: + # 缩小图片时使用高质量重采样算法 + resized_img = img.resize(size, resample=resample) + else: + # 放大图片时使用NEAREST算法避免模糊(适用于需要清晰边缘的情况) + resized_img = img.resize(size, resample=Image.NEAREST) + + # 应用锐化滤镜(可选) + if sharpen: + enhancer = ImageEnhance.Sharpness(resized_img) + resized_img = enhancer.enhance(1.5) # 增强锐度 + + icon_sizes.append(resized_img) + + # 保存为ICO文件 + if len(icon_sizes) > 0: + # 使用第一个尺寸作为主图标 + icon_sizes[0].save( + ico_path, + format='ICO', + sizes=[(img.size[0], img.size[1]) for img in icon_sizes] + ) + print(f"成功将 {png_path} 转换为 {ico_path}") + print(f"包含尺寸: {sizes}") + if has_transparency: + print("注意:已保留图片的透明通道(圆角效果)") + else: + print("图片没有检测到透明通道(可能没有圆角效果)") + else: + print("错误:未指定有效尺寸") + + except Exception as e: + print(f"转换失败: {e}") + +if __name__ == "__main__": + # 输入和输出文件路径 + png_path = 'app.png' + ico_path = 'favicon.ico' + + # 调用函数进行转换,禁用锐化处理 + convert_png_to_ico(png_path, ico_path, sizes=[(256, 256)], sharpen=False) \ No newline at end of file diff --git a/scripts/venv.sh b/scripts/venv.sh new file mode 100644 index 0000000..d73437a --- /dev/null +++ b/scripts/venv.sh @@ -0,0 +1,23 @@ +default_value=vue-fastapi-admin +app=${1:-$default_value} +platform=$(uname -s) + +if [ "${platform}" == "Linux" ]; then + env_path=/data/env/python/${app} + bin_path=${env_path}/bin +# 使用 [[ ]] 进行模式匹配 +elif [[ "${platform}" == *"MINGW64"* ]]; then + env_path=/d/data/python-venv/${app} + bin_path=${env_path}/Scripts +fi + +if [ -d "${env_path}" ]; then + echo "Activating virtual environment..." + source ${bin_path}/activate +else + echo "Creating virtual environment..." + python -m venv ${env_path} + echo "Activating virtual environment..." + source ${bin_path}/activate + pip install -r requirements.txt +fi diff --git a/static/swagger-ui/swagger-ui-bundle.js b/static/swagger-ui/swagger-ui-bundle.js new file mode 100644 index 0000000..8dbca51 --- /dev/null +++ b/static/swagger-ui/swagger-ui-bundle.js @@ -0,0 +1,3 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function webpackUniversalModuleDefinition(i,s){"object"==typeof exports&&"object"==typeof module?module.exports=s():"function"==typeof define&&define.amd?define([],s):"object"==typeof exports?exports.SwaggerUIBundle=s():i.SwaggerUIBundle=s()}(this,(()=>(()=>{var i={17967:(i,s)=>{"use strict";s.Nm=s.Rq=void 0;var u=/^([^\w]*)(javascript|data|vbscript)/im,m=/&#(\w+)(^\w|;)?/g,v=/&(newline|tab);/gi,_=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,j=/^.+(:|:)/gim,M=[".","/"];s.Rq="about:blank",s.Nm=function sanitizeUrl(i){if(!i)return s.Rq;var $=function decodeHtmlCharacters(i){return i.replace(_,"").replace(m,(function(i,s){return String.fromCharCode(s)}))}(i).replace(v,"").replace(_,"").trim();if(!$)return s.Rq;if(function isRelativeUrlWithoutProtocol(i){return M.indexOf(i[0])>-1}($))return $;var W=$.match(j);if(!W)return $;var X=W[0];return u.test(X)?s.Rq:$}},79742:(i,s)=>{"use strict";s.byteLength=function byteLength(i){var s=getLens(i),u=s[0],m=s[1];return 3*(u+m)/4-m},s.toByteArray=function toByteArray(i){var s,u,_=getLens(i),j=_[0],M=_[1],$=new v(function _byteLength(i,s,u){return 3*(s+u)/4-u}(0,j,M)),W=0,X=M>0?j-4:j;for(u=0;u>16&255,$[W++]=s>>8&255,$[W++]=255&s;2===M&&(s=m[i.charCodeAt(u)]<<2|m[i.charCodeAt(u+1)]>>4,$[W++]=255&s);1===M&&(s=m[i.charCodeAt(u)]<<10|m[i.charCodeAt(u+1)]<<4|m[i.charCodeAt(u+2)]>>2,$[W++]=s>>8&255,$[W++]=255&s);return $},s.fromByteArray=function fromByteArray(i){for(var s,m=i.length,v=m%3,_=[],j=16383,M=0,$=m-v;M<$;M+=j)_.push(encodeChunk(i,M,M+j>$?$:M+j));1===v?(s=i[m-1],_.push(u[s>>2]+u[s<<4&63]+"==")):2===v&&(s=(i[m-2]<<8)+i[m-1],_.push(u[s>>10]+u[s>>4&63]+u[s<<2&63]+"="));return _.join("")};for(var u=[],m=[],v="undefined"!=typeof Uint8Array?Uint8Array:Array,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",j=0;j<64;++j)u[j]=_[j],m[_.charCodeAt(j)]=j;function getLens(i){var s=i.length;if(s%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var u=i.indexOf("=");return-1===u&&(u=s),[u,u===s?0:4-u%4]}function encodeChunk(i,s,m){for(var v,_,j=[],M=s;M>18&63]+u[_>>12&63]+u[_>>6&63]+u[63&_]);return j.join("")}m["-".charCodeAt(0)]=62,m["_".charCodeAt(0)]=63},48764:(i,s,u)=>{"use strict";const m=u(79742),v=u(80645),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;s.Buffer=Buffer,s.SlowBuffer=function SlowBuffer(i){+i!=i&&(i=0);return Buffer.alloc(+i)},s.INSPECT_MAX_BYTES=50;const j=2147483647;function createBuffer(i){if(i>j)throw new RangeError('The value "'+i+'" is invalid for option "size"');const s=new Uint8Array(i);return Object.setPrototypeOf(s,Buffer.prototype),s}function Buffer(i,s,u){if("number"==typeof i){if("string"==typeof s)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(i)}return from(i,s,u)}function from(i,s,u){if("string"==typeof i)return function fromString(i,s){"string"==typeof s&&""!==s||(s="utf8");if(!Buffer.isEncoding(s))throw new TypeError("Unknown encoding: "+s);const u=0|byteLength(i,s);let m=createBuffer(u);const v=m.write(i,s);v!==u&&(m=m.slice(0,v));return m}(i,s);if(ArrayBuffer.isView(i))return function fromArrayView(i){if(isInstance(i,Uint8Array)){const s=new Uint8Array(i);return fromArrayBuffer(s.buffer,s.byteOffset,s.byteLength)}return fromArrayLike(i)}(i);if(null==i)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof i);if(isInstance(i,ArrayBuffer)||i&&isInstance(i.buffer,ArrayBuffer))return fromArrayBuffer(i,s,u);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(i,SharedArrayBuffer)||i&&isInstance(i.buffer,SharedArrayBuffer)))return fromArrayBuffer(i,s,u);if("number"==typeof i)throw new TypeError('The "value" argument must not be of type number. Received type number');const m=i.valueOf&&i.valueOf();if(null!=m&&m!==i)return Buffer.from(m,s,u);const v=function fromObject(i){if(Buffer.isBuffer(i)){const s=0|checked(i.length),u=createBuffer(s);return 0===u.length||i.copy(u,0,0,s),u}if(void 0!==i.length)return"number"!=typeof i.length||numberIsNaN(i.length)?createBuffer(0):fromArrayLike(i);if("Buffer"===i.type&&Array.isArray(i.data))return fromArrayLike(i.data)}(i);if(v)return v;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof i[Symbol.toPrimitive])return Buffer.from(i[Symbol.toPrimitive]("string"),s,u);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof i)}function assertSize(i){if("number"!=typeof i)throw new TypeError('"size" argument must be of type number');if(i<0)throw new RangeError('The value "'+i+'" is invalid for option "size"')}function allocUnsafe(i){return assertSize(i),createBuffer(i<0?0:0|checked(i))}function fromArrayLike(i){const s=i.length<0?0:0|checked(i.length),u=createBuffer(s);for(let m=0;m=j)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+j.toString(16)+" bytes");return 0|i}function byteLength(i,s){if(Buffer.isBuffer(i))return i.length;if(ArrayBuffer.isView(i)||isInstance(i,ArrayBuffer))return i.byteLength;if("string"!=typeof i)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof i);const u=i.length,m=arguments.length>2&&!0===arguments[2];if(!m&&0===u)return 0;let v=!1;for(;;)switch(s){case"ascii":case"latin1":case"binary":return u;case"utf8":case"utf-8":return utf8ToBytes(i).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*u;case"hex":return u>>>1;case"base64":return base64ToBytes(i).length;default:if(v)return m?-1:utf8ToBytes(i).length;s=(""+s).toLowerCase(),v=!0}}function slowToString(i,s,u){let m=!1;if((void 0===s||s<0)&&(s=0),s>this.length)return"";if((void 0===u||u>this.length)&&(u=this.length),u<=0)return"";if((u>>>=0)<=(s>>>=0))return"";for(i||(i="utf8");;)switch(i){case"hex":return hexSlice(this,s,u);case"utf8":case"utf-8":return utf8Slice(this,s,u);case"ascii":return asciiSlice(this,s,u);case"latin1":case"binary":return latin1Slice(this,s,u);case"base64":return base64Slice(this,s,u);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,s,u);default:if(m)throw new TypeError("Unknown encoding: "+i);i=(i+"").toLowerCase(),m=!0}}function swap(i,s,u){const m=i[s];i[s]=i[u],i[u]=m}function bidirectionalIndexOf(i,s,u,m,v){if(0===i.length)return-1;if("string"==typeof u?(m=u,u=0):u>2147483647?u=2147483647:u<-2147483648&&(u=-2147483648),numberIsNaN(u=+u)&&(u=v?0:i.length-1),u<0&&(u=i.length+u),u>=i.length){if(v)return-1;u=i.length-1}else if(u<0){if(!v)return-1;u=0}if("string"==typeof s&&(s=Buffer.from(s,m)),Buffer.isBuffer(s))return 0===s.length?-1:arrayIndexOf(i,s,u,m,v);if("number"==typeof s)return s&=255,"function"==typeof Uint8Array.prototype.indexOf?v?Uint8Array.prototype.indexOf.call(i,s,u):Uint8Array.prototype.lastIndexOf.call(i,s,u):arrayIndexOf(i,[s],u,m,v);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(i,s,u,m,v){let _,j=1,M=i.length,$=s.length;if(void 0!==m&&("ucs2"===(m=String(m).toLowerCase())||"ucs-2"===m||"utf16le"===m||"utf-16le"===m)){if(i.length<2||s.length<2)return-1;j=2,M/=2,$/=2,u/=2}function read(i,s){return 1===j?i[s]:i.readUInt16BE(s*j)}if(v){let m=-1;for(_=u;_M&&(u=M-$),_=u;_>=0;_--){let u=!0;for(let m=0;m<$;m++)if(read(i,_+m)!==read(s,m)){u=!1;break}if(u)return _}return-1}function hexWrite(i,s,u,m){u=Number(u)||0;const v=i.length-u;m?(m=Number(m))>v&&(m=v):m=v;const _=s.length;let j;for(m>_/2&&(m=_/2),j=0;j>8,v=u%256,_.push(v),_.push(m);return _}(s,i.length-u),i,u,m)}function base64Slice(i,s,u){return 0===s&&u===i.length?m.fromByteArray(i):m.fromByteArray(i.slice(s,u))}function utf8Slice(i,s,u){u=Math.min(i.length,u);const m=[];let v=s;for(;v239?4:s>223?3:s>191?2:1;if(v+j<=u){let u,m,M,$;switch(j){case 1:s<128&&(_=s);break;case 2:u=i[v+1],128==(192&u)&&($=(31&s)<<6|63&u,$>127&&(_=$));break;case 3:u=i[v+1],m=i[v+2],128==(192&u)&&128==(192&m)&&($=(15&s)<<12|(63&u)<<6|63&m,$>2047&&($<55296||$>57343)&&(_=$));break;case 4:u=i[v+1],m=i[v+2],M=i[v+3],128==(192&u)&&128==(192&m)&&128==(192&M)&&($=(15&s)<<18|(63&u)<<12|(63&m)<<6|63&M,$>65535&&$<1114112&&(_=$))}}null===_?(_=65533,j=1):_>65535&&(_-=65536,m.push(_>>>10&1023|55296),_=56320|1023&_),m.push(_),v+=j}return function decodeCodePointsArray(i){const s=i.length;if(s<=M)return String.fromCharCode.apply(String,i);let u="",m=0;for(;mm.length?(Buffer.isBuffer(s)||(s=Buffer.from(s)),s.copy(m,v)):Uint8Array.prototype.set.call(m,s,v);else{if(!Buffer.isBuffer(s))throw new TypeError('"list" argument must be an Array of Buffers');s.copy(m,v)}v+=s.length}return m},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const i=this.length;if(i%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let s=0;su&&(i+=" ... "),""},_&&(Buffer.prototype[_]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(i,s,u,m,v){if(isInstance(i,Uint8Array)&&(i=Buffer.from(i,i.offset,i.byteLength)),!Buffer.isBuffer(i))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof i);if(void 0===s&&(s=0),void 0===u&&(u=i?i.length:0),void 0===m&&(m=0),void 0===v&&(v=this.length),s<0||u>i.length||m<0||v>this.length)throw new RangeError("out of range index");if(m>=v&&s>=u)return 0;if(m>=v)return-1;if(s>=u)return 1;if(this===i)return 0;let _=(v>>>=0)-(m>>>=0),j=(u>>>=0)-(s>>>=0);const M=Math.min(_,j),$=this.slice(m,v),W=i.slice(s,u);for(let i=0;i>>=0,isFinite(u)?(u>>>=0,void 0===m&&(m="utf8")):(m=u,u=void 0)}const v=this.length-s;if((void 0===u||u>v)&&(u=v),i.length>0&&(u<0||s<0)||s>this.length)throw new RangeError("Attempt to write outside buffer bounds");m||(m="utf8");let _=!1;for(;;)switch(m){case"hex":return hexWrite(this,i,s,u);case"utf8":case"utf-8":return utf8Write(this,i,s,u);case"ascii":case"latin1":case"binary":return asciiWrite(this,i,s,u);case"base64":return base64Write(this,i,s,u);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,i,s,u);default:if(_)throw new TypeError("Unknown encoding: "+m);m=(""+m).toLowerCase(),_=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const M=4096;function asciiSlice(i,s,u){let m="";u=Math.min(i.length,u);for(let v=s;vm)&&(u=m);let v="";for(let m=s;mu)throw new RangeError("Trying to access beyond buffer length")}function checkInt(i,s,u,m,v,_){if(!Buffer.isBuffer(i))throw new TypeError('"buffer" argument must be a Buffer instance');if(s>v||s<_)throw new RangeError('"value" argument is out of bounds');if(u+m>i.length)throw new RangeError("Index out of range")}function wrtBigUInt64LE(i,s,u,m,v){checkIntBI(s,m,v,i,u,7);let _=Number(s&BigInt(4294967295));i[u++]=_,_>>=8,i[u++]=_,_>>=8,i[u++]=_,_>>=8,i[u++]=_;let j=Number(s>>BigInt(32)&BigInt(4294967295));return i[u++]=j,j>>=8,i[u++]=j,j>>=8,i[u++]=j,j>>=8,i[u++]=j,u}function wrtBigUInt64BE(i,s,u,m,v){checkIntBI(s,m,v,i,u,7);let _=Number(s&BigInt(4294967295));i[u+7]=_,_>>=8,i[u+6]=_,_>>=8,i[u+5]=_,_>>=8,i[u+4]=_;let j=Number(s>>BigInt(32)&BigInt(4294967295));return i[u+3]=j,j>>=8,i[u+2]=j,j>>=8,i[u+1]=j,j>>=8,i[u]=j,u+8}function checkIEEE754(i,s,u,m,v,_){if(u+m>i.length)throw new RangeError("Index out of range");if(u<0)throw new RangeError("Index out of range")}function writeFloat(i,s,u,m,_){return s=+s,u>>>=0,_||checkIEEE754(i,0,u,4),v.write(i,s,u,m,23,4),u+4}function writeDouble(i,s,u,m,_){return s=+s,u>>>=0,_||checkIEEE754(i,0,u,8),v.write(i,s,u,m,52,8),u+8}Buffer.prototype.slice=function slice(i,s){const u=this.length;(i=~~i)<0?(i+=u)<0&&(i=0):i>u&&(i=u),(s=void 0===s?u:~~s)<0?(s+=u)<0&&(s=0):s>u&&(s=u),s>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=this[i],v=1,_=0;for(;++_>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=this[i+--s],v=1;for(;s>0&&(v*=256);)m+=this[i+--s]*v;return m},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(i,s){return i>>>=0,s||checkOffset(i,1,this.length),this[i]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(i,s){return i>>>=0,s||checkOffset(i,2,this.length),this[i]|this[i+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(i,s){return i>>>=0,s||checkOffset(i,2,this.length),this[i]<<8|this[i+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),(this[i]|this[i+1]<<8|this[i+2]<<16)+16777216*this[i+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),16777216*this[i]+(this[i+1]<<16|this[i+2]<<8|this[i+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(i){validateNumber(i>>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=s+256*this[++i]+65536*this[++i]+this[++i]*2**24,v=this[++i]+256*this[++i]+65536*this[++i]+u*2**24;return BigInt(m)+(BigInt(v)<>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=s*2**24+65536*this[++i]+256*this[++i]+this[++i],v=this[++i]*2**24+65536*this[++i]+256*this[++i]+u;return(BigInt(m)<>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=this[i],v=1,_=0;for(;++_=v&&(m-=Math.pow(2,8*s)),m},Buffer.prototype.readIntBE=function readIntBE(i,s,u){i>>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=s,v=1,_=this[i+--m];for(;m>0&&(v*=256);)_+=this[i+--m]*v;return v*=128,_>=v&&(_-=Math.pow(2,8*s)),_},Buffer.prototype.readInt8=function readInt8(i,s){return i>>>=0,s||checkOffset(i,1,this.length),128&this[i]?-1*(255-this[i]+1):this[i]},Buffer.prototype.readInt16LE=function readInt16LE(i,s){i>>>=0,s||checkOffset(i,2,this.length);const u=this[i]|this[i+1]<<8;return 32768&u?4294901760|u:u},Buffer.prototype.readInt16BE=function readInt16BE(i,s){i>>>=0,s||checkOffset(i,2,this.length);const u=this[i+1]|this[i]<<8;return 32768&u?4294901760|u:u},Buffer.prototype.readInt32LE=function readInt32LE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),this[i]|this[i+1]<<8|this[i+2]<<16|this[i+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),this[i]<<24|this[i+1]<<16|this[i+2]<<8|this[i+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(i){validateNumber(i>>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=this[i+4]+256*this[i+5]+65536*this[i+6]+(u<<24);return(BigInt(m)<>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=(s<<24)+65536*this[++i]+256*this[++i]+this[++i];return(BigInt(m)<>>=0,s||checkOffset(i,4,this.length),v.read(this,i,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),v.read(this,i,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(i,s){return i>>>=0,s||checkOffset(i,8,this.length),v.read(this,i,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(i,s){return i>>>=0,s||checkOffset(i,8,this.length),v.read(this,i,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(i,s,u,m){if(i=+i,s>>>=0,u>>>=0,!m){checkInt(this,i,s,u,Math.pow(2,8*u)-1,0)}let v=1,_=0;for(this[s]=255&i;++_>>=0,u>>>=0,!m){checkInt(this,i,s,u,Math.pow(2,8*u)-1,0)}let v=u-1,_=1;for(this[s+v]=255&i;--v>=0&&(_*=256);)this[s+v]=i/_&255;return s+u},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,1,255,0),this[s]=255&i,s+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,65535,0),this[s]=255&i,this[s+1]=i>>>8,s+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,65535,0),this[s]=i>>>8,this[s+1]=255&i,s+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,4294967295,0),this[s+3]=i>>>24,this[s+2]=i>>>16,this[s+1]=i>>>8,this[s]=255&i,s+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,4294967295,0),this[s]=i>>>24,this[s+1]=i>>>16,this[s+2]=i>>>8,this[s+3]=255&i,s+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(i,s=0){return wrtBigUInt64LE(this,i,s,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(i,s=0){return wrtBigUInt64BE(this,i,s,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeIntLE=function writeIntLE(i,s,u,m){if(i=+i,s>>>=0,!m){const m=Math.pow(2,8*u-1);checkInt(this,i,s,u,m-1,-m)}let v=0,_=1,j=0;for(this[s]=255&i;++v>0)-j&255;return s+u},Buffer.prototype.writeIntBE=function writeIntBE(i,s,u,m){if(i=+i,s>>>=0,!m){const m=Math.pow(2,8*u-1);checkInt(this,i,s,u,m-1,-m)}let v=u-1,_=1,j=0;for(this[s+v]=255&i;--v>=0&&(_*=256);)i<0&&0===j&&0!==this[s+v+1]&&(j=1),this[s+v]=(i/_>>0)-j&255;return s+u},Buffer.prototype.writeInt8=function writeInt8(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,1,127,-128),i<0&&(i=255+i+1),this[s]=255&i,s+1},Buffer.prototype.writeInt16LE=function writeInt16LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,32767,-32768),this[s]=255&i,this[s+1]=i>>>8,s+2},Buffer.prototype.writeInt16BE=function writeInt16BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,32767,-32768),this[s]=i>>>8,this[s+1]=255&i,s+2},Buffer.prototype.writeInt32LE=function writeInt32LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,2147483647,-2147483648),this[s]=255&i,this[s+1]=i>>>8,this[s+2]=i>>>16,this[s+3]=i>>>24,s+4},Buffer.prototype.writeInt32BE=function writeInt32BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,2147483647,-2147483648),i<0&&(i=4294967295+i+1),this[s]=i>>>24,this[s+1]=i>>>16,this[s+2]=i>>>8,this[s+3]=255&i,s+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(i,s=0){return wrtBigUInt64LE(this,i,s,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(i,s=0){return wrtBigUInt64BE(this,i,s,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(i,s,u){return writeFloat(this,i,s,!0,u)},Buffer.prototype.writeFloatBE=function writeFloatBE(i,s,u){return writeFloat(this,i,s,!1,u)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(i,s,u){return writeDouble(this,i,s,!0,u)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(i,s,u){return writeDouble(this,i,s,!1,u)},Buffer.prototype.copy=function copy(i,s,u,m){if(!Buffer.isBuffer(i))throw new TypeError("argument should be a Buffer");if(u||(u=0),m||0===m||(m=this.length),s>=i.length&&(s=i.length),s||(s=0),m>0&&m=this.length)throw new RangeError("Index out of range");if(m<0)throw new RangeError("sourceEnd out of bounds");m>this.length&&(m=this.length),i.length-s>>=0,u=void 0===u?this.length:u>>>0,i||(i=0),"number"==typeof i)for(v=s;v=m+4;u-=3)s=`_${i.slice(u-3,u)}${s}`;return`${i.slice(0,u)}${s}`}function checkIntBI(i,s,u,m,v,_){if(i>u||i3?0===s||s===BigInt(0)?`>= 0${m} and < 2${m} ** ${8*(_+1)}${m}`:`>= -(2${m} ** ${8*(_+1)-1}${m}) and < 2 ** ${8*(_+1)-1}${m}`:`>= ${s}${m} and <= ${u}${m}`,new $.ERR_OUT_OF_RANGE("value",v,i)}!function checkBounds(i,s,u){validateNumber(s,"offset"),void 0!==i[s]&&void 0!==i[s+u]||boundsError(s,i.length-(u+1))}(m,v,_)}function validateNumber(i,s){if("number"!=typeof i)throw new $.ERR_INVALID_ARG_TYPE(s,"number",i)}function boundsError(i,s,u){if(Math.floor(i)!==i)throw validateNumber(i,u),new $.ERR_OUT_OF_RANGE(u||"offset","an integer",i);if(s<0)throw new $.ERR_BUFFER_OUT_OF_BOUNDS;throw new $.ERR_OUT_OF_RANGE(u||"offset",`>= ${u?1:0} and <= ${s}`,i)}E("ERR_BUFFER_OUT_OF_BOUNDS",(function(i){return i?`${i} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),E("ERR_INVALID_ARG_TYPE",(function(i,s){return`The "${i}" argument must be of type number. Received type ${typeof s}`}),TypeError),E("ERR_OUT_OF_RANGE",(function(i,s,u){let m=`The value of "${i}" is out of range.`,v=u;return Number.isInteger(u)&&Math.abs(u)>2**32?v=addNumericalSeparator(String(u)):"bigint"==typeof u&&(v=String(u),(u>BigInt(2)**BigInt(32)||u<-(BigInt(2)**BigInt(32)))&&(v=addNumericalSeparator(v)),v+="n"),m+=` It must be ${s}. Received ${v}`,m}),RangeError);const W=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(i,s){let u;s=s||1/0;const m=i.length;let v=null;const _=[];for(let j=0;j55295&&u<57344){if(!v){if(u>56319){(s-=3)>-1&&_.push(239,191,189);continue}if(j+1===m){(s-=3)>-1&&_.push(239,191,189);continue}v=u;continue}if(u<56320){(s-=3)>-1&&_.push(239,191,189),v=u;continue}u=65536+(v-55296<<10|u-56320)}else v&&(s-=3)>-1&&_.push(239,191,189);if(v=null,u<128){if((s-=1)<0)break;_.push(u)}else if(u<2048){if((s-=2)<0)break;_.push(u>>6|192,63&u|128)}else if(u<65536){if((s-=3)<0)break;_.push(u>>12|224,u>>6&63|128,63&u|128)}else{if(!(u<1114112))throw new Error("Invalid code point");if((s-=4)<0)break;_.push(u>>18|240,u>>12&63|128,u>>6&63|128,63&u|128)}}return _}function base64ToBytes(i){return m.toByteArray(function base64clean(i){if((i=(i=i.split("=")[0]).trim().replace(W,"")).length<2)return"";for(;i.length%4!=0;)i+="=";return i}(i))}function blitBuffer(i,s,u,m){let v;for(v=0;v=s.length||v>=i.length);++v)s[v+u]=i[v];return v}function isInstance(i,s){return i instanceof s||null!=i&&null!=i.constructor&&null!=i.constructor.name&&i.constructor.name===s.name}function numberIsNaN(i){return i!=i}const X=function(){const i="0123456789abcdef",s=new Array(256);for(let u=0;u<16;++u){const m=16*u;for(let v=0;v<16;++v)s[m+v]=i[u]+i[v]}return s}();function defineBigIntMethod(i){return"undefined"==typeof BigInt?BufferBigIntNotDefined:i}function BufferBigIntNotDefined(){throw new Error("BigInt not supported")}},21924:(i,s,u)=>{"use strict";var m=u(40210),v=u(55559),_=v(m("String.prototype.indexOf"));i.exports=function callBoundIntrinsic(i,s){var u=m(i,!!s);return"function"==typeof u&&_(i,".prototype.")>-1?v(u):u}},55559:(i,s,u)=>{"use strict";var m=u(58612),v=u(40210),_=v("%Function.prototype.apply%"),j=v("%Function.prototype.call%"),M=v("%Reflect.apply%",!0)||m.call(j,_),$=v("%Object.getOwnPropertyDescriptor%",!0),W=v("%Object.defineProperty%",!0),X=v("%Math.max%");if(W)try{W({},"a",{value:1})}catch(i){W=null}i.exports=function callBind(i){var s=M(m,j,arguments);$&&W&&($(s,"length").configurable&&W(s,"length",{value:1+X(0,i.length-(arguments.length-1))}));return s};var Y=function applyBind(){return M(m,_,arguments)};W?W(i.exports,"apply",{value:Y}):i.exports.apply=Y},94184:(i,s)=>{var u;!function(){"use strict";var m={}.hasOwnProperty;function classNames(){for(var i=[],s=0;s{"use strict";s.parse=function parse(i,s){if("string"!=typeof i)throw new TypeError("argument str must be a string");var u={},m=(s||{}).decode||decode,v=0;for(;v{"use strict";var m=u(11742),v={"text/plain":"Text","text/html":"Url",default:"Text"};i.exports=function copy(i,s){var u,_,j,M,$,W,X=!1;s||(s={}),u=s.debug||!1;try{if(j=m(),M=document.createRange(),$=document.getSelection(),(W=document.createElement("span")).textContent=i,W.ariaHidden="true",W.style.all="unset",W.style.position="fixed",W.style.top=0,W.style.clip="rect(0, 0, 0, 0)",W.style.whiteSpace="pre",W.style.webkitUserSelect="text",W.style.MozUserSelect="text",W.style.msUserSelect="text",W.style.userSelect="text",W.addEventListener("copy",(function(m){if(m.stopPropagation(),s.format)if(m.preventDefault(),void 0===m.clipboardData){u&&console.warn("unable to use e.clipboardData"),u&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var _=v[s.format]||v.default;window.clipboardData.setData(_,i)}else m.clipboardData.clearData(),m.clipboardData.setData(s.format,i);s.onCopy&&(m.preventDefault(),s.onCopy(m.clipboardData))})),document.body.appendChild(W),M.selectNodeContents(W),$.addRange(M),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");X=!0}catch(m){u&&console.error("unable to copy using execCommand: ",m),u&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(s.format||"text",i),s.onCopy&&s.onCopy(window.clipboardData),X=!0}catch(m){u&&console.error("unable to copy using clipboardData: ",m),u&&console.error("falling back to prompt"),_=function format(i){var s=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return i.replace(/#{\s*key\s*}/g,s)}("message"in s?s.message:"Copy to clipboard: #{key}, Enter"),window.prompt(_,i)}}finally{$&&("function"==typeof $.removeRange?$.removeRange(M):$.removeAllRanges()),W&&document.body.removeChild(W),j()}return X}},44101:(i,s,u)=>{var m=u(18957);i.exports=m},90093:(i,s,u)=>{var m=u(28196);i.exports=m},65362:(i,s,u)=>{var m=u(63383);i.exports=m},50415:(i,s,u)=>{u(61181),u(47627),u(24415),u(66274),u(77971);var m=u(54058);i.exports=m.AggregateError},27700:(i,s,u)=>{u(73381);var m=u(35703);i.exports=m("Function").bind},16246:(i,s,u)=>{var m=u(7046),v=u(27700),_=Function.prototype;i.exports=function(i){var s=i.bind;return i===_||m(_,i)&&s===_.bind?v:s}},45999:(i,s,u)=>{u(49221);var m=u(54058);i.exports=m.Object.assign},16121:(i,s,u)=>{i.exports=u(38644)},14122:(i,s,u)=>{i.exports=u(89097)},60269:(i,s,u)=>{i.exports=u(76936)},38644:(i,s,u)=>{u(89731);var m=u(44101);i.exports=m},89097:(i,s,u)=>{var m=u(90093);i.exports=m},76936:(i,s,u)=>{var m=u(65362);i.exports=m},24883:(i,s,u)=>{var m=u(57475),v=u(69826),_=TypeError;i.exports=function(i){if(m(i))return i;throw _(v(i)+" is not a function")}},11851:(i,s,u)=>{var m=u(57475),v=String,_=TypeError;i.exports=function(i){if("object"==typeof i||m(i))return i;throw _("Can't set "+v(i)+" as a prototype")}},18479:i=>{i.exports=function(){}},96059:(i,s,u)=>{var m=u(10941),v=String,_=TypeError;i.exports=function(i){if(m(i))return i;throw _(v(i)+" is not an object")}},31692:(i,s,u)=>{var m=u(74529),v=u(59413),_=u(10623),createMethod=function(i){return function(s,u,j){var M,$=m(s),W=_($),X=v(j,W);if(i&&u!=u){for(;W>X;)if((M=$[X++])!=M)return!0}else for(;W>X;X++)if((i||X in $)&&$[X]===u)return i||X||0;return!i&&-1}};i.exports={includes:createMethod(!0),indexOf:createMethod(!1)}},93765:(i,s,u)=>{var m=u(95329);i.exports=m([].slice)},82532:(i,s,u)=>{var m=u(95329),v=m({}.toString),_=m("".slice);i.exports=function(i){return _(v(i),8,-1)}},9697:(i,s,u)=>{var m=u(22885),v=u(57475),_=u(82532),j=u(99813)("toStringTag"),M=Object,$="Arguments"==_(function(){return arguments}());i.exports=m?_:function(i){var s,u,m;return void 0===i?"Undefined":null===i?"Null":"string"==typeof(u=function(i,s){try{return i[s]}catch(i){}}(s=M(i),j))?u:$?_(s):"Object"==(m=_(s))&&v(s.callee)?"Arguments":m}},23489:(i,s,u)=>{var m=u(90953),v=u(31136),_=u(49677),j=u(65988);i.exports=function(i,s,u){for(var M=v(s),$=j.f,W=_.f,X=0;X{var m=u(95981);i.exports=!m((function(){function F(){}return F.prototype.constructor=null,Object.getPrototypeOf(new F)!==F.prototype}))},23538:i=>{i.exports=function(i,s){return{value:i,done:s}}},32029:(i,s,u)=>{var m=u(55746),v=u(65988),_=u(31887);i.exports=m?function(i,s,u){return v.f(i,s,_(1,u))}:function(i,s,u){return i[s]=u,i}},31887:i=>{i.exports=function(i,s){return{enumerable:!(1&i),configurable:!(2&i),writable:!(4&i),value:s}}},95929:(i,s,u)=>{var m=u(32029);i.exports=function(i,s,u,v){return v&&v.enumerable?i[s]=u:m(i,s,u),i}},75609:(i,s,u)=>{var m=u(21899),v=Object.defineProperty;i.exports=function(i,s){try{v(m,i,{value:s,configurable:!0,writable:!0})}catch(u){m[i]=s}return s}},55746:(i,s,u)=>{var m=u(95981);i.exports=!m((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},76616:i=>{var s="object"==typeof document&&document.all,u=void 0===s&&void 0!==s;i.exports={all:s,IS_HTMLDDA:u}},61333:(i,s,u)=>{var m=u(21899),v=u(10941),_=m.document,j=v(_)&&v(_.createElement);i.exports=function(i){return j?_.createElement(i):{}}},63281:i=>{i.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},2861:i=>{i.exports="undefined"!=typeof navigator&&String(navigator.userAgent)||""},53385:(i,s,u)=>{var m,v,_=u(21899),j=u(2861),M=_.process,$=_.Deno,W=M&&M.versions||$&&$.version,X=W&&W.v8;X&&(v=(m=X.split("."))[0]>0&&m[0]<4?1:+(m[0]+m[1])),!v&&j&&(!(m=j.match(/Edge\/(\d+)/))||m[1]>=74)&&(m=j.match(/Chrome\/(\d+)/))&&(v=+m[1]),i.exports=v},35703:(i,s,u)=>{var m=u(54058);i.exports=function(i){return m[i+"Prototype"]}},56759:i=>{i.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},53995:(i,s,u)=>{var m=u(95329),v=Error,_=m("".replace),j=String(v("zxcasd").stack),M=/\n\s*at [^:]*:[^\n]*/,$=M.test(j);i.exports=function(i,s){if($&&"string"==typeof i&&!v.prepareStackTrace)for(;s--;)i=_(i,M,"");return i}},79585:(i,s,u)=>{var m=u(32029),v=u(53995),_=u(18780),j=Error.captureStackTrace;i.exports=function(i,s,u,M){_&&(j?j(i,s):m(i,"stack",v(u,M)))}},18780:(i,s,u)=>{var m=u(95981),v=u(31887);i.exports=!m((function(){var i=Error("a");return!("stack"in i)||(Object.defineProperty(i,"stack",v(1,7)),7!==i.stack)}))},76887:(i,s,u)=>{"use strict";var m=u(21899),v=u(79730),_=u(97484),j=u(57475),M=u(49677).f,$=u(37252),W=u(54058),X=u(86843),Y=u(32029),Z=u(90953),wrapConstructor=function(i){var Wrapper=function(s,u,m){if(this instanceof Wrapper){switch(arguments.length){case 0:return new i;case 1:return new i(s);case 2:return new i(s,u)}return new i(s,u,m)}return v(i,this,arguments)};return Wrapper.prototype=i.prototype,Wrapper};i.exports=function(i,s){var u,v,ee,ae,ie,le,ce,pe,de,fe=i.target,ye=i.global,be=i.stat,_e=i.proto,we=ye?m:be?m[fe]:(m[fe]||{}).prototype,Se=ye?W:W[fe]||Y(W,fe,{})[fe],xe=Se.prototype;for(ae in s)v=!(u=$(ye?ae:fe+(be?".":"#")+ae,i.forced))&&we&&Z(we,ae),le=Se[ae],v&&(ce=i.dontCallGetSet?(de=M(we,ae))&&de.value:we[ae]),ie=v&&ce?ce:s[ae],v&&typeof le==typeof ie||(pe=i.bind&&v?X(ie,m):i.wrap&&v?wrapConstructor(ie):_e&&j(ie)?_(ie):ie,(i.sham||ie&&ie.sham||le&&le.sham)&&Y(pe,"sham",!0),Y(Se,ae,pe),_e&&(Z(W,ee=fe+"Prototype")||Y(W,ee,{}),Y(W[ee],ae,ie),i.real&&xe&&(u||!xe[ae])&&Y(xe,ae,ie)))}},95981:i=>{i.exports=function(i){try{return!!i()}catch(i){return!0}}},79730:(i,s,u)=>{var m=u(18285),v=Function.prototype,_=v.apply,j=v.call;i.exports="object"==typeof Reflect&&Reflect.apply||(m?j.bind(_):function(){return j.apply(_,arguments)})},86843:(i,s,u)=>{var m=u(97484),v=u(24883),_=u(18285),j=m(m.bind);i.exports=function(i,s){return v(i),void 0===s?i:_?j(i,s):function(){return i.apply(s,arguments)}}},18285:(i,s,u)=>{var m=u(95981);i.exports=!m((function(){var i=function(){}.bind();return"function"!=typeof i||i.hasOwnProperty("prototype")}))},98308:(i,s,u)=>{"use strict";var m=u(95329),v=u(24883),_=u(10941),j=u(90953),M=u(93765),$=u(18285),W=Function,X=m([].concat),Y=m([].join),Z={};i.exports=$?W.bind:function bind(i){var s=v(this),u=s.prototype,m=M(arguments,1),$=function bound(){var u=X(m,M(arguments));return this instanceof $?function(i,s,u){if(!j(Z,s)){for(var m=[],v=0;v{var m=u(18285),v=Function.prototype.call;i.exports=m?v.bind(v):function(){return v.apply(v,arguments)}},79417:(i,s,u)=>{var m=u(55746),v=u(90953),_=Function.prototype,j=m&&Object.getOwnPropertyDescriptor,M=v(_,"name"),$=M&&"something"===function something(){}.name,W=M&&(!m||m&&j(_,"name").configurable);i.exports={EXISTS:M,PROPER:$,CONFIGURABLE:W}},45526:(i,s,u)=>{var m=u(95329),v=u(24883);i.exports=function(i,s,u){try{return m(v(Object.getOwnPropertyDescriptor(i,s)[u]))}catch(i){}}},97484:(i,s,u)=>{var m=u(82532),v=u(95329);i.exports=function(i){if("Function"===m(i))return v(i)}},95329:(i,s,u)=>{var m=u(18285),v=Function.prototype,_=v.call,j=m&&v.bind.bind(_,_);i.exports=m?j:function(i){return function(){return _.apply(i,arguments)}}},626:(i,s,u)=>{var m=u(54058),v=u(21899),_=u(57475),aFunction=function(i){return _(i)?i:void 0};i.exports=function(i,s){return arguments.length<2?aFunction(m[i])||aFunction(v[i]):m[i]&&m[i][s]||v[i]&&v[i][s]}},22902:(i,s,u)=>{var m=u(9697),v=u(14229),_=u(82119),j=u(12077),M=u(99813)("iterator");i.exports=function(i){if(!_(i))return v(i,M)||v(i,"@@iterator")||j[m(i)]}},53476:(i,s,u)=>{var m=u(78834),v=u(24883),_=u(96059),j=u(69826),M=u(22902),$=TypeError;i.exports=function(i,s){var u=arguments.length<2?M(i):s;if(v(u))return _(m(u,i));throw $(j(i)+" is not iterable")}},14229:(i,s,u)=>{var m=u(24883),v=u(82119);i.exports=function(i,s){var u=i[s];return v(u)?void 0:m(u)}},21899:function(i,s,u){var check=function(i){return i&&i.Math==Math&&i};i.exports=check("object"==typeof globalThis&&globalThis)||check("object"==typeof window&&window)||check("object"==typeof self&&self)||check("object"==typeof u.g&&u.g)||function(){return this}()||this||Function("return this")()},90953:(i,s,u)=>{var m=u(95329),v=u(89678),_=m({}.hasOwnProperty);i.exports=Object.hasOwn||function hasOwn(i,s){return _(v(i),s)}},27748:i=>{i.exports={}},15463:(i,s,u)=>{var m=u(626);i.exports=m("document","documentElement")},2840:(i,s,u)=>{var m=u(55746),v=u(95981),_=u(61333);i.exports=!m&&!v((function(){return 7!=Object.defineProperty(_("div"),"a",{get:function(){return 7}}).a}))},37026:(i,s,u)=>{var m=u(95329),v=u(95981),_=u(82532),j=Object,M=m("".split);i.exports=v((function(){return!j("z").propertyIsEnumerable(0)}))?function(i){return"String"==_(i)?M(i,""):j(i)}:j},70926:(i,s,u)=>{var m=u(57475),v=u(10941),_=u(88929);i.exports=function(i,s,u){var j,M;return _&&m(j=s.constructor)&&j!==u&&v(M=j.prototype)&&M!==u.prototype&&_(i,M),i}},53794:(i,s,u)=>{var m=u(10941),v=u(32029);i.exports=function(i,s){m(s)&&"cause"in s&&v(i,"cause",s.cause)}},45402:(i,s,u)=>{var m,v,_,j=u(47093),M=u(21899),$=u(10941),W=u(32029),X=u(90953),Y=u(63030),Z=u(44262),ee=u(27748),ae="Object already initialized",ie=M.TypeError,le=M.WeakMap;if(j||Y.state){var ce=Y.state||(Y.state=new le);ce.get=ce.get,ce.has=ce.has,ce.set=ce.set,m=function(i,s){if(ce.has(i))throw ie(ae);return s.facade=i,ce.set(i,s),s},v=function(i){return ce.get(i)||{}},_=function(i){return ce.has(i)}}else{var pe=Z("state");ee[pe]=!0,m=function(i,s){if(X(i,pe))throw ie(ae);return s.facade=i,W(i,pe,s),s},v=function(i){return X(i,pe)?i[pe]:{}},_=function(i){return X(i,pe)}}i.exports={set:m,get:v,has:_,enforce:function(i){return _(i)?v(i):m(i,{})},getterFor:function(i){return function(s){var u;if(!$(s)||(u=v(s)).type!==i)throw ie("Incompatible receiver, "+i+" required");return u}}}},6782:(i,s,u)=>{var m=u(99813),v=u(12077),_=m("iterator"),j=Array.prototype;i.exports=function(i){return void 0!==i&&(v.Array===i||j[_]===i)}},57475:(i,s,u)=>{var m=u(76616),v=m.all;i.exports=m.IS_HTMLDDA?function(i){return"function"==typeof i||i===v}:function(i){return"function"==typeof i}},37252:(i,s,u)=>{var m=u(95981),v=u(57475),_=/#|\.prototype\./,isForced=function(i,s){var u=M[j(i)];return u==W||u!=$&&(v(s)?m(s):!!s)},j=isForced.normalize=function(i){return String(i).replace(_,".").toLowerCase()},M=isForced.data={},$=isForced.NATIVE="N",W=isForced.POLYFILL="P";i.exports=isForced},82119:i=>{i.exports=function(i){return null==i}},10941:(i,s,u)=>{var m=u(57475),v=u(76616),_=v.all;i.exports=v.IS_HTMLDDA?function(i){return"object"==typeof i?null!==i:m(i)||i===_}:function(i){return"object"==typeof i?null!==i:m(i)}},82529:i=>{i.exports=!0},56664:(i,s,u)=>{var m=u(626),v=u(57475),_=u(7046),j=u(32302),M=Object;i.exports=j?function(i){return"symbol"==typeof i}:function(i){var s=m("Symbol");return v(s)&&_(s.prototype,M(i))}},93091:(i,s,u)=>{var m=u(86843),v=u(78834),_=u(96059),j=u(69826),M=u(6782),$=u(10623),W=u(7046),X=u(53476),Y=u(22902),Z=u(7609),ee=TypeError,Result=function(i,s){this.stopped=i,this.result=s},ae=Result.prototype;i.exports=function(i,s,u){var ie,le,ce,pe,de,fe,ye,be=u&&u.that,_e=!(!u||!u.AS_ENTRIES),we=!(!u||!u.IS_RECORD),Se=!(!u||!u.IS_ITERATOR),xe=!(!u||!u.INTERRUPTED),Pe=m(s,be),stop=function(i){return ie&&Z(ie,"normal",i),new Result(!0,i)},callFn=function(i){return _e?(_(i),xe?Pe(i[0],i[1],stop):Pe(i[0],i[1])):xe?Pe(i,stop):Pe(i)};if(we)ie=i.iterator;else if(Se)ie=i;else{if(!(le=Y(i)))throw ee(j(i)+" is not iterable");if(M(le)){for(ce=0,pe=$(i);pe>ce;ce++)if((de=callFn(i[ce]))&&W(ae,de))return de;return new Result(!1)}ie=X(i,le)}for(fe=we?i.next:ie.next;!(ye=v(fe,ie)).done;){try{de=callFn(ye.value)}catch(i){Z(ie,"throw",i)}if("object"==typeof de&&de&&W(ae,de))return de}return new Result(!1)}},7609:(i,s,u)=>{var m=u(78834),v=u(96059),_=u(14229);i.exports=function(i,s,u){var j,M;v(i);try{if(!(j=_(i,"return"))){if("throw"===s)throw u;return u}j=m(j,i)}catch(i){M=!0,j=i}if("throw"===s)throw u;if(M)throw j;return v(j),u}},53847:(i,s,u)=>{"use strict";var m=u(35143).IteratorPrototype,v=u(29290),_=u(31887),j=u(90904),M=u(12077),returnThis=function(){return this};i.exports=function(i,s,u,$){var W=s+" Iterator";return i.prototype=v(m,{next:_(+!$,u)}),j(i,W,!1,!0),M[W]=returnThis,i}},75105:(i,s,u)=>{"use strict";var m=u(76887),v=u(78834),_=u(82529),j=u(79417),M=u(57475),$=u(53847),W=u(249),X=u(88929),Y=u(90904),Z=u(32029),ee=u(95929),ae=u(99813),ie=u(12077),le=u(35143),ce=j.PROPER,pe=j.CONFIGURABLE,de=le.IteratorPrototype,fe=le.BUGGY_SAFARI_ITERATORS,ye=ae("iterator"),be="keys",_e="values",we="entries",returnThis=function(){return this};i.exports=function(i,s,u,j,ae,le,Se){$(u,s,j);var xe,Pe,Ie,getIterationMethod=function(i){if(i===ae&&Ve)return Ve;if(!fe&&i in qe)return qe[i];switch(i){case be:return function keys(){return new u(this,i)};case _e:return function values(){return new u(this,i)};case we:return function entries(){return new u(this,i)}}return function(){return new u(this)}},Te=s+" Iterator",Re=!1,qe=i.prototype,ze=qe[ye]||qe["@@iterator"]||ae&&qe[ae],Ve=!fe&&ze||getIterationMethod(ae),We="Array"==s&&qe.entries||ze;if(We&&(xe=W(We.call(new i)))!==Object.prototype&&xe.next&&(_||W(xe)===de||(X?X(xe,de):M(xe[ye])||ee(xe,ye,returnThis)),Y(xe,Te,!0,!0),_&&(ie[Te]=returnThis)),ce&&ae==_e&&ze&&ze.name!==_e&&(!_&&pe?Z(qe,"name",_e):(Re=!0,Ve=function values(){return v(ze,this)})),ae)if(Pe={values:getIterationMethod(_e),keys:le?Ve:getIterationMethod(be),entries:getIterationMethod(we)},Se)for(Ie in Pe)(fe||Re||!(Ie in qe))&&ee(qe,Ie,Pe[Ie]);else m({target:s,proto:!0,forced:fe||Re},Pe);return _&&!Se||qe[ye]===Ve||ee(qe,ye,Ve,{name:ae}),ie[s]=Ve,Pe}},35143:(i,s,u)=>{"use strict";var m,v,_,j=u(95981),M=u(57475),$=u(10941),W=u(29290),X=u(249),Y=u(95929),Z=u(99813),ee=u(82529),ae=Z("iterator"),ie=!1;[].keys&&("next"in(_=[].keys())?(v=X(X(_)))!==Object.prototype&&(m=v):ie=!0),!$(m)||j((function(){var i={};return m[ae].call(i)!==i}))?m={}:ee&&(m=W(m)),M(m[ae])||Y(m,ae,(function(){return this})),i.exports={IteratorPrototype:m,BUGGY_SAFARI_ITERATORS:ie}},12077:i=>{i.exports={}},10623:(i,s,u)=>{var m=u(43057);i.exports=function(i){return m(i.length)}},35331:i=>{var s=Math.ceil,u=Math.floor;i.exports=Math.trunc||function trunc(i){var m=+i;return(m>0?u:s)(m)}},14649:(i,s,u)=>{var m=u(85803);i.exports=function(i,s){return void 0===i?arguments.length<2?"":s:m(i)}},24420:(i,s,u)=>{"use strict";var m=u(55746),v=u(95329),_=u(78834),j=u(95981),M=u(14771),$=u(87857),W=u(36760),X=u(89678),Y=u(37026),Z=Object.assign,ee=Object.defineProperty,ae=v([].concat);i.exports=!Z||j((function(){if(m&&1!==Z({b:1},Z(ee({},"a",{enumerable:!0,get:function(){ee(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var i={},s={},u=Symbol(),v="abcdefghijklmnopqrst";return i[u]=7,v.split("").forEach((function(i){s[i]=i})),7!=Z({},i)[u]||M(Z({},s)).join("")!=v}))?function assign(i,s){for(var u=X(i),v=arguments.length,j=1,Z=$.f,ee=W.f;v>j;)for(var ie,le=Y(arguments[j++]),ce=Z?ae(M(le),Z(le)):M(le),pe=ce.length,de=0;pe>de;)ie=ce[de++],m&&!_(ee,le,ie)||(u[ie]=le[ie]);return u}:Z},29290:(i,s,u)=>{var m,v=u(96059),_=u(59938),j=u(56759),M=u(27748),$=u(15463),W=u(61333),X=u(44262),Y="prototype",Z="script",ee=X("IE_PROTO"),EmptyConstructor=function(){},scriptTag=function(i){return"<"+Z+">"+i+""},NullProtoObjectViaActiveX=function(i){i.write(scriptTag("")),i.close();var s=i.parentWindow.Object;return i=null,s},NullProtoObject=function(){try{m=new ActiveXObject("htmlfile")}catch(i){}var i,s,u;NullProtoObject="undefined"!=typeof document?document.domain&&m?NullProtoObjectViaActiveX(m):(s=W("iframe"),u="java"+Z+":",s.style.display="none",$.appendChild(s),s.src=String(u),(i=s.contentWindow.document).open(),i.write(scriptTag("document.F=Object")),i.close(),i.F):NullProtoObjectViaActiveX(m);for(var v=j.length;v--;)delete NullProtoObject[Y][j[v]];return NullProtoObject()};M[ee]=!0,i.exports=Object.create||function create(i,s){var u;return null!==i?(EmptyConstructor[Y]=v(i),u=new EmptyConstructor,EmptyConstructor[Y]=null,u[ee]=i):u=NullProtoObject(),void 0===s?u:_.f(u,s)}},59938:(i,s,u)=>{var m=u(55746),v=u(83937),_=u(65988),j=u(96059),M=u(74529),$=u(14771);s.f=m&&!v?Object.defineProperties:function defineProperties(i,s){j(i);for(var u,m=M(s),v=$(s),W=v.length,X=0;W>X;)_.f(i,u=v[X++],m[u]);return i}},65988:(i,s,u)=>{var m=u(55746),v=u(2840),_=u(83937),j=u(96059),M=u(83894),$=TypeError,W=Object.defineProperty,X=Object.getOwnPropertyDescriptor,Y="enumerable",Z="configurable",ee="writable";s.f=m?_?function defineProperty(i,s,u){if(j(i),s=M(s),j(u),"function"==typeof i&&"prototype"===s&&"value"in u&&ee in u&&!u[ee]){var m=X(i,s);m&&m[ee]&&(i[s]=u.value,u={configurable:Z in u?u[Z]:m[Z],enumerable:Y in u?u[Y]:m[Y],writable:!1})}return W(i,s,u)}:W:function defineProperty(i,s,u){if(j(i),s=M(s),j(u),v)try{return W(i,s,u)}catch(i){}if("get"in u||"set"in u)throw $("Accessors not supported");return"value"in u&&(i[s]=u.value),i}},49677:(i,s,u)=>{var m=u(55746),v=u(78834),_=u(36760),j=u(31887),M=u(74529),$=u(83894),W=u(90953),X=u(2840),Y=Object.getOwnPropertyDescriptor;s.f=m?Y:function getOwnPropertyDescriptor(i,s){if(i=M(i),s=$(s),X)try{return Y(i,s)}catch(i){}if(W(i,s))return j(!v(_.f,i,s),i[s])}},10946:(i,s,u)=>{var m=u(55629),v=u(56759).concat("length","prototype");s.f=Object.getOwnPropertyNames||function getOwnPropertyNames(i){return m(i,v)}},87857:(i,s)=>{s.f=Object.getOwnPropertySymbols},249:(i,s,u)=>{var m=u(90953),v=u(57475),_=u(89678),j=u(44262),M=u(91310),$=j("IE_PROTO"),W=Object,X=W.prototype;i.exports=M?W.getPrototypeOf:function(i){var s=_(i);if(m(s,$))return s[$];var u=s.constructor;return v(u)&&s instanceof u?u.prototype:s instanceof W?X:null}},7046:(i,s,u)=>{var m=u(95329);i.exports=m({}.isPrototypeOf)},55629:(i,s,u)=>{var m=u(95329),v=u(90953),_=u(74529),j=u(31692).indexOf,M=u(27748),$=m([].push);i.exports=function(i,s){var u,m=_(i),W=0,X=[];for(u in m)!v(M,u)&&v(m,u)&&$(X,u);for(;s.length>W;)v(m,u=s[W++])&&(~j(X,u)||$(X,u));return X}},14771:(i,s,u)=>{var m=u(55629),v=u(56759);i.exports=Object.keys||function keys(i){return m(i,v)}},36760:(i,s)=>{"use strict";var u={}.propertyIsEnumerable,m=Object.getOwnPropertyDescriptor,v=m&&!u.call({1:2},1);s.f=v?function propertyIsEnumerable(i){var s=m(this,i);return!!s&&s.enumerable}:u},88929:(i,s,u)=>{var m=u(45526),v=u(96059),_=u(11851);i.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var i,s=!1,u={};try{(i=m(Object.prototype,"__proto__","set"))(u,[]),s=u instanceof Array}catch(i){}return function setPrototypeOf(u,m){return v(u),_(m),s?i(u,m):u.__proto__=m,u}}():void 0)},95623:(i,s,u)=>{"use strict";var m=u(22885),v=u(9697);i.exports=m?{}.toString:function toString(){return"[object "+v(this)+"]"}},39811:(i,s,u)=>{var m=u(78834),v=u(57475),_=u(10941),j=TypeError;i.exports=function(i,s){var u,M;if("string"===s&&v(u=i.toString)&&!_(M=m(u,i)))return M;if(v(u=i.valueOf)&&!_(M=m(u,i)))return M;if("string"!==s&&v(u=i.toString)&&!_(M=m(u,i)))return M;throw j("Can't convert object to primitive value")}},31136:(i,s,u)=>{var m=u(626),v=u(95329),_=u(10946),j=u(87857),M=u(96059),$=v([].concat);i.exports=m("Reflect","ownKeys")||function ownKeys(i){var s=_.f(M(i)),u=j.f;return u?$(s,u(i)):s}},54058:i=>{i.exports={}},9056:(i,s,u)=>{var m=u(65988).f;i.exports=function(i,s,u){u in i||m(i,u,{configurable:!0,get:function(){return s[u]},set:function(i){s[u]=i}})}},48219:(i,s,u)=>{var m=u(82119),v=TypeError;i.exports=function(i){if(m(i))throw v("Can't call method on "+i);return i}},90904:(i,s,u)=>{var m=u(22885),v=u(65988).f,_=u(32029),j=u(90953),M=u(95623),$=u(99813)("toStringTag");i.exports=function(i,s,u,W){if(i){var X=u?i:i.prototype;j(X,$)||v(X,$,{configurable:!0,value:s}),W&&!m&&_(X,"toString",M)}}},44262:(i,s,u)=>{var m=u(68726),v=u(99418),_=m("keys");i.exports=function(i){return _[i]||(_[i]=v(i))}},63030:(i,s,u)=>{var m=u(21899),v=u(75609),_="__core-js_shared__",j=m[_]||v(_,{});i.exports=j},68726:(i,s,u)=>{var m=u(82529),v=u(63030);(i.exports=function(i,s){return v[i]||(v[i]=void 0!==s?s:{})})("versions",[]).push({version:"3.31.1",mode:m?"pure":"global",copyright:"© 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.31.1/LICENSE",source:"https://github.com/zloirock/core-js"})},64620:(i,s,u)=>{var m=u(95329),v=u(62435),_=u(85803),j=u(48219),M=m("".charAt),$=m("".charCodeAt),W=m("".slice),createMethod=function(i){return function(s,u){var m,X,Y=_(j(s)),Z=v(u),ee=Y.length;return Z<0||Z>=ee?i?"":void 0:(m=$(Y,Z))<55296||m>56319||Z+1===ee||(X=$(Y,Z+1))<56320||X>57343?i?M(Y,Z):m:i?W(Y,Z,Z+2):X-56320+(m-55296<<10)+65536}};i.exports={codeAt:createMethod(!1),charAt:createMethod(!0)}},63405:(i,s,u)=>{var m=u(53385),v=u(95981),_=u(21899).String;i.exports=!!Object.getOwnPropertySymbols&&!v((function(){var i=Symbol();return!_(i)||!(Object(i)instanceof Symbol)||!Symbol.sham&&m&&m<41}))},59413:(i,s,u)=>{var m=u(62435),v=Math.max,_=Math.min;i.exports=function(i,s){var u=m(i);return u<0?v(u+s,0):_(u,s)}},74529:(i,s,u)=>{var m=u(37026),v=u(48219);i.exports=function(i){return m(v(i))}},62435:(i,s,u)=>{var m=u(35331);i.exports=function(i){var s=+i;return s!=s||0===s?0:m(s)}},43057:(i,s,u)=>{var m=u(62435),v=Math.min;i.exports=function(i){return i>0?v(m(i),9007199254740991):0}},89678:(i,s,u)=>{var m=u(48219),v=Object;i.exports=function(i){return v(m(i))}},46935:(i,s,u)=>{var m=u(78834),v=u(10941),_=u(56664),j=u(14229),M=u(39811),$=u(99813),W=TypeError,X=$("toPrimitive");i.exports=function(i,s){if(!v(i)||_(i))return i;var u,$=j(i,X);if($){if(void 0===s&&(s="default"),u=m($,i,s),!v(u)||_(u))return u;throw W("Can't convert object to primitive value")}return void 0===s&&(s="number"),M(i,s)}},83894:(i,s,u)=>{var m=u(46935),v=u(56664);i.exports=function(i){var s=m(i,"string");return v(s)?s:s+""}},22885:(i,s,u)=>{var m={};m[u(99813)("toStringTag")]="z",i.exports="[object z]"===String(m)},85803:(i,s,u)=>{var m=u(9697),v=String;i.exports=function(i){if("Symbol"===m(i))throw TypeError("Cannot convert a Symbol value to a string");return v(i)}},69826:i=>{var s=String;i.exports=function(i){try{return s(i)}catch(i){return"Object"}}},99418:(i,s,u)=>{var m=u(95329),v=0,_=Math.random(),j=m(1..toString);i.exports=function(i){return"Symbol("+(void 0===i?"":i)+")_"+j(++v+_,36)}},32302:(i,s,u)=>{var m=u(63405);i.exports=m&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},83937:(i,s,u)=>{var m=u(55746),v=u(95981);i.exports=m&&v((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))},47093:(i,s,u)=>{var m=u(21899),v=u(57475),_=m.WeakMap;i.exports=v(_)&&/native code/.test(String(_))},99813:(i,s,u)=>{var m=u(21899),v=u(68726),_=u(90953),j=u(99418),M=u(63405),$=u(32302),W=m.Symbol,X=v("wks"),Y=$?W.for||W:W&&W.withoutSetter||j;i.exports=function(i){return _(X,i)||(X[i]=M&&_(W,i)?W[i]:Y("Symbol."+i)),X[i]}},62864:(i,s,u)=>{"use strict";var m=u(626),v=u(90953),_=u(32029),j=u(7046),M=u(88929),$=u(23489),W=u(9056),X=u(70926),Y=u(14649),Z=u(53794),ee=u(79585),ae=u(55746),ie=u(82529);i.exports=function(i,s,u,le){var ce="stackTraceLimit",pe=le?2:1,de=i.split("."),fe=de[de.length-1],ye=m.apply(null,de);if(ye){var be=ye.prototype;if(!ie&&v(be,"cause")&&delete be.cause,!u)return ye;var _e=m("Error"),we=s((function(i,s){var u=Y(le?s:i,void 0),m=le?new ye(i):new ye;return void 0!==u&&_(m,"message",u),ee(m,we,m.stack,2),this&&j(be,this)&&X(m,this,we),arguments.length>pe&&Z(m,arguments[pe]),m}));if(we.prototype=be,"Error"!==fe?M?M(we,_e):$(we,_e,{name:!0}):ae&&ce in ye&&(W(we,ye,ce),W(we,ye,"prepareStackTrace")),$(we,ye),!ie)try{be.name!==fe&&_(be,"name",fe),be.constructor=we}catch(i){}return we}}},24415:(i,s,u)=>{var m=u(76887),v=u(626),_=u(79730),j=u(95981),M=u(62864),$="AggregateError",W=v($),X=!j((function(){return 1!==W([1]).errors[0]}))&&j((function(){return 7!==W([1],$,{cause:7}).cause}));m({global:!0,constructor:!0,arity:2,forced:X},{AggregateError:M($,(function(i){return function AggregateError(s,u){return _(i,this,arguments)}}),X,!0)})},49812:(i,s,u)=>{"use strict";var m=u(76887),v=u(7046),_=u(249),j=u(88929),M=u(23489),$=u(29290),W=u(32029),X=u(31887),Y=u(53794),Z=u(79585),ee=u(93091),ae=u(14649),ie=u(99813)("toStringTag"),le=Error,ce=[].push,pe=function AggregateError(i,s){var u,m=v(de,this);j?u=j(le(),m?_(this):de):(u=m?this:$(de),W(u,ie,"Error")),void 0!==s&&W(u,"message",ae(s)),Z(u,pe,u.stack,1),arguments.length>2&&Y(u,arguments[2]);var M=[];return ee(i,ce,{that:M}),W(u,"errors",M),u};j?j(pe,le):M(pe,le,{name:!0});var de=pe.prototype=$(le.prototype,{constructor:X(1,pe),message:X(1,""),name:X(1,"AggregateError")});m({global:!0,constructor:!0,arity:2},{AggregateError:pe})},47627:(i,s,u)=>{u(49812)},66274:(i,s,u)=>{"use strict";var m=u(74529),v=u(18479),_=u(12077),j=u(45402),M=u(65988).f,$=u(75105),W=u(23538),X=u(82529),Y=u(55746),Z="Array Iterator",ee=j.set,ae=j.getterFor(Z);i.exports=$(Array,"Array",(function(i,s){ee(this,{type:Z,target:m(i),index:0,kind:s})}),(function(){var i=ae(this),s=i.target,u=i.kind,m=i.index++;return!s||m>=s.length?(i.target=void 0,W(void 0,!0)):W("keys"==u?m:"values"==u?s[m]:[m,s[m]],!1)}),"values");var ie=_.Arguments=_.Array;if(v("keys"),v("values"),v("entries"),!X&&Y&&"values"!==ie.name)try{M(ie,"name",{value:"values"})}catch(i){}},61181:(i,s,u)=>{var m=u(76887),v=u(21899),_=u(79730),j=u(62864),M="WebAssembly",$=v[M],W=7!==Error("e",{cause:7}).cause,exportGlobalErrorCauseWrapper=function(i,s){var u={};u[i]=j(i,s,W),m({global:!0,constructor:!0,arity:1,forced:W},u)},exportWebAssemblyErrorCauseWrapper=function(i,s){if($&&$[i]){var u={};u[i]=j(M+"."+i,s,W),m({target:M,stat:!0,constructor:!0,arity:1,forced:W},u)}};exportGlobalErrorCauseWrapper("Error",(function(i){return function Error(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("EvalError",(function(i){return function EvalError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("RangeError",(function(i){return function RangeError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("ReferenceError",(function(i){return function ReferenceError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("SyntaxError",(function(i){return function SyntaxError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("TypeError",(function(i){return function TypeError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("URIError",(function(i){return function URIError(s){return _(i,this,arguments)}})),exportWebAssemblyErrorCauseWrapper("CompileError",(function(i){return function CompileError(s){return _(i,this,arguments)}})),exportWebAssemblyErrorCauseWrapper("LinkError",(function(i){return function LinkError(s){return _(i,this,arguments)}})),exportWebAssemblyErrorCauseWrapper("RuntimeError",(function(i){return function RuntimeError(s){return _(i,this,arguments)}}))},73381:(i,s,u)=>{var m=u(76887),v=u(98308);m({target:"Function",proto:!0,forced:Function.bind!==v},{bind:v})},49221:(i,s,u)=>{var m=u(76887),v=u(24420);m({target:"Object",stat:!0,arity:2,forced:Object.assign!==v},{assign:v})},77971:(i,s,u)=>{"use strict";var m=u(64620).charAt,v=u(85803),_=u(45402),j=u(75105),M=u(23538),$="String Iterator",W=_.set,X=_.getterFor($);j(String,"String",(function(i){W(this,{type:$,string:v(i),index:0})}),(function next(){var i,s=X(this),u=s.string,v=s.index;return v>=u.length?M(void 0,!0):(i=m(u,v),s.index+=i.length,M(i,!1))}))},89731:(i,s,u)=>{u(47627)},7634:(i,s,u)=>{u(66274);var m=u(63281),v=u(21899),_=u(9697),j=u(32029),M=u(12077),$=u(99813)("toStringTag");for(var W in m){var X=v[W],Y=X&&X.prototype;Y&&_(Y)!==$&&j(Y,$,W),M[W]=M.Array}},18957:(i,s,u)=>{u(89731);var m=u(50415);u(7634),i.exports=m},28196:(i,s,u)=>{var m=u(16246);i.exports=m},63383:(i,s,u)=>{var m=u(45999);i.exports=m},8269:function(i,s,u){var m;m=void 0!==u.g?u.g:this,i.exports=function(i){if(i.CSS&&i.CSS.escape)return i.CSS.escape;var cssEscape=function(i){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var s,u=String(i),m=u.length,v=-1,_="",j=u.charCodeAt(0);++v=1&&s<=31||127==s||0==v&&s>=48&&s<=57||1==v&&s>=48&&s<=57&&45==j?"\\"+s.toString(16)+" ":0==v&&1==m&&45==s||!(s>=128||45==s||95==s||s>=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122)?"\\"+u.charAt(v):u.charAt(v):_+="�";return _};return i.CSS||(i.CSS={}),i.CSS.escape=cssEscape,cssEscape}(m)},27698:(i,s,u)=>{"use strict";var m=u(48764).Buffer;function isSpecificValue(i){return i instanceof m||i instanceof Date||i instanceof RegExp}function cloneSpecificValue(i){if(i instanceof m){var s=m.alloc?m.alloc(i.length):new m(i.length);return i.copy(s),s}if(i instanceof Date)return new Date(i.getTime());if(i instanceof RegExp)return new RegExp(i);throw new Error("Unexpected situation")}function deepCloneArray(i){var s=[];return i.forEach((function(i,u){"object"==typeof i&&null!==i?Array.isArray(i)?s[u]=deepCloneArray(i):isSpecificValue(i)?s[u]=cloneSpecificValue(i):s[u]=v({},i):s[u]=i})),s}function safeGetProperty(i,s){return"__proto__"===s?void 0:i[s]}var v=i.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var i,s,u=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(m){"object"!=typeof m||null===m||Array.isArray(m)||Object.keys(m).forEach((function(_){return s=safeGetProperty(u,_),(i=safeGetProperty(m,_))===u?void 0:"object"!=typeof i||null===i?void(u[_]=i):Array.isArray(i)?void(u[_]=deepCloneArray(i)):isSpecificValue(i)?void(u[_]=cloneSpecificValue(i)):"object"!=typeof s||null===s||Array.isArray(s)?void(u[_]=v({},i)):void(u[_]=v(s,i))}))})),u}},9996:i=>{"use strict";var s=function isMergeableObject(i){return function isNonNullObject(i){return!!i&&"object"==typeof i}(i)&&!function isSpecial(i){var s=Object.prototype.toString.call(i);return"[object RegExp]"===s||"[object Date]"===s||function isReactElement(i){return i.$$typeof===u}(i)}(i)};var u="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(i,s){return!1!==s.clone&&s.isMergeableObject(i)?deepmerge(function emptyTarget(i){return Array.isArray(i)?[]:{}}(i),i,s):i}function defaultArrayMerge(i,s,u){return i.concat(s).map((function(i){return cloneUnlessOtherwiseSpecified(i,u)}))}function getKeys(i){return Object.keys(i).concat(function getEnumerableOwnPropertySymbols(i){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(i).filter((function(s){return Object.propertyIsEnumerable.call(i,s)})):[]}(i))}function propertyIsOnObject(i,s){try{return s in i}catch(i){return!1}}function mergeObject(i,s,u){var m={};return u.isMergeableObject(i)&&getKeys(i).forEach((function(s){m[s]=cloneUnlessOtherwiseSpecified(i[s],u)})),getKeys(s).forEach((function(v){(function propertyIsUnsafe(i,s){return propertyIsOnObject(i,s)&&!(Object.hasOwnProperty.call(i,s)&&Object.propertyIsEnumerable.call(i,s))})(i,v)||(propertyIsOnObject(i,v)&&u.isMergeableObject(s[v])?m[v]=function getMergeFunction(i,s){if(!s.customMerge)return deepmerge;var u=s.customMerge(i);return"function"==typeof u?u:deepmerge}(v,u)(i[v],s[v],u):m[v]=cloneUnlessOtherwiseSpecified(s[v],u))})),m}function deepmerge(i,u,m){(m=m||{}).arrayMerge=m.arrayMerge||defaultArrayMerge,m.isMergeableObject=m.isMergeableObject||s,m.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var v=Array.isArray(u);return v===Array.isArray(i)?v?m.arrayMerge(i,u,m):mergeObject(i,u,m):cloneUnlessOtherwiseSpecified(u,m)}deepmerge.all=function deepmergeAll(i,s){if(!Array.isArray(i))throw new Error("first argument should be an array");return i.reduce((function(i,u){return deepmerge(i,u,s)}),{})};var m=deepmerge;i.exports=m},27856:function(i){i.exports=function(){"use strict";const{entries:i,setPrototypeOf:s,isFrozen:u,getPrototypeOf:m,getOwnPropertyDescriptor:v}=Object;let{freeze:_,seal:j,create:M}=Object,{apply:$,construct:W}="undefined"!=typeof Reflect&&Reflect;_||(_=function freeze(i){return i}),j||(j=function seal(i){return i}),$||($=function apply(i,s,u){return i.apply(s,u)}),W||(W=function construct(i,s){return new i(...s)});const X=unapply(Array.prototype.forEach),Y=unapply(Array.prototype.pop),Z=unapply(Array.prototype.push),ee=unapply(String.prototype.toLowerCase),ae=unapply(String.prototype.toString),ie=unapply(String.prototype.match),le=unapply(String.prototype.replace),ce=unapply(String.prototype.indexOf),pe=unapply(String.prototype.trim),de=unapply(RegExp.prototype.test),fe=unconstruct(TypeError);function unapply(i){return function(s){for(var u=arguments.length,m=new Array(u>1?u-1:0),v=1;v2&&void 0!==arguments[2]?arguments[2]:ee;s&&s(i,null);let _=m.length;for(;_--;){let s=m[_];if("string"==typeof s){const i=v(s);i!==s&&(u(m)||(m[_]=i),s=i)}i[s]=!0}return i}function clone(s){const u=M(null);for(const[m,_]of i(s))void 0!==v(s,m)&&(u[m]=_);return u}function lookupGetter(i,s){for(;null!==i;){const u=v(i,s);if(u){if(u.get)return unapply(u.get);if("function"==typeof u.value)return unapply(u.value)}i=m(i)}function fallbackValue(i){return console.warn("fallback value for",i),null}return fallbackValue}const ye=_(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),be=_(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),_e=_(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),we=_(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),Se=_(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),xe=_(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Pe=_(["#text"]),Ie=_(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),Te=_(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),Re=_(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),qe=_(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),ze=j(/\{\{[\w\W]*|[\w\W]*\}\}/gm),Ve=j(/<%[\w\W]*|[\w\W]*%>/gm),We=j(/\${[\w\W]*}/gm),He=j(/^data-[\-\w.\u00B7-\uFFFF]/),Xe=j(/^aria-[\-\w]+$/),Ye=j(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Qe=j(/^(?:\w+script|data):/i),et=j(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),tt=j(/^html$/i);var rt=Object.freeze({__proto__:null,MUSTACHE_EXPR:ze,ERB_EXPR:Ve,TMPLIT_EXPR:We,DATA_ATTR:He,ARIA_ATTR:Xe,IS_ALLOWED_URI:Ye,IS_SCRIPT_OR_DATA:Qe,ATTR_WHITESPACE:et,DOCTYPE_NAME:tt});const nt=function getGlobal(){return"undefined"==typeof window?null:window},ot=function _createTrustedTypesPolicy(i,s){if("object"!=typeof i||"function"!=typeof i.createPolicy)return null;let u=null;const m="data-tt-policy-suffix";s&&s.hasAttribute(m)&&(u=s.getAttribute(m));const v="dompurify"+(u?"#"+u:"");try{return i.createPolicy(v,{createHTML:i=>i,createScriptURL:i=>i})}catch(i){return console.warn("TrustedTypes policy "+v+" could not be created."),null}};function createDOMPurify(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:nt();const DOMPurify=i=>createDOMPurify(i);if(DOMPurify.version="3.0.6",DOMPurify.removed=[],!s||!s.document||9!==s.document.nodeType)return DOMPurify.isSupported=!1,DOMPurify;let{document:u}=s;const m=u,v=m.currentScript,{DocumentFragment:j,HTMLTemplateElement:$,Node:W,Element:ze,NodeFilter:Ve,NamedNodeMap:We=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:He,DOMParser:Xe,trustedTypes:Qe}=s,et=ze.prototype,at=lookupGetter(et,"cloneNode"),it=lookupGetter(et,"nextSibling"),st=lookupGetter(et,"childNodes"),lt=lookupGetter(et,"parentNode");if("function"==typeof $){const i=u.createElement("template");i.content&&i.content.ownerDocument&&(u=i.content.ownerDocument)}let ct,ut="";const{implementation:pt,createNodeIterator:ht,createDocumentFragment:dt,getElementsByTagName:mt}=u,{importNode:gt}=m;let yt={};DOMPurify.isSupported="function"==typeof i&&"function"==typeof lt&&pt&&void 0!==pt.createHTMLDocument;const{MUSTACHE_EXPR:vt,ERB_EXPR:bt,TMPLIT_EXPR:_t,DATA_ATTR:Et,ARIA_ATTR:wt,IS_SCRIPT_OR_DATA:St,ATTR_WHITESPACE:xt}=rt;let{IS_ALLOWED_URI:kt}=rt,Ot=null;const At=addToSet({},[...ye,...be,..._e,...Se,...Pe]);let Ct=null;const jt=addToSet({},[...Ie,...Te,...Re,...qe]);let Pt=Object.seal(M(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),It=null,Nt=null,Tt=!0,Mt=!0,Rt=!1,Bt=!0,Dt=!1,Lt=!1,Ft=!1,qt=!1,$t=!1,zt=!1,Ut=!1,Vt=!0,Wt=!1;const Kt="user-content-";let Ht=!0,Jt=!1,Gt={},Xt=null;const Yt=addToSet({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Qt=null;const Zt=addToSet({},["audio","video","img","source","image","track"]);let er=null;const tr=addToSet({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),rr="http://www.w3.org/1998/Math/MathML",nr="http://www.w3.org/2000/svg",ar="http://www.w3.org/1999/xhtml";let ir=ar,sr=!1,lr=null;const cr=addToSet({},[rr,nr,ar],ae);let ur=null;const pr=["application/xhtml+xml","text/html"],dr="text/html";let fr=null,mr=null;const gr=u.createElement("form"),yr=function isRegexOrFunction(i){return i instanceof RegExp||i instanceof Function},vr=function _parseConfig(){let i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!mr||mr!==i){if(i&&"object"==typeof i||(i={}),i=clone(i),ur=ur=-1===pr.indexOf(i.PARSER_MEDIA_TYPE)?dr:i.PARSER_MEDIA_TYPE,fr="application/xhtml+xml"===ur?ae:ee,Ot="ALLOWED_TAGS"in i?addToSet({},i.ALLOWED_TAGS,fr):At,Ct="ALLOWED_ATTR"in i?addToSet({},i.ALLOWED_ATTR,fr):jt,lr="ALLOWED_NAMESPACES"in i?addToSet({},i.ALLOWED_NAMESPACES,ae):cr,er="ADD_URI_SAFE_ATTR"in i?addToSet(clone(tr),i.ADD_URI_SAFE_ATTR,fr):tr,Qt="ADD_DATA_URI_TAGS"in i?addToSet(clone(Zt),i.ADD_DATA_URI_TAGS,fr):Zt,Xt="FORBID_CONTENTS"in i?addToSet({},i.FORBID_CONTENTS,fr):Yt,It="FORBID_TAGS"in i?addToSet({},i.FORBID_TAGS,fr):{},Nt="FORBID_ATTR"in i?addToSet({},i.FORBID_ATTR,fr):{},Gt="USE_PROFILES"in i&&i.USE_PROFILES,Tt=!1!==i.ALLOW_ARIA_ATTR,Mt=!1!==i.ALLOW_DATA_ATTR,Rt=i.ALLOW_UNKNOWN_PROTOCOLS||!1,Bt=!1!==i.ALLOW_SELF_CLOSE_IN_ATTR,Dt=i.SAFE_FOR_TEMPLATES||!1,Lt=i.WHOLE_DOCUMENT||!1,$t=i.RETURN_DOM||!1,zt=i.RETURN_DOM_FRAGMENT||!1,Ut=i.RETURN_TRUSTED_TYPE||!1,qt=i.FORCE_BODY||!1,Vt=!1!==i.SANITIZE_DOM,Wt=i.SANITIZE_NAMED_PROPS||!1,Ht=!1!==i.KEEP_CONTENT,Jt=i.IN_PLACE||!1,kt=i.ALLOWED_URI_REGEXP||Ye,ir=i.NAMESPACE||ar,Pt=i.CUSTOM_ELEMENT_HANDLING||{},i.CUSTOM_ELEMENT_HANDLING&&yr(i.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Pt.tagNameCheck=i.CUSTOM_ELEMENT_HANDLING.tagNameCheck),i.CUSTOM_ELEMENT_HANDLING&&yr(i.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Pt.attributeNameCheck=i.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),i.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof i.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Pt.allowCustomizedBuiltInElements=i.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Dt&&(Mt=!1),zt&&($t=!0),Gt&&(Ot=addToSet({},[...Pe]),Ct=[],!0===Gt.html&&(addToSet(Ot,ye),addToSet(Ct,Ie)),!0===Gt.svg&&(addToSet(Ot,be),addToSet(Ct,Te),addToSet(Ct,qe)),!0===Gt.svgFilters&&(addToSet(Ot,_e),addToSet(Ct,Te),addToSet(Ct,qe)),!0===Gt.mathMl&&(addToSet(Ot,Se),addToSet(Ct,Re),addToSet(Ct,qe))),i.ADD_TAGS&&(Ot===At&&(Ot=clone(Ot)),addToSet(Ot,i.ADD_TAGS,fr)),i.ADD_ATTR&&(Ct===jt&&(Ct=clone(Ct)),addToSet(Ct,i.ADD_ATTR,fr)),i.ADD_URI_SAFE_ATTR&&addToSet(er,i.ADD_URI_SAFE_ATTR,fr),i.FORBID_CONTENTS&&(Xt===Yt&&(Xt=clone(Xt)),addToSet(Xt,i.FORBID_CONTENTS,fr)),Ht&&(Ot["#text"]=!0),Lt&&addToSet(Ot,["html","head","body"]),Ot.table&&(addToSet(Ot,["tbody"]),delete It.tbody),i.TRUSTED_TYPES_POLICY){if("function"!=typeof i.TRUSTED_TYPES_POLICY.createHTML)throw fe('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof i.TRUSTED_TYPES_POLICY.createScriptURL)throw fe('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ct=i.TRUSTED_TYPES_POLICY,ut=ct.createHTML("")}else void 0===ct&&(ct=ot(Qe,v)),null!==ct&&"string"==typeof ut&&(ut=ct.createHTML(""));_&&_(i),mr=i}},br=addToSet({},["mi","mo","mn","ms","mtext"]),_r=addToSet({},["foreignobject","desc","title","annotation-xml"]),Er=addToSet({},["title","style","font","a","script"]),wr=addToSet({},be);addToSet(wr,_e),addToSet(wr,we);const Sr=addToSet({},Se);addToSet(Sr,xe);const xr=function _checkValidNamespace(i){let s=lt(i);s&&s.tagName||(s={namespaceURI:ir,tagName:"template"});const u=ee(i.tagName),m=ee(s.tagName);return!!lr[i.namespaceURI]&&(i.namespaceURI===nr?s.namespaceURI===ar?"svg"===u:s.namespaceURI===rr?"svg"===u&&("annotation-xml"===m||br[m]):Boolean(wr[u]):i.namespaceURI===rr?s.namespaceURI===ar?"math"===u:s.namespaceURI===nr?"math"===u&&_r[m]:Boolean(Sr[u]):i.namespaceURI===ar?!(s.namespaceURI===nr&&!_r[m])&&!(s.namespaceURI===rr&&!br[m])&&!Sr[u]&&(Er[u]||!wr[u]):!("application/xhtml+xml"!==ur||!lr[i.namespaceURI]))},kr=function _forceRemove(i){Z(DOMPurify.removed,{element:i});try{i.parentNode.removeChild(i)}catch(s){i.remove()}},Or=function _removeAttribute(i,s){try{Z(DOMPurify.removed,{attribute:s.getAttributeNode(i),from:s})}catch(i){Z(DOMPurify.removed,{attribute:null,from:s})}if(s.removeAttribute(i),"is"===i&&!Ct[i])if($t||zt)try{kr(s)}catch(i){}else try{s.setAttribute(i,"")}catch(i){}},Ar=function _initDocument(i){let s=null,m=null;if(qt)i=""+i;else{const s=ie(i,/^[\r\n\t ]+/);m=s&&s[0]}"application/xhtml+xml"===ur&&ir===ar&&(i=''+i+"");const v=ct?ct.createHTML(i):i;if(ir===ar)try{s=(new Xe).parseFromString(v,ur)}catch(i){}if(!s||!s.documentElement){s=pt.createDocument(ir,"template",null);try{s.documentElement.innerHTML=sr?ut:v}catch(i){}}const _=s.body||s.documentElement;return i&&m&&_.insertBefore(u.createTextNode(m),_.childNodes[0]||null),ir===ar?mt.call(s,Lt?"html":"body")[0]:Lt?s.documentElement:_},Cr=function _createNodeIterator(i){return ht.call(i.ownerDocument||i,i,Ve.SHOW_ELEMENT|Ve.SHOW_COMMENT|Ve.SHOW_TEXT,null)},jr=function _isClobbered(i){return i instanceof He&&("string"!=typeof i.nodeName||"string"!=typeof i.textContent||"function"!=typeof i.removeChild||!(i.attributes instanceof We)||"function"!=typeof i.removeAttribute||"function"!=typeof i.setAttribute||"string"!=typeof i.namespaceURI||"function"!=typeof i.insertBefore||"function"!=typeof i.hasChildNodes)},Pr=function _isNode(i){return"function"==typeof W&&i instanceof W},Ir=function _executeHook(i,s,u){yt[i]&&X(yt[i],(i=>{i.call(DOMPurify,s,u,mr)}))},Nr=function _sanitizeElements(i){let s=null;if(Ir("beforeSanitizeElements",i,null),jr(i))return kr(i),!0;const u=fr(i.nodeName);if(Ir("uponSanitizeElement",i,{tagName:u,allowedTags:Ot}),i.hasChildNodes()&&!Pr(i.firstElementChild)&&de(/<[/\w]/g,i.innerHTML)&&de(/<[/\w]/g,i.textContent))return kr(i),!0;if(!Ot[u]||It[u]){if(!It[u]&&Mr(u)){if(Pt.tagNameCheck instanceof RegExp&&de(Pt.tagNameCheck,u))return!1;if(Pt.tagNameCheck instanceof Function&&Pt.tagNameCheck(u))return!1}if(Ht&&!Xt[u]){const s=lt(i)||i.parentNode,u=st(i)||i.childNodes;if(u&&s)for(let m=u.length-1;m>=0;--m)s.insertBefore(at(u[m],!0),it(i))}return kr(i),!0}return i instanceof ze&&!xr(i)?(kr(i),!0):"noscript"!==u&&"noembed"!==u&&"noframes"!==u||!de(/<\/no(script|embed|frames)/i,i.innerHTML)?(Dt&&3===i.nodeType&&(s=i.textContent,X([vt,bt,_t],(i=>{s=le(s,i," ")})),i.textContent!==s&&(Z(DOMPurify.removed,{element:i.cloneNode()}),i.textContent=s)),Ir("afterSanitizeElements",i,null),!1):(kr(i),!0)},Tr=function _isValidAttribute(i,s,m){if(Vt&&("id"===s||"name"===s)&&(m in u||m in gr))return!1;if(Mt&&!Nt[s]&&de(Et,s));else if(Tt&&de(wt,s));else if(!Ct[s]||Nt[s]){if(!(Mr(i)&&(Pt.tagNameCheck instanceof RegExp&&de(Pt.tagNameCheck,i)||Pt.tagNameCheck instanceof Function&&Pt.tagNameCheck(i))&&(Pt.attributeNameCheck instanceof RegExp&&de(Pt.attributeNameCheck,s)||Pt.attributeNameCheck instanceof Function&&Pt.attributeNameCheck(s))||"is"===s&&Pt.allowCustomizedBuiltInElements&&(Pt.tagNameCheck instanceof RegExp&&de(Pt.tagNameCheck,m)||Pt.tagNameCheck instanceof Function&&Pt.tagNameCheck(m))))return!1}else if(er[s]);else if(de(kt,le(m,xt,"")));else if("src"!==s&&"xlink:href"!==s&&"href"!==s||"script"===i||0!==ce(m,"data:")||!Qt[i])if(Rt&&!de(St,le(m,xt,"")));else if(m)return!1;return!0},Mr=function _isBasicCustomElement(i){return i.indexOf("-")>0},Rr=function _sanitizeAttributes(i){Ir("beforeSanitizeAttributes",i,null);const{attributes:s}=i;if(!s)return;const u={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ct};let m=s.length;for(;m--;){const v=s[m],{name:_,namespaceURI:j,value:M}=v,$=fr(_);let W="value"===_?M:pe(M);if(u.attrName=$,u.attrValue=W,u.keepAttr=!0,u.forceKeepAttr=void 0,Ir("uponSanitizeAttribute",i,u),W=u.attrValue,u.forceKeepAttr)continue;if(Or(_,i),!u.keepAttr)continue;if(!Bt&&de(/\/>/i,W)){Or(_,i);continue}Dt&&X([vt,bt,_t],(i=>{W=le(W,i," ")}));const Z=fr(i.nodeName);if(Tr(Z,$,W)){if(!Wt||"id"!==$&&"name"!==$||(Or(_,i),W=Kt+W),ct&&"object"==typeof Qe&&"function"==typeof Qe.getAttributeType)if(j);else switch(Qe.getAttributeType(Z,$)){case"TrustedHTML":W=ct.createHTML(W);break;case"TrustedScriptURL":W=ct.createScriptURL(W)}try{j?i.setAttributeNS(j,_,W):i.setAttribute(_,W),Y(DOMPurify.removed)}catch(i){}}}Ir("afterSanitizeAttributes",i,null)},Br=function _sanitizeShadowDOM(i){let s=null;const u=Cr(i);for(Ir("beforeSanitizeShadowDOM",i,null);s=u.nextNode();)Ir("uponSanitizeShadowNode",s,null),Nr(s)||(s.content instanceof j&&_sanitizeShadowDOM(s.content),Rr(s));Ir("afterSanitizeShadowDOM",i,null)};return DOMPurify.sanitize=function(i){let s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},u=null,v=null,_=null,M=null;if(sr=!i,sr&&(i="\x3c!--\x3e"),"string"!=typeof i&&!Pr(i)){if("function"!=typeof i.toString)throw fe("toString is not a function");if("string"!=typeof(i=i.toString()))throw fe("dirty is not a string, aborting")}if(!DOMPurify.isSupported)return i;if(Ft||vr(s),DOMPurify.removed=[],"string"==typeof i&&(Jt=!1),Jt){if(i.nodeName){const s=fr(i.nodeName);if(!Ot[s]||It[s])throw fe("root node is forbidden and cannot be sanitized in-place")}}else if(i instanceof W)u=Ar("\x3c!----\x3e"),v=u.ownerDocument.importNode(i,!0),1===v.nodeType&&"BODY"===v.nodeName||"HTML"===v.nodeName?u=v:u.appendChild(v);else{if(!$t&&!Dt&&!Lt&&-1===i.indexOf("<"))return ct&&Ut?ct.createHTML(i):i;if(u=Ar(i),!u)return $t?null:Ut?ut:""}u&&qt&&kr(u.firstChild);const $=Cr(Jt?i:u);for(;_=$.nextNode();)Nr(_)||(_.content instanceof j&&Br(_.content),Rr(_));if(Jt)return i;if($t){if(zt)for(M=dt.call(u.ownerDocument);u.firstChild;)M.appendChild(u.firstChild);else M=u;return(Ct.shadowroot||Ct.shadowrootmode)&&(M=gt.call(m,M,!0)),M}let Y=Lt?u.outerHTML:u.innerHTML;return Lt&&Ot["!doctype"]&&u.ownerDocument&&u.ownerDocument.doctype&&u.ownerDocument.doctype.name&&de(tt,u.ownerDocument.doctype.name)&&(Y="\n"+Y),Dt&&X([vt,bt,_t],(i=>{Y=le(Y,i," ")})),ct&&Ut?ct.createHTML(Y):Y},DOMPurify.setConfig=function(){vr(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ft=!0},DOMPurify.clearConfig=function(){mr=null,Ft=!1},DOMPurify.isValidAttribute=function(i,s,u){mr||vr({});const m=fr(i),v=fr(s);return Tr(m,v,u)},DOMPurify.addHook=function(i,s){"function"==typeof s&&(yt[i]=yt[i]||[],Z(yt[i],s))},DOMPurify.removeHook=function(i){if(yt[i])return Y(yt[i])},DOMPurify.removeHooks=function(i){yt[i]&&(yt[i]=[])},DOMPurify.removeAllHooks=function(){yt={}},DOMPurify}return createDOMPurify()}()},69450:i=>{"use strict";class SubRange{constructor(i,s){this.low=i,this.high=s,this.length=1+s-i}overlaps(i){return!(this.highi.high)}touches(i){return!(this.high+1i.high)}add(i){return new SubRange(Math.min(this.low,i.low),Math.max(this.high,i.high))}subtract(i){return i.low<=this.low&&i.high>=this.high?[]:i.low>this.low&&i.highi+s.length),0)}add(i,s){var _add=i=>{for(var s=0;s{for(var s=0;s{for(var s=0;s{for(var u=s.low;u<=s.high;)i.push(u),u++;return i}),[])}subranges(){return this.ranges.map((i=>({low:i.low,high:i.high,length:1+i.high-i.low})))}}i.exports=DRange},17187:i=>{"use strict";var s,u="object"==typeof Reflect?Reflect:null,m=u&&"function"==typeof u.apply?u.apply:function ReflectApply(i,s,u){return Function.prototype.apply.call(i,s,u)};s=u&&"function"==typeof u.ownKeys?u.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(i){return Object.getOwnPropertyNames(i).concat(Object.getOwnPropertySymbols(i))}:function ReflectOwnKeys(i){return Object.getOwnPropertyNames(i)};var v=Number.isNaN||function NumberIsNaN(i){return i!=i};function EventEmitter(){EventEmitter.init.call(this)}i.exports=EventEmitter,i.exports.once=function once(i,s){return new Promise((function(u,m){function errorListener(u){i.removeListener(s,resolver),m(u)}function resolver(){"function"==typeof i.removeListener&&i.removeListener("error",errorListener),u([].slice.call(arguments))}eventTargetAgnosticAddListener(i,s,resolver,{once:!0}),"error"!==s&&function addErrorHandlerIfEventEmitter(i,s,u){"function"==typeof i.on&&eventTargetAgnosticAddListener(i,"error",s,u)}(i,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var _=10;function checkListener(i){if("function"!=typeof i)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof i)}function _getMaxListeners(i){return void 0===i._maxListeners?EventEmitter.defaultMaxListeners:i._maxListeners}function _addListener(i,s,u,m){var v,_,j;if(checkListener(u),void 0===(_=i._events)?(_=i._events=Object.create(null),i._eventsCount=0):(void 0!==_.newListener&&(i.emit("newListener",s,u.listener?u.listener:u),_=i._events),j=_[s]),void 0===j)j=_[s]=u,++i._eventsCount;else if("function"==typeof j?j=_[s]=m?[u,j]:[j,u]:m?j.unshift(u):j.push(u),(v=_getMaxListeners(i))>0&&j.length>v&&!j.warned){j.warned=!0;var M=new Error("Possible EventEmitter memory leak detected. "+j.length+" "+String(s)+" listeners added. Use emitter.setMaxListeners() to increase limit");M.name="MaxListenersExceededWarning",M.emitter=i,M.type=s,M.count=j.length,function ProcessEmitWarning(i){console&&console.warn&&console.warn(i)}(M)}return i}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(i,s,u){var m={fired:!1,wrapFn:void 0,target:i,type:s,listener:u},v=onceWrapper.bind(m);return v.listener=u,m.wrapFn=v,v}function _listeners(i,s,u){var m=i._events;if(void 0===m)return[];var v=m[s];return void 0===v?[]:"function"==typeof v?u?[v.listener||v]:[v]:u?function unwrapListeners(i){for(var s=new Array(i.length),u=0;u0&&(j=s[0]),j instanceof Error)throw j;var M=new Error("Unhandled error."+(j?" ("+j.message+")":""));throw M.context=j,M}var $=_[i];if(void 0===$)return!1;if("function"==typeof $)m($,this,s);else{var W=$.length,X=arrayClone($,W);for(u=0;u=0;_--)if(u[_]===s||u[_].listener===s){j=u[_].listener,v=_;break}if(v<0)return this;0===v?u.shift():function spliceOne(i,s){for(;s+1=0;m--)this.removeListener(i,s[m]);return this},EventEmitter.prototype.listeners=function listeners(i){return _listeners(this,i,!0)},EventEmitter.prototype.rawListeners=function rawListeners(i){return _listeners(this,i,!1)},EventEmitter.listenerCount=function(i,s){return"function"==typeof i.listenerCount?i.listenerCount(s):listenerCount.call(i,s)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?s(this._events):[]}},21102:(i,s,u)=>{"use strict";var m=u(46291),v=create(Error);function create(i){return FormattedError.displayName=i.displayName||i.name,FormattedError;function FormattedError(s){return s&&(s=m.apply(null,arguments)),new i(s)}}i.exports=v,v.eval=create(EvalError),v.range=create(RangeError),v.reference=create(ReferenceError),v.syntax=create(SyntaxError),v.type=create(TypeError),v.uri=create(URIError),v.create=create},46291:i=>{!function(){var s;function format(i){for(var s,u,m,v,_=1,j=[].slice.call(arguments),M=0,$=i.length,W="",X=!1,Y=!1,nextArg=function(){return j[_++]},slurpNumber=function(){for(var u="";/\d/.test(i[M]);)u+=i[M++],s=i[M];return u.length>0?parseInt(u):null};M<$;++M)if(s=i[M],X)switch(X=!1,"."==s?(Y=!1,s=i[++M]):"0"==s&&"."==i[M+1]?(Y=!0,s=i[M+=2]):Y=!0,v=slurpNumber(),s){case"b":W+=parseInt(nextArg(),10).toString(2);break;case"c":W+="string"==typeof(u=nextArg())||u instanceof String?u:String.fromCharCode(parseInt(u,10));break;case"d":W+=parseInt(nextArg(),10);break;case"f":m=String(parseFloat(nextArg()).toFixed(v||6)),W+=Y?m:m.replace(/^0/,"");break;case"j":W+=JSON.stringify(nextArg());break;case"o":W+="0"+parseInt(nextArg(),10).toString(8);break;case"s":W+=nextArg();break;case"x":W+="0x"+parseInt(nextArg(),10).toString(16);break;case"X":W+="0x"+parseInt(nextArg(),10).toString(16).toUpperCase();break;default:W+=s}else"%"===s?X=!0:W+=s;return W}(s=i.exports=format).format=format,s.vsprintf=function vsprintf(i,s){return format.apply(null,[i].concat(s))},"undefined"!=typeof console&&"function"==typeof console.log&&(s.printf=function printf(){console.log(format.apply(null,arguments))})}()},17648:i=>{"use strict";var s=Array.prototype.slice,u=Object.prototype.toString;i.exports=function bind(i){var m=this;if("function"!=typeof m||"[object Function]"!==u.call(m))throw new TypeError("Function.prototype.bind called on incompatible "+m);for(var v,_=s.call(arguments,1),j=Math.max(0,m.length-_.length),M=[],$=0;${"use strict";var m=u(17648);i.exports=Function.prototype.bind||m},40210:(i,s,u)=>{"use strict";var m,v=SyntaxError,_=Function,j=TypeError,getEvalledConstructor=function(i){try{return _('"use strict"; return ('+i+").constructor;")()}catch(i){}},M=Object.getOwnPropertyDescriptor;if(M)try{M({},"")}catch(i){M=null}var throwTypeError=function(){throw new j},$=M?function(){try{return throwTypeError}catch(i){try{return M(arguments,"callee").get}catch(i){return throwTypeError}}}():throwTypeError,W=u(41405)(),X=u(28185)(),Y=Object.getPrototypeOf||(X?function(i){return i.__proto__}:null),Z={},ee="undefined"!=typeof Uint8Array&&Y?Y(Uint8Array):m,ae={"%AggregateError%":"undefined"==typeof AggregateError?m:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?m:ArrayBuffer,"%ArrayIteratorPrototype%":W&&Y?Y([][Symbol.iterator]()):m,"%AsyncFromSyncIteratorPrototype%":m,"%AsyncFunction%":Z,"%AsyncGenerator%":Z,"%AsyncGeneratorFunction%":Z,"%AsyncIteratorPrototype%":Z,"%Atomics%":"undefined"==typeof Atomics?m:Atomics,"%BigInt%":"undefined"==typeof BigInt?m:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?m:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?m:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?m:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?m:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?m:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?m:FinalizationRegistry,"%Function%":_,"%GeneratorFunction%":Z,"%Int8Array%":"undefined"==typeof Int8Array?m:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?m:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?m:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":W&&Y?Y(Y([][Symbol.iterator]())):m,"%JSON%":"object"==typeof JSON?JSON:m,"%Map%":"undefined"==typeof Map?m:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&W&&Y?Y((new Map)[Symbol.iterator]()):m,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?m:Promise,"%Proxy%":"undefined"==typeof Proxy?m:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?m:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?m:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&W&&Y?Y((new Set)[Symbol.iterator]()):m,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?m:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":W&&Y?Y(""[Symbol.iterator]()):m,"%Symbol%":W?Symbol:m,"%SyntaxError%":v,"%ThrowTypeError%":$,"%TypedArray%":ee,"%TypeError%":j,"%Uint8Array%":"undefined"==typeof Uint8Array?m:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?m:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?m:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?m:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?m:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?m:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?m:WeakSet};if(Y)try{null.error}catch(i){var ie=Y(Y(i));ae["%Error.prototype%"]=ie}var le=function doEval(i){var s;if("%AsyncFunction%"===i)s=getEvalledConstructor("async function () {}");else if("%GeneratorFunction%"===i)s=getEvalledConstructor("function* () {}");else if("%AsyncGeneratorFunction%"===i)s=getEvalledConstructor("async function* () {}");else if("%AsyncGenerator%"===i){var u=doEval("%AsyncGeneratorFunction%");u&&(s=u.prototype)}else if("%AsyncIteratorPrototype%"===i){var m=doEval("%AsyncGenerator%");m&&Y&&(s=Y(m.prototype))}return ae[i]=s,s},ce={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},pe=u(58612),de=u(17642),fe=pe.call(Function.call,Array.prototype.concat),ye=pe.call(Function.apply,Array.prototype.splice),be=pe.call(Function.call,String.prototype.replace),_e=pe.call(Function.call,String.prototype.slice),we=pe.call(Function.call,RegExp.prototype.exec),Se=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,xe=/\\(\\)?/g,Pe=function getBaseIntrinsic(i,s){var u,m=i;if(de(ce,m)&&(m="%"+(u=ce[m])[0]+"%"),de(ae,m)){var _=ae[m];if(_===Z&&(_=le(m)),void 0===_&&!s)throw new j("intrinsic "+i+" exists, but is not available. Please file an issue!");return{alias:u,name:m,value:_}}throw new v("intrinsic "+i+" does not exist!")};i.exports=function GetIntrinsic(i,s){if("string"!=typeof i||0===i.length)throw new j("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof s)throw new j('"allowMissing" argument must be a boolean');if(null===we(/^%?[^%]*%?$/,i))throw new v("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var u=function stringToPath(i){var s=_e(i,0,1),u=_e(i,-1);if("%"===s&&"%"!==u)throw new v("invalid intrinsic syntax, expected closing `%`");if("%"===u&&"%"!==s)throw new v("invalid intrinsic syntax, expected opening `%`");var m=[];return be(i,Se,(function(i,s,u,v){m[m.length]=u?be(v,xe,"$1"):s||i})),m}(i),m=u.length>0?u[0]:"",_=Pe("%"+m+"%",s),$=_.name,W=_.value,X=!1,Y=_.alias;Y&&(m=Y[0],ye(u,fe([0,1],Y)));for(var Z=1,ee=!0;Z=u.length){var pe=M(W,ie);W=(ee=!!pe)&&"get"in pe&&!("originalValue"in pe.get)?pe.get:W[ie]}else ee=de(W,ie),W=W[ie];ee&&!X&&(ae[$]=W)}}return W}},28185:i=>{"use strict";var s={foo:{}},u=Object;i.exports=function hasProto(){return{__proto__:s}.foo===s.foo&&!({__proto__:null}instanceof u)}},41405:(i,s,u)=>{"use strict";var m="undefined"!=typeof Symbol&&Symbol,v=u(55419);i.exports=function hasNativeSymbols(){return"function"==typeof m&&("function"==typeof Symbol&&("symbol"==typeof m("foo")&&("symbol"==typeof Symbol("bar")&&v())))}},55419:i=>{"use strict";i.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var i={},s=Symbol("test"),u=Object(s);if("string"==typeof s)return!1;if("[object Symbol]"!==Object.prototype.toString.call(s))return!1;if("[object Symbol]"!==Object.prototype.toString.call(u))return!1;for(s in i[s]=42,i)return!1;if("function"==typeof Object.keys&&0!==Object.keys(i).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(i).length)return!1;var m=Object.getOwnPropertySymbols(i);if(1!==m.length||m[0]!==s)return!1;if(!Object.prototype.propertyIsEnumerable.call(i,s))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var v=Object.getOwnPropertyDescriptor(i,s);if(42!==v.value||!0!==v.enumerable)return!1}return!0}},17642:(i,s,u)=>{"use strict";var m=u(58612);i.exports=m.call(Function.call,Object.prototype.hasOwnProperty)},47802:i=>{function deepFreeze(i){return i instanceof Map?i.clear=i.delete=i.set=function(){throw new Error("map is read-only")}:i instanceof Set&&(i.add=i.clear=i.delete=function(){throw new Error("set is read-only")}),Object.freeze(i),Object.getOwnPropertyNames(i).forEach((function(s){var u=i[s];"object"!=typeof u||Object.isFrozen(u)||deepFreeze(u)})),i}var s=deepFreeze,u=deepFreeze;s.default=u;class Response{constructor(i){void 0===i.data&&(i.data={}),this.data=i.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(i){return i.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(i,...s){const u=Object.create(null);for(const s in i)u[s]=i[s];return s.forEach((function(i){for(const s in i)u[s]=i[s]})),u}const emitsWrappingTags=i=>!!i.kind;class HTMLRenderer{constructor(i,s){this.buffer="",this.classPrefix=s.classPrefix,i.walk(this)}addText(i){this.buffer+=escapeHTML(i)}openNode(i){if(!emitsWrappingTags(i))return;let s=i.kind;i.sublanguage||(s=`${this.classPrefix}${s}`),this.span(s)}closeNode(i){emitsWrappingTags(i)&&(this.buffer+="")}value(){return this.buffer}span(i){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(i){this.top.children.push(i)}openNode(i){const s={kind:i,children:[]};this.add(s),this.stack.push(s)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(i){return this.constructor._walk(i,this.rootNode)}static _walk(i,s){return"string"==typeof s?i.addText(s):s.children&&(i.openNode(s),s.children.forEach((s=>this._walk(i,s))),i.closeNode(s)),i}static _collapse(i){"string"!=typeof i&&i.children&&(i.children.every((i=>"string"==typeof i))?i.children=[i.children.join("")]:i.children.forEach((i=>{TokenTree._collapse(i)})))}}class TokenTreeEmitter extends TokenTree{constructor(i){super(),this.options=i}addKeyword(i,s){""!==i&&(this.openNode(s),this.addText(i),this.closeNode())}addText(i){""!==i&&this.add(i)}addSublanguage(i,s){const u=i.root;u.kind=s,u.sublanguage=!0,this.add(u)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(i){return i?"string"==typeof i?i:i.source:null}const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const v="[a-zA-Z]\\w*",_="[a-zA-Z_]\\w*",j="\\b\\d+(\\.\\d+)?",M="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",$="\\b(0b[01]+)",W={begin:"\\\\[\\s\\S]",relevance:0},X={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[W]},Y={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[W]},Z={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(i,s,u={}){const m=inherit({className:"comment",begin:i,end:s,contains:[]},u);return m.contains.push(Z),m.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),m},ee=COMMENT("//","$"),ae=COMMENT("/\\*","\\*/"),ie=COMMENT("#","$"),le={className:"number",begin:j,relevance:0},ce={className:"number",begin:M,relevance:0},pe={className:"number",begin:$,relevance:0},de={className:"number",begin:j+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},fe={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[W,{begin:/\[/,end:/\]/,relevance:0,contains:[W]}]}]},ye={className:"title",begin:v,relevance:0},be={className:"title",begin:_,relevance:0},_e={begin:"\\.\\s*"+_,relevance:0};var we=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:v,UNDERSCORE_IDENT_RE:_,NUMBER_RE:j,C_NUMBER_RE:M,BINARY_NUMBER_RE:$,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(i={})=>{const s=/^#![ ]*\//;return i.binary&&(i.begin=function concat(...i){return i.map((i=>source(i))).join("")}(s,/.*\b/,i.binary,/\b.*/)),inherit({className:"meta",begin:s,end:/$/,relevance:0,"on:begin":(i,s)=>{0!==i.index&&s.ignoreMatch()}},i)},BACKSLASH_ESCAPE:W,APOS_STRING_MODE:X,QUOTE_STRING_MODE:Y,PHRASAL_WORDS_MODE:Z,COMMENT,C_LINE_COMMENT_MODE:ee,C_BLOCK_COMMENT_MODE:ae,HASH_COMMENT_MODE:ie,NUMBER_MODE:le,C_NUMBER_MODE:ce,BINARY_NUMBER_MODE:pe,CSS_NUMBER_MODE:de,REGEXP_MODE:fe,TITLE_MODE:ye,UNDERSCORE_TITLE_MODE:be,METHOD_GUARD:_e,END_SAME_AS_BEGIN:function(i){return Object.assign(i,{"on:begin":(i,s)=>{s.data._beginMatch=i[1]},"on:end":(i,s)=>{s.data._beginMatch!==i[1]&&s.ignoreMatch()}})}});function skipIfhasPrecedingDot(i,s){"."===i.input[i.index-1]&&s.ignoreMatch()}function beginKeywords(i,s){s&&i.beginKeywords&&(i.begin="\\b("+i.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",i.__beforeBegin=skipIfhasPrecedingDot,i.keywords=i.keywords||i.beginKeywords,delete i.beginKeywords,void 0===i.relevance&&(i.relevance=0))}function compileIllegal(i,s){Array.isArray(i.illegal)&&(i.illegal=function either(...i){return"("+i.map((i=>source(i))).join("|")+")"}(...i.illegal))}function compileMatch(i,s){if(i.match){if(i.begin||i.end)throw new Error("begin & end are not supported with match");i.begin=i.match,delete i.match}}function compileRelevance(i,s){void 0===i.relevance&&(i.relevance=1)}const Se=["of","and","for","in","not","or","if","then","parent","list","value"],xe="keyword";function compileKeywords(i,s,u=xe){const m={};return"string"==typeof i?compileList(u,i.split(" ")):Array.isArray(i)?compileList(u,i):Object.keys(i).forEach((function(u){Object.assign(m,compileKeywords(i[u],s,u))})),m;function compileList(i,u){s&&(u=u.map((i=>i.toLowerCase()))),u.forEach((function(s){const u=s.split("|");m[u[0]]=[i,scoreForKeyword(u[0],u[1])]}))}}function scoreForKeyword(i,s){return s?Number(s):function commonKeyword(i){return Se.includes(i.toLowerCase())}(i)?0:1}function compileLanguage(i,{plugins:s}){function langRe(s,u){return new RegExp(source(s),"m"+(i.case_insensitive?"i":"")+(u?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(i,s){s.position=this.position++,this.matchIndexes[this.matchAt]=s,this.regexes.push([s,i]),this.matchAt+=function countMatchGroups(i){return new RegExp(i.toString()+"|").exec("").length-1}(i)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const i=this.regexes.map((i=>i[1]));this.matcherRe=langRe(function join(i,s="|"){let u=0;return i.map((i=>{u+=1;const s=u;let v=source(i),_="";for(;v.length>0;){const i=m.exec(v);if(!i){_+=v;break}_+=v.substring(0,i.index),v=v.substring(i.index+i[0].length),"\\"===i[0][0]&&i[1]?_+="\\"+String(Number(i[1])+s):(_+=i[0],"("===i[0]&&u++)}return _})).map((i=>`(${i})`)).join(s)}(i),!0),this.lastIndex=0}exec(i){this.matcherRe.lastIndex=this.lastIndex;const s=this.matcherRe.exec(i);if(!s)return null;const u=s.findIndex(((i,s)=>s>0&&void 0!==i)),m=this.matchIndexes[u];return s.splice(0,u),Object.assign(s,m)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(i){if(this.multiRegexes[i])return this.multiRegexes[i];const s=new MultiRegex;return this.rules.slice(i).forEach((([i,u])=>s.addRule(i,u))),s.compile(),this.multiRegexes[i]=s,s}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(i,s){this.rules.push([i,s]),"begin"===s.type&&this.count++}exec(i){const s=this.getMatcher(this.regexIndex);s.lastIndex=this.lastIndex;let u=s.exec(i);if(this.resumingScanAtSamePosition())if(u&&u.index===this.lastIndex);else{const s=this.getMatcher(0);s.lastIndex=this.lastIndex+1,u=s.exec(i)}return u&&(this.regexIndex+=u.position+1,this.regexIndex===this.count&&this.considerAll()),u}}if(i.compilerExtensions||(i.compilerExtensions=[]),i.contains&&i.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return i.classNameAliases=inherit(i.classNameAliases||{}),function compileMode(s,u){const m=s;if(s.isCompiled)return m;[compileMatch].forEach((i=>i(s,u))),i.compilerExtensions.forEach((i=>i(s,u))),s.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((i=>i(s,u))),s.isCompiled=!0;let v=null;if("object"==typeof s.keywords&&(v=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=compileKeywords(s.keywords,i.case_insensitive)),s.lexemes&&v)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return v=v||s.lexemes||/\w+/,m.keywordPatternRe=langRe(v,!0),u&&(s.begin||(s.begin=/\B|\b/),m.beginRe=langRe(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(m.endRe=langRe(s.end)),m.terminatorEnd=source(s.end)||"",s.endsWithParent&&u.terminatorEnd&&(m.terminatorEnd+=(s.end?"|":"")+u.terminatorEnd)),s.illegal&&(m.illegalRe=langRe(s.illegal)),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(i){return function expandOrCloneMode(i){i.variants&&!i.cachedVariants&&(i.cachedVariants=i.variants.map((function(s){return inherit(i,{variants:null},s)})));if(i.cachedVariants)return i.cachedVariants;if(dependencyOnParent(i))return inherit(i,{starts:i.starts?inherit(i.starts):null});if(Object.isFrozen(i))return inherit(i);return i}("self"===i?s:i)}))),s.contains.forEach((function(i){compileMode(i,m)})),s.starts&&compileMode(s.starts,u),m.matcher=function buildModeRegex(i){const s=new ResumableMultiRegex;return i.contains.forEach((i=>s.addRule(i.begin,{rule:i,type:"begin"}))),i.terminatorEnd&&s.addRule(i.terminatorEnd,{type:"end"}),i.illegal&&s.addRule(i.illegal,{type:"illegal"}),s}(m),m}(i)}function dependencyOnParent(i){return!!i&&(i.endsWithParent||dependencyOnParent(i.starts))}function BuildVuePlugin(i){const s={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!i.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let s={};return this.autoDetect?(s=i.highlightAuto(this.code),this.detectedLanguage=s.language):(s=i.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),s.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(i){return Boolean(i||""===i)}(this.autodetect)},ignoreIllegals:()=>!0},render(i){return i("pre",{},[i("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:s,VuePlugin:{install(i){i.component("highlightjs",s)}}}}const Pe={"after:highlightElement":({el:i,result:s,text:u})=>{const m=nodeStream(i);if(!m.length)return;const v=document.createElement("div");v.innerHTML=s.value,s.value=function mergeStreams(i,s,u){let m=0,v="";const _=[];function selectStream(){return i.length&&s.length?i[0].offset!==s[0].offset?i[0].offset"}function close(i){v+=""}function render(i){("start"===i.event?open:close)(i.node)}for(;i.length||s.length;){let s=selectStream();if(v+=escapeHTML(u.substring(m,s[0].offset)),m=s[0].offset,s===i){_.reverse().forEach(close);do{render(s.splice(0,1)[0]),s=selectStream()}while(s===i&&s.length&&s[0].offset===m);_.reverse().forEach(open)}else"start"===s[0].event?_.push(s[0].node):_.pop(),render(s.splice(0,1)[0])}return v+escapeHTML(u.substr(m))}(m,nodeStream(v),u)}};function tag(i){return i.nodeName.toLowerCase()}function nodeStream(i){const s=[];return function _nodeStream(i,u){for(let m=i.firstChild;m;m=m.nextSibling)3===m.nodeType?u+=m.nodeValue.length:1===m.nodeType&&(s.push({event:"start",offset:u,node:m}),u=_nodeStream(m,u),tag(m).match(/br|hr|img|input/)||s.push({event:"stop",offset:u,node:m}));return u}(i,0),s}const Ie={},error=i=>{console.error(i)},warn=(i,...s)=>{console.log(`WARN: ${i}`,...s)},deprecated=(i,s)=>{Ie[`${i}/${s}`]||(console.log(`Deprecated as of ${i}. ${s}`),Ie[`${i}/${s}`]=!0)},Te=escapeHTML,Re=inherit,qe=Symbol("nomatch");var ze=function(i){const u=Object.create(null),m=Object.create(null),v=[];let _=!0;const j=/(^(<[^>]+>|\t|)+|\n)/gm,M="Could not find the language '{}', did you forget to load/include a language module?",$={disableAutodetect:!0,name:"Plain text",contains:[]};let W={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(i){return W.noHighlightRe.test(i)}function highlight(i,s,u,m){let v="",_="";"object"==typeof s?(v=i,u=s.ignoreIllegals,_=s.language,m=void 0):(deprecated("10.7.0","highlight(lang, code, ...args) has been deprecated."),deprecated("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),_=i,v=s);const j={code:v,language:_};fire("before:highlight",j);const M=j.result?j.result:_highlight(j.language,j.code,u,m);return M.code=j.code,fire("after:highlight",M),M}function _highlight(i,s,m,j){function keywordData(i,s){const u=X.case_insensitive?s[0].toLowerCase():s[0];return Object.prototype.hasOwnProperty.call(i.keywords,u)&&i.keywords[u]}function processBuffer(){null!=ee.subLanguage?function processSubLanguage(){if(""===le)return;let i=null;if("string"==typeof ee.subLanguage){if(!u[ee.subLanguage])return void ie.addText(le);i=_highlight(ee.subLanguage,le,!0,ae[ee.subLanguage]),ae[ee.subLanguage]=i.top}else i=highlightAuto(le,ee.subLanguage.length?ee.subLanguage:null);ee.relevance>0&&(ce+=i.relevance),ie.addSublanguage(i.emitter,i.language)}():function processKeywords(){if(!ee.keywords)return void ie.addText(le);let i=0;ee.keywordPatternRe.lastIndex=0;let s=ee.keywordPatternRe.exec(le),u="";for(;s;){u+=le.substring(i,s.index);const m=keywordData(ee,s);if(m){const[i,v]=m;if(ie.addText(u),u="",ce+=v,i.startsWith("_"))u+=s[0];else{const u=X.classNameAliases[i]||i;ie.addKeyword(s[0],u)}}else u+=s[0];i=ee.keywordPatternRe.lastIndex,s=ee.keywordPatternRe.exec(le)}u+=le.substr(i),ie.addText(u)}(),le=""}function startNewMode(i){return i.className&&ie.openNode(X.classNameAliases[i.className]||i.className),ee=Object.create(i,{parent:{value:ee}}),ee}function endOfMode(i,s,u){let m=function startsWith(i,s){const u=i&&i.exec(s);return u&&0===u.index}(i.endRe,u);if(m){if(i["on:end"]){const u=new Response(i);i["on:end"](s,u),u.isMatchIgnored&&(m=!1)}if(m){for(;i.endsParent&&i.parent;)i=i.parent;return i}}if(i.endsWithParent)return endOfMode(i.parent,s,u)}function doIgnore(i){return 0===ee.matcher.regexIndex?(le+=i[0],1):(fe=!0,0)}function doBeginMatch(i){const s=i[0],u=i.rule,m=new Response(u),v=[u.__beforeBegin,u["on:begin"]];for(const u of v)if(u&&(u(i,m),m.isMatchIgnored))return doIgnore(s);return u&&u.endSameAsBegin&&(u.endRe=function escape(i){return new RegExp(i.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(s)),u.skip?le+=s:(u.excludeBegin&&(le+=s),processBuffer(),u.returnBegin||u.excludeBegin||(le=s)),startNewMode(u),u.returnBegin?0:s.length}function doEndMatch(i){const u=i[0],m=s.substr(i.index),v=endOfMode(ee,i,m);if(!v)return qe;const _=ee;_.skip?le+=u:(_.returnEnd||_.excludeEnd||(le+=u),processBuffer(),_.excludeEnd&&(le=u));do{ee.className&&ie.closeNode(),ee.skip||ee.subLanguage||(ce+=ee.relevance),ee=ee.parent}while(ee!==v.parent);return v.starts&&(v.endSameAsBegin&&(v.starts.endRe=v.endRe),startNewMode(v.starts)),_.returnEnd?0:u.length}let $={};function processLexeme(u,v){const j=v&&v[0];if(le+=u,null==j)return processBuffer(),0;if("begin"===$.type&&"end"===v.type&&$.index===v.index&&""===j){if(le+=s.slice(v.index,v.index+1),!_){const s=new Error("0 width match regex");throw s.languageName=i,s.badRule=$.rule,s}return 1}if($=v,"begin"===v.type)return doBeginMatch(v);if("illegal"===v.type&&!m){const i=new Error('Illegal lexeme "'+j+'" for mode "'+(ee.className||"")+'"');throw i.mode=ee,i}if("end"===v.type){const i=doEndMatch(v);if(i!==qe)return i}if("illegal"===v.type&&""===j)return 1;if(de>1e5&&de>3*v.index){throw new Error("potential infinite loop, way more iterations than matches")}return le+=j,j.length}const X=getLanguage(i);if(!X)throw error(M.replace("{}",i)),new Error('Unknown language: "'+i+'"');const Y=compileLanguage(X,{plugins:v});let Z="",ee=j||Y;const ae={},ie=new W.__emitter(W);!function processContinuations(){const i=[];for(let s=ee;s!==X;s=s.parent)s.className&&i.unshift(s.className);i.forEach((i=>ie.openNode(i)))}();let le="",ce=0,pe=0,de=0,fe=!1;try{for(ee.matcher.considerAll();;){de++,fe?fe=!1:ee.matcher.considerAll(),ee.matcher.lastIndex=pe;const i=ee.matcher.exec(s);if(!i)break;const u=processLexeme(s.substring(pe,i.index),i);pe=i.index+u}return processLexeme(s.substr(pe)),ie.closeAllNodes(),ie.finalize(),Z=ie.toHTML(),{relevance:Math.floor(ce),value:Z,language:i,illegal:!1,emitter:ie,top:ee}}catch(u){if(u.message&&u.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:u.message,context:s.slice(pe-100,pe+100),mode:u.mode},sofar:Z,relevance:0,value:Te(s),emitter:ie};if(_)return{illegal:!1,relevance:0,value:Te(s),emitter:ie,language:i,top:ee,errorRaised:u};throw u}}function highlightAuto(i,s){s=s||W.languages||Object.keys(u);const m=function justTextHighlightResult(i){const s={relevance:0,emitter:new W.__emitter(W),value:Te(i),illegal:!1,top:$};return s.emitter.addText(i),s}(i),v=s.filter(getLanguage).filter(autoDetection).map((s=>_highlight(s,i,!1)));v.unshift(m);const _=v.sort(((i,s)=>{if(i.relevance!==s.relevance)return s.relevance-i.relevance;if(i.language&&s.language){if(getLanguage(i.language).supersetOf===s.language)return 1;if(getLanguage(s.language).supersetOf===i.language)return-1}return 0})),[j,M]=_,X=j;return X.second_best=M,X}const X={"before:highlightElement":({el:i})=>{W.useBR&&(i.innerHTML=i.innerHTML.replace(/\n/g,"").replace(//g,"\n"))},"after:highlightElement":({result:i})=>{W.useBR&&(i.value=i.value.replace(/\n/g,"
"))}},Y=/^(<[^>]+>|\t)+/gm,Z={"after:highlightElement":({result:i})=>{W.tabReplace&&(i.value=i.value.replace(Y,(i=>i.replace(/\t/g,W.tabReplace))))}};function highlightElement(i){let s=null;const u=function blockLanguage(i){let s=i.className+" ";s+=i.parentNode?i.parentNode.className:"";const u=W.languageDetectRe.exec(s);if(u){const s=getLanguage(u[1]);return s||(warn(M.replace("{}",u[1])),warn("Falling back to no-highlight mode for this block.",i)),s?u[1]:"no-highlight"}return s.split(/\s+/).find((i=>shouldNotHighlight(i)||getLanguage(i)))}(i);if(shouldNotHighlight(u))return;fire("before:highlightElement",{el:i,language:u}),s=i;const v=s.textContent,_=u?highlight(v,{language:u,ignoreIllegals:!0}):highlightAuto(v);fire("after:highlightElement",{el:i,result:_,text:v}),i.innerHTML=_.value,function updateClassName(i,s,u){const v=s?m[s]:u;i.classList.add("hljs"),v&&i.classList.add(v)}(i,u,_.language),i.result={language:_.language,re:_.relevance,relavance:_.relevance},_.second_best&&(i.second_best={language:_.second_best.language,re:_.second_best.relevance,relavance:_.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(highlightElement)};let ee=!1;function highlightAll(){if("loading"===document.readyState)return void(ee=!0);document.querySelectorAll("pre code").forEach(highlightElement)}function getLanguage(i){return i=(i||"").toLowerCase(),u[i]||u[m[i]]}function registerAliases(i,{languageName:s}){"string"==typeof i&&(i=[i]),i.forEach((i=>{m[i.toLowerCase()]=s}))}function autoDetection(i){const s=getLanguage(i);return s&&!s.disableAutodetect}function fire(i,s){const u=i;v.forEach((function(i){i[u]&&i[u](s)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function boot(){ee&&highlightAll()}),!1),Object.assign(i,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(i){return deprecated("10.2.0","fixMarkup will be removed entirely in v11.0"),deprecated("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function fixMarkup(i){return W.tabReplace||W.useBR?i.replace(j,(i=>"\n"===i?W.useBR?"
":i:W.tabReplace?i.replace(/\t/g,W.tabReplace):i)):i}(i)},highlightElement,highlightBlock:function deprecateHighlightBlock(i){return deprecated("10.7.0","highlightBlock will be removed entirely in v12.0"),deprecated("10.7.0","Please use highlightElement now."),highlightElement(i)},configure:function configure(i){i.useBR&&(deprecated("10.3.0","'useBR' will be removed entirely in v11.0"),deprecated("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),W=Re(W,i)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),ee=!0},registerLanguage:function registerLanguage(s,m){let v=null;try{v=m(i)}catch(i){if(error("Language definition for '{}' could not be registered.".replace("{}",s)),!_)throw i;error(i),v=$}v.name||(v.name=s),u[s]=v,v.rawDefinition=m.bind(null,i),v.aliases&®isterAliases(v.aliases,{languageName:s})},unregisterLanguage:function unregisterLanguage(i){delete u[i];for(const s of Object.keys(m))m[s]===i&&delete m[s]},listLanguages:function listLanguages(){return Object.keys(u)},getLanguage,registerAliases,requireLanguage:function requireLanguage(i){deprecated("10.4.0","requireLanguage will be removed entirely in v11."),deprecated("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const s=getLanguage(i);if(s)return s;throw new Error("The '{}' language is required, but not loaded.".replace("{}",i))},autoDetection,inherit:Re,addPlugin:function addPlugin(i){!function upgradePluginAPI(i){i["before:highlightBlock"]&&!i["before:highlightElement"]&&(i["before:highlightElement"]=s=>{i["before:highlightBlock"](Object.assign({block:s.el},s))}),i["after:highlightBlock"]&&!i["after:highlightElement"]&&(i["after:highlightElement"]=s=>{i["after:highlightBlock"](Object.assign({block:s.el},s))})}(i),v.push(i)},vuePlugin:BuildVuePlugin(i).VuePlugin}),i.debugMode=function(){_=!1},i.safeMode=function(){_=!0},i.versionString="10.7.3";for(const i in we)"object"==typeof we[i]&&s(we[i]);return Object.assign(i,we),i.addPlugin(X),i.addPlugin(Pe),i.addPlugin(Z),i}({});i.exports=ze},61519:i=>{function concat(...i){return i.map((i=>function source(i){return i?"string"==typeof i?i:i.source:null}(i))).join("")}i.exports=function bash(i){const s={},u={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[s]}]};Object.assign(s,{className:"variable",variants:[{begin:concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},u]});const m={className:"subst",begin:/\$\(/,end:/\)/,contains:[i.BACKSLASH_ESCAPE]},v={begin:/<<-?\s*(?=\w+)/,starts:{contains:[i.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},_={className:"string",begin:/"/,end:/"/,contains:[i.BACKSLASH_ESCAPE,s,m]};m.contains.push(_);const j={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},i.NUMBER_MODE,s]},M=i.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),$={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[i.inherit(i.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[M,i.SHEBANG(),$,j,i.HASH_COMMENT_MODE,v,_,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}},30786:i=>{function concat(...i){return i.map((i=>function source(i){return i?"string"==typeof i?i:i.source:null}(i))).join("")}i.exports=function http(i){const s="HTTP/(2|1\\.[01])",u={className:"attribute",begin:concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},m=[u,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+s+" \\d{3})",end:/$/,contains:[{className:"meta",begin:s},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:m}},{begin:"(?=^[A-Z]+ (.*?) "+s+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:s},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:m}},i.inherit(u,{relevance:0})]}}},96344:i=>{const s="[A-Za-z$_][0-9A-Za-z$_]*",u=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],m=["true","false","null","undefined","NaN","Infinity"],v=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function lookahead(i){return concat("(?=",i,")")}function concat(...i){return i.map((i=>function source(i){return i?"string"==typeof i?i:i.source:null}(i))).join("")}i.exports=function javascript(i){const _=s,j="<>",M="",$={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(i,s)=>{const u=i[0].length+i.index,m=i.input[u];"<"!==m?">"===m&&(((i,{after:s})=>{const u="",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:i.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:W,contains:ye}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:j,end:M},{begin:$.begin,"on:begin":$.isTrulyOpeningTag,end:$.end}],subLanguage:"xml",contains:[{begin:$.begin,end:$.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:W,contains:["self",i.inherit(i.TITLE_MODE,{begin:_}),be],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:i.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[be,i.inherit(i.TITLE_MODE,{begin:_})]},{variants:[{begin:"\\."+_},{begin:"\\$"+_}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},i.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[i.inherit(i.TITLE_MODE,{begin:_}),"self",be]},{begin:"(get|set)\\s+(?="+_+"\\()",end:/\{/,keywords:"get set",contains:[i.inherit(i.TITLE_MODE,{begin:_}),{begin:/\(\)/},be]},{begin:/\$[(.]/}]}}},82026:i=>{i.exports=function json(i){const s={literal:"true false null"},u=[i.C_LINE_COMMENT_MODE,i.C_BLOCK_COMMENT_MODE],m=[i.QUOTE_STRING_MODE,i.C_NUMBER_MODE],v={end:",",endsWithParent:!0,excludeEnd:!0,contains:m,keywords:s},_={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[i.BACKSLASH_ESCAPE],illegal:"\\n"},i.inherit(v,{begin:/:/})].concat(u),illegal:"\\S"},j={begin:"\\[",end:"\\]",contains:[i.inherit(v)],illegal:"\\S"};return m.push(_,j),u.forEach((function(i){m.push(i)})),{name:"JSON",contains:m,keywords:s,illegal:"\\S"}}},66336:i=>{i.exports=function powershell(i){const s={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},u={begin:"`[\\s\\S]",relevance:0},m={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},v={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[u,m,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},_={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},j=i.inherit(i.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),M={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},$={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[i.TITLE_MODE]},W={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[m]}]},X={begin:/using\s/,end:/$/,returnBegin:!0,contains:[v,_,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},Y={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},Z={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(s.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},i.inherit(i.TITLE_MODE,{endsParent:!0})]},ee=[Z,j,u,i.NUMBER_MODE,v,_,M,m,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],ae={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",ee,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return Z.contains.unshift(ae),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:s,contains:ee.concat($,W,X,Y,ae)}}},42157:i=>{function source(i){return i?"string"==typeof i?i:i.source:null}function lookahead(i){return concat("(?=",i,")")}function concat(...i){return i.map((i=>source(i))).join("")}function either(...i){return"("+i.map((i=>source(i))).join("|")+")"}i.exports=function xml(i){const s=concat(/[A-Z_]/,function optional(i){return concat("(",i,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),u={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},m={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},v=i.inherit(m,{begin:/\(/,end:/\)/}),_=i.inherit(i.APOS_STRING_MODE,{className:"meta-string"}),j=i.inherit(i.QUOTE_STRING_MODE,{className:"meta-string"}),M={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[m,j,_,v,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[m,v,j,_]}]}]},i.COMMENT(//,{relevance:10}),{begin://,relevance:10},u,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[M],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[M],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:s,relevance:0,starts:M}]},{className:"tag",begin:concat(/<\//,lookahead(concat(s,/>/))),contains:[{className:"name",begin:s,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},54587:i=>{i.exports=function yaml(i){var s="true false yes no null",u="[\\w#;/?:@&=+$,.~*'()[\\]]+",m={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[i.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},v=i.inherit(m,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),_={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},j={end:",",endsWithParent:!0,excludeEnd:!0,keywords:s,relevance:0},M={begin:/\{/,end:/\}/,contains:[j],illegal:"\\n",relevance:0},$={begin:"\\[",end:"\\]",contains:[j],illegal:"\\n",relevance:0},W=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+u},{className:"type",begin:"!<"+u+">"},{className:"type",begin:"!"+u},{className:"type",begin:"!!"+u},{className:"meta",begin:"&"+i.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+i.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},i.HASH_COMMENT_MODE,{beginKeywords:s,keywords:{literal:s}},_,{className:"number",begin:i.C_NUMBER_RE+"\\b",relevance:0},M,$,m],X=[...W];return X.pop(),X.push(v),j.contains=X,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:W}}},8679:(i,s,u)=>{"use strict";var m=u(59864),v={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},_={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},j={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},M={};function getStatics(i){return m.isMemo(i)?j:M[i.$$typeof]||v}M[m.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},M[m.Memo]=j;var $=Object.defineProperty,W=Object.getOwnPropertyNames,X=Object.getOwnPropertySymbols,Y=Object.getOwnPropertyDescriptor,Z=Object.getPrototypeOf,ee=Object.prototype;i.exports=function hoistNonReactStatics(i,s,u){if("string"!=typeof s){if(ee){var m=Z(s);m&&m!==ee&&hoistNonReactStatics(i,m,u)}var v=W(s);X&&(v=v.concat(X(s)));for(var j=getStatics(i),M=getStatics(s),ae=0;ae{s.read=function(i,s,u,m,v){var _,j,M=8*v-m-1,$=(1<>1,X=-7,Y=u?v-1:0,Z=u?-1:1,ee=i[s+Y];for(Y+=Z,_=ee&(1<<-X)-1,ee>>=-X,X+=M;X>0;_=256*_+i[s+Y],Y+=Z,X-=8);for(j=_&(1<<-X)-1,_>>=-X,X+=m;X>0;j=256*j+i[s+Y],Y+=Z,X-=8);if(0===_)_=1-W;else{if(_===$)return j?NaN:1/0*(ee?-1:1);j+=Math.pow(2,m),_-=W}return(ee?-1:1)*j*Math.pow(2,_-m)},s.write=function(i,s,u,m,v,_){var j,M,$,W=8*_-v-1,X=(1<>1,Z=23===v?Math.pow(2,-24)-Math.pow(2,-77):0,ee=m?0:_-1,ae=m?1:-1,ie=s<0||0===s&&1/s<0?1:0;for(s=Math.abs(s),isNaN(s)||s===1/0?(M=isNaN(s)?1:0,j=X):(j=Math.floor(Math.log(s)/Math.LN2),s*($=Math.pow(2,-j))<1&&(j--,$*=2),(s+=j+Y>=1?Z/$:Z*Math.pow(2,1-Y))*$>=2&&(j++,$/=2),j+Y>=X?(M=0,j=X):j+Y>=1?(M=(s*$-1)*Math.pow(2,v),j+=Y):(M=s*Math.pow(2,Y-1)*Math.pow(2,v),j=0));v>=8;i[u+ee]=255&M,ee+=ae,M/=256,v-=8);for(j=j<0;i[u+ee]=255&j,ee+=ae,j/=256,W-=8);i[u+ee-ae]|=128*ie}},43393:function(i){i.exports=function(){"use strict";var i=Array.prototype.slice;function createClass(i,s){s&&(i.prototype=Object.create(s.prototype)),i.prototype.constructor=i}function Iterable(i){return isIterable(i)?i:Seq(i)}function KeyedIterable(i){return isKeyed(i)?i:KeyedSeq(i)}function IndexedIterable(i){return isIndexed(i)?i:IndexedSeq(i)}function SetIterable(i){return isIterable(i)&&!isAssociative(i)?i:SetSeq(i)}function isIterable(i){return!(!i||!i[s])}function isKeyed(i){return!(!i||!i[u])}function isIndexed(i){return!(!i||!i[m])}function isAssociative(i){return isKeyed(i)||isIndexed(i)}function isOrdered(i){return!(!i||!i[v])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var s="@@__IMMUTABLE_ITERABLE__@@",u="@@__IMMUTABLE_KEYED__@@",m="@@__IMMUTABLE_INDEXED__@@",v="@@__IMMUTABLE_ORDERED__@@",_="delete",j=5,M=1<>>0;if(""+u!==s||4294967295===u)return NaN;s=u}return s<0?ensureSize(i)+s:s}function returnTrue(){return!0}function wholeSlice(i,s,u){return(0===i||void 0!==u&&i<=-u)&&(void 0===s||void 0!==u&&s>=u)}function resolveBegin(i,s){return resolveIndex(i,s,0)}function resolveEnd(i,s){return resolveIndex(i,s,s)}function resolveIndex(i,s,u){return void 0===i?u:i<0?Math.max(0,s+i):void 0===s?i:Math.min(s,i)}var Z=0,ee=1,ae=2,ie="function"==typeof Symbol&&Symbol.iterator,le="@@iterator",ce=ie||le;function Iterator(i){this.next=i}function iteratorValue(i,s,u,m){var v=0===i?s:1===i?u:[s,u];return m?m.value=v:m={value:v,done:!1},m}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(i){return!!getIteratorFn(i)}function isIterator(i){return i&&"function"==typeof i.next}function getIterator(i){var s=getIteratorFn(i);return s&&s.call(i)}function getIteratorFn(i){var s=i&&(ie&&i[ie]||i[le]);if("function"==typeof s)return s}function isArrayLike(i){return i&&"number"==typeof i.length}function Seq(i){return null==i?emptySequence():isIterable(i)?i.toSeq():seqFromValue(i)}function KeyedSeq(i){return null==i?emptySequence().toKeyedSeq():isIterable(i)?isKeyed(i)?i.toSeq():i.fromEntrySeq():keyedSeqFromValue(i)}function IndexedSeq(i){return null==i?emptySequence():isIterable(i)?isKeyed(i)?i.entrySeq():i.toIndexedSeq():indexedSeqFromValue(i)}function SetSeq(i){return(null==i?emptySequence():isIterable(i)?isKeyed(i)?i.entrySeq():i:indexedSeqFromValue(i)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=Z,Iterator.VALUES=ee,Iterator.ENTRIES=ae,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[ce]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(i,s){return seqIterate(this,i,s,!0)},Seq.prototype.__iterator=function(i,s){return seqIterator(this,i,s,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(i,s){return seqIterate(this,i,s,!1)},IndexedSeq.prototype.__iterator=function(i,s){return seqIterator(this,i,s,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var pe,de,fe,ye="@@__IMMUTABLE_SEQ__@@";function ArraySeq(i){this._array=i,this.size=i.length}function ObjectSeq(i){var s=Object.keys(i);this._object=i,this._keys=s,this.size=s.length}function IterableSeq(i){this._iterable=i,this.size=i.length||i.size}function IteratorSeq(i){this._iterator=i,this._iteratorCache=[]}function isSeq(i){return!(!i||!i[ye])}function emptySequence(){return pe||(pe=new ArraySeq([]))}function keyedSeqFromValue(i){var s=Array.isArray(i)?new ArraySeq(i).fromEntrySeq():isIterator(i)?new IteratorSeq(i).fromEntrySeq():hasIterator(i)?new IterableSeq(i).fromEntrySeq():"object"==typeof i?new ObjectSeq(i):void 0;if(!s)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+i);return s}function indexedSeqFromValue(i){var s=maybeIndexedSeqFromValue(i);if(!s)throw new TypeError("Expected Array or iterable object of values: "+i);return s}function seqFromValue(i){var s=maybeIndexedSeqFromValue(i)||"object"==typeof i&&new ObjectSeq(i);if(!s)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+i);return s}function maybeIndexedSeqFromValue(i){return isArrayLike(i)?new ArraySeq(i):isIterator(i)?new IteratorSeq(i):hasIterator(i)?new IterableSeq(i):void 0}function seqIterate(i,s,u,m){var v=i._cache;if(v){for(var _=v.length-1,j=0;j<=_;j++){var M=v[u?_-j:j];if(!1===s(M[1],m?M[0]:j,i))return j+1}return j}return i.__iterateUncached(s,u)}function seqIterator(i,s,u,m){var v=i._cache;if(v){var _=v.length-1,j=0;return new Iterator((function(){var i=v[u?_-j:j];return j++>_?iteratorDone():iteratorValue(s,m?i[0]:j-1,i[1])}))}return i.__iteratorUncached(s,u)}function fromJS(i,s){return s?fromJSWith(s,i,"",{"":i}):fromJSDefault(i)}function fromJSWith(i,s,u,m){return Array.isArray(s)?i.call(m,u,IndexedSeq(s).map((function(u,m){return fromJSWith(i,u,m,s)}))):isPlainObj(s)?i.call(m,u,KeyedSeq(s).map((function(u,m){return fromJSWith(i,u,m,s)}))):s}function fromJSDefault(i){return Array.isArray(i)?IndexedSeq(i).map(fromJSDefault).toList():isPlainObj(i)?KeyedSeq(i).map(fromJSDefault).toMap():i}function isPlainObj(i){return i&&(i.constructor===Object||void 0===i.constructor)}function is(i,s){if(i===s||i!=i&&s!=s)return!0;if(!i||!s)return!1;if("function"==typeof i.valueOf&&"function"==typeof s.valueOf){if((i=i.valueOf())===(s=s.valueOf())||i!=i&&s!=s)return!0;if(!i||!s)return!1}return!("function"!=typeof i.equals||"function"!=typeof s.equals||!i.equals(s))}function deepEqual(i,s){if(i===s)return!0;if(!isIterable(s)||void 0!==i.size&&void 0!==s.size&&i.size!==s.size||void 0!==i.__hash&&void 0!==s.__hash&&i.__hash!==s.__hash||isKeyed(i)!==isKeyed(s)||isIndexed(i)!==isIndexed(s)||isOrdered(i)!==isOrdered(s))return!1;if(0===i.size&&0===s.size)return!0;var u=!isAssociative(i);if(isOrdered(i)){var m=i.entries();return s.every((function(i,s){var v=m.next().value;return v&&is(v[1],i)&&(u||is(v[0],s))}))&&m.next().done}var v=!1;if(void 0===i.size)if(void 0===s.size)"function"==typeof i.cacheResult&&i.cacheResult();else{v=!0;var _=i;i=s,s=_}var j=!0,M=s.__iterate((function(s,m){if(u?!i.has(s):v?!is(s,i.get(m,W)):!is(i.get(m,W),s))return j=!1,!1}));return j&&i.size===M}function Repeat(i,s){if(!(this instanceof Repeat))return new Repeat(i,s);if(this._value=i,this.size=void 0===s?1/0:Math.max(0,s),0===this.size){if(de)return de;de=this}}function invariant(i,s){if(!i)throw new Error(s)}function Range(i,s,u){if(!(this instanceof Range))return new Range(i,s,u);if(invariant(0!==u,"Cannot step a Range by 0"),i=i||0,void 0===s&&(s=1/0),u=void 0===u?1:Math.abs(u),sm?iteratorDone():iteratorValue(i,v,u[s?m-v++:v++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(i,s){return void 0===s||this.has(i)?this._object[i]:s},ObjectSeq.prototype.has=function(i){return this._object.hasOwnProperty(i)},ObjectSeq.prototype.__iterate=function(i,s){for(var u=this._object,m=this._keys,v=m.length-1,_=0;_<=v;_++){var j=m[s?v-_:_];if(!1===i(u[j],j,this))return _+1}return _},ObjectSeq.prototype.__iterator=function(i,s){var u=this._object,m=this._keys,v=m.length-1,_=0;return new Iterator((function(){var j=m[s?v-_:_];return _++>v?iteratorDone():iteratorValue(i,j,u[j])}))},ObjectSeq.prototype[v]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(i,s){if(s)return this.cacheResult().__iterate(i,s);var u=getIterator(this._iterable),m=0;if(isIterator(u))for(var v;!(v=u.next()).done&&!1!==i(v.value,m++,this););return m},IterableSeq.prototype.__iteratorUncached=function(i,s){if(s)return this.cacheResult().__iterator(i,s);var u=getIterator(this._iterable);if(!isIterator(u))return new Iterator(iteratorDone);var m=0;return new Iterator((function(){var s=u.next();return s.done?s:iteratorValue(i,m++,s.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(i,s){if(s)return this.cacheResult().__iterate(i,s);for(var u,m=this._iterator,v=this._iteratorCache,_=0;_=m.length){var s=u.next();if(s.done)return s;m[v]=s.value}return iteratorValue(i,v,m[v++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(i,s){return this.has(i)?this._value:s},Repeat.prototype.includes=function(i){return is(this._value,i)},Repeat.prototype.slice=function(i,s){var u=this.size;return wholeSlice(i,s,u)?this:new Repeat(this._value,resolveEnd(s,u)-resolveBegin(i,u))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(i){return is(this._value,i)?0:-1},Repeat.prototype.lastIndexOf=function(i){return is(this._value,i)?this.size:-1},Repeat.prototype.__iterate=function(i,s){for(var u=0;u=0&&s=0&&uu?iteratorDone():iteratorValue(i,_++,j)}))},Range.prototype.equals=function(i){return i instanceof Range?this._start===i._start&&this._end===i._end&&this._step===i._step:deepEqual(this,i)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var be="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(i,s){var u=65535&(i|=0),m=65535&(s|=0);return u*m+((i>>>16)*m+u*(s>>>16)<<16>>>0)|0};function smi(i){return i>>>1&1073741824|3221225471&i}function hash(i){if(!1===i||null==i)return 0;if("function"==typeof i.valueOf&&(!1===(i=i.valueOf())||null==i))return 0;if(!0===i)return 1;var s=typeof i;if("number"===s){if(i!=i||i===1/0)return 0;var u=0|i;for(u!==i&&(u^=4294967295*i);i>4294967295;)u^=i/=4294967295;return smi(u)}if("string"===s)return i.length>Te?cachedHashString(i):hashString(i);if("function"==typeof i.hashCode)return i.hashCode();if("object"===s)return hashJSObj(i);if("function"==typeof i.toString)return hashString(i.toString());throw new Error("Value type "+s+" cannot be hashed.")}function cachedHashString(i){var s=ze[i];return void 0===s&&(s=hashString(i),qe===Re&&(qe=0,ze={}),qe++,ze[i]=s),s}function hashString(i){for(var s=0,u=0;u0)switch(i.nodeType){case 1:return i.uniqueID;case 9:return i.documentElement&&i.documentElement.uniqueID}}var Se,xe="function"==typeof WeakMap;xe&&(Se=new WeakMap);var Pe=0,Ie="__immutablehash__";"function"==typeof Symbol&&(Ie=Symbol(Ie));var Te=16,Re=255,qe=0,ze={};function assertNotInfinite(i){invariant(i!==1/0,"Cannot perform this action with an infinite size.")}function Map(i){return null==i?emptyMap():isMap(i)&&!isOrdered(i)?i:emptyMap().withMutations((function(s){var u=KeyedIterable(i);assertNotInfinite(u.size),u.forEach((function(i,u){return s.set(u,i)}))}))}function isMap(i){return!(!i||!i[We])}createClass(Map,KeyedCollection),Map.of=function(){var s=i.call(arguments,0);return emptyMap().withMutations((function(i){for(var u=0;u=s.length)throw new Error("Missing value for key: "+s[u]);i.set(s[u],s[u+1])}}))},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(i,s){return this._root?this._root.get(0,void 0,i,s):s},Map.prototype.set=function(i,s){return updateMap(this,i,s)},Map.prototype.setIn=function(i,s){return this.updateIn(i,W,(function(){return s}))},Map.prototype.remove=function(i){return updateMap(this,i,W)},Map.prototype.deleteIn=function(i){return this.updateIn(i,(function(){return W}))},Map.prototype.update=function(i,s,u){return 1===arguments.length?i(this):this.updateIn([i],s,u)},Map.prototype.updateIn=function(i,s,u){u||(u=s,s=void 0);var m=updateInDeepMap(this,forceIterator(i),s,u);return m===W?void 0:m},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(s){return mergeIntoMapWith(this,s,i.call(arguments,1))},Map.prototype.mergeIn=function(s){var u=i.call(arguments,1);return this.updateIn(s,emptyMap(),(function(i){return"function"==typeof i.merge?i.merge.apply(i,u):u[u.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(s){var u=i.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(s),u)},Map.prototype.mergeDeepIn=function(s){var u=i.call(arguments,1);return this.updateIn(s,emptyMap(),(function(i){return"function"==typeof i.mergeDeep?i.mergeDeep.apply(i,u):u[u.length-1]}))},Map.prototype.sort=function(i){return OrderedMap(sortFactory(this,i))},Map.prototype.sortBy=function(i,s){return OrderedMap(sortFactory(this,s,i))},Map.prototype.withMutations=function(i){var s=this.asMutable();return i(s),s.wasAltered()?s.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(i,s){return new MapIterator(this,i,s)},Map.prototype.__iterate=function(i,s){var u=this,m=0;return this._root&&this._root.iterate((function(s){return m++,i(s[1],s[0],u)}),s),m},Map.prototype.__ensureOwner=function(i){return i===this.__ownerID?this:i?makeMap(this.size,this._root,i,this.__hash):(this.__ownerID=i,this.__altered=!1,this)},Map.isMap=isMap;var Ve,We="@@__IMMUTABLE_MAP__@@",He=Map.prototype;function ArrayMapNode(i,s){this.ownerID=i,this.entries=s}function BitmapIndexedNode(i,s,u){this.ownerID=i,this.bitmap=s,this.nodes=u}function HashArrayMapNode(i,s,u){this.ownerID=i,this.count=s,this.nodes=u}function HashCollisionNode(i,s,u){this.ownerID=i,this.keyHash=s,this.entries=u}function ValueNode(i,s,u){this.ownerID=i,this.keyHash=s,this.entry=u}function MapIterator(i,s,u){this._type=s,this._reverse=u,this._stack=i._root&&mapIteratorFrame(i._root)}function mapIteratorValue(i,s){return iteratorValue(i,s[0],s[1])}function mapIteratorFrame(i,s){return{node:i,index:0,__prev:s}}function makeMap(i,s,u,m){var v=Object.create(He);return v.size=i,v._root=s,v.__ownerID=u,v.__hash=m,v.__altered=!1,v}function emptyMap(){return Ve||(Ve=makeMap(0))}function updateMap(i,s,u){var m,v;if(i._root){var _=MakeRef(X),j=MakeRef(Y);if(m=updateNode(i._root,i.__ownerID,0,void 0,s,u,_,j),!j.value)return i;v=i.size+(_.value?u===W?-1:1:0)}else{if(u===W)return i;v=1,m=new ArrayMapNode(i.__ownerID,[[s,u]])}return i.__ownerID?(i.size=v,i._root=m,i.__hash=void 0,i.__altered=!0,i):m?makeMap(v,m):emptyMap()}function updateNode(i,s,u,m,v,_,j,M){return i?i.update(s,u,m,v,_,j,M):_===W?i:(SetRef(M),SetRef(j),new ValueNode(s,m,[v,_]))}function isLeafNode(i){return i.constructor===ValueNode||i.constructor===HashCollisionNode}function mergeIntoNode(i,s,u,m,v){if(i.keyHash===m)return new HashCollisionNode(s,m,[i.entry,v]);var _,M=(0===u?i.keyHash:i.keyHash>>>u)&$,W=(0===u?m:m>>>u)&$;return new BitmapIndexedNode(s,1<>>=1)j[$]=1&u?s[_++]:void 0;return j[m]=v,new HashArrayMapNode(i,_+1,j)}function mergeIntoMapWith(i,s,u){for(var m=[],v=0;v>1&1431655765))+(i>>2&858993459))+(i>>4)&252645135,i+=i>>8,127&(i+=i>>16)}function setIn(i,s,u,m){var v=m?i:arrCopy(i);return v[s]=u,v}function spliceIn(i,s,u,m){var v=i.length+1;if(m&&s+1===v)return i[s]=u,i;for(var _=new Array(v),j=0,M=0;M=Xe)return createNodes(i,$,m,v);var ee=i&&i===this.ownerID,ae=ee?$:arrCopy($);return Z?M?X===Y-1?ae.pop():ae[X]=ae.pop():ae[X]=[m,v]:ae.push([m,v]),ee?(this.entries=ae,this):new ArrayMapNode(i,ae)}},BitmapIndexedNode.prototype.get=function(i,s,u,m){void 0===s&&(s=hash(u));var v=1<<((0===i?s:s>>>i)&$),_=this.bitmap;return 0==(_&v)?m:this.nodes[popCount(_&v-1)].get(i+j,s,u,m)},BitmapIndexedNode.prototype.update=function(i,s,u,m,v,_,M){void 0===u&&(u=hash(m));var X=(0===s?u:u>>>s)&$,Y=1<=Ye)return expandNodes(i,ie,Z,X,ce);if(ee&&!ce&&2===ie.length&&isLeafNode(ie[1^ae]))return ie[1^ae];if(ee&&ce&&1===ie.length&&isLeafNode(ce))return ce;var pe=i&&i===this.ownerID,de=ee?ce?Z:Z^Y:Z|Y,fe=ee?ce?setIn(ie,ae,ce,pe):spliceOut(ie,ae,pe):spliceIn(ie,ae,ce,pe);return pe?(this.bitmap=de,this.nodes=fe,this):new BitmapIndexedNode(i,de,fe)},HashArrayMapNode.prototype.get=function(i,s,u,m){void 0===s&&(s=hash(u));var v=(0===i?s:s>>>i)&$,_=this.nodes[v];return _?_.get(i+j,s,u,m):m},HashArrayMapNode.prototype.update=function(i,s,u,m,v,_,M){void 0===u&&(u=hash(m));var X=(0===s?u:u>>>s)&$,Y=v===W,Z=this.nodes,ee=Z[X];if(Y&&!ee)return this;var ae=updateNode(ee,i,s+j,u,m,v,_,M);if(ae===ee)return this;var ie=this.count;if(ee){if(!ae&&--ie0&&m=0&&i>>s&$;if(m>=this.array.length)return new VNode([],i);var v,_=0===m;if(s>0){var M=this.array[m];if((v=M&&M.removeBefore(i,s-j,u))===M&&_)return this}if(_&&!v)return this;var W=editableVNode(this,i);if(!_)for(var X=0;X>>s&$;if(v>=this.array.length)return this;if(s>0){var _=this.array[v];if((m=_&&_.removeAfter(i,s-j,u))===_&&v===this.array.length-1)return this}var M=editableVNode(this,i);return M.array.splice(v+1),m&&(M.array[v]=m),M};var rt,nt,ot={};function iterateList(i,s){var u=i._origin,m=i._capacity,v=getTailOffset(m),_=i._tail;return iterateNodeOrLeaf(i._root,i._level,0);function iterateNodeOrLeaf(i,s,u){return 0===s?iterateLeaf(i,u):iterateNode(i,s,u)}function iterateLeaf(i,j){var $=j===v?_&&_.array:i&&i.array,W=j>u?0:u-j,X=m-j;return X>M&&(X=M),function(){if(W===X)return ot;var i=s?--X:W++;return $&&$[i]}}function iterateNode(i,v,_){var $,W=i&&i.array,X=_>u?0:u-_>>v,Y=1+(m-_>>v);return Y>M&&(Y=M),function(){for(;;){if($){var i=$();if(i!==ot)return i;$=null}if(X===Y)return ot;var u=s?--Y:X++;$=iterateNodeOrLeaf(W&&W[u],v-j,_+(u<=i.size||s<0)return i.withMutations((function(i){s<0?setListBounds(i,s).set(0,u):setListBounds(i,0,s+1).set(s,u)}));s+=i._origin;var m=i._tail,v=i._root,_=MakeRef(Y);return s>=getTailOffset(i._capacity)?m=updateVNode(m,i.__ownerID,0,s,u,_):v=updateVNode(v,i.__ownerID,i._level,s,u,_),_.value?i.__ownerID?(i._root=v,i._tail=m,i.__hash=void 0,i.__altered=!0,i):makeList(i._origin,i._capacity,i._level,v,m):i}function updateVNode(i,s,u,m,v,_){var M,W=m>>>u&$,X=i&&W0){var Y=i&&i.array[W],Z=updateVNode(Y,s,u-j,m,v,_);return Z===Y?i:((M=editableVNode(i,s)).array[W]=Z,M)}return X&&i.array[W]===v?i:(SetRef(_),M=editableVNode(i,s),void 0===v&&W===M.array.length-1?M.array.pop():M.array[W]=v,M)}function editableVNode(i,s){return s&&i&&s===i.ownerID?i:new VNode(i?i.array.slice():[],s)}function listNodeFor(i,s){if(s>=getTailOffset(i._capacity))return i._tail;if(s<1<0;)u=u.array[s>>>m&$],m-=j;return u}}function setListBounds(i,s,u){void 0!==s&&(s|=0),void 0!==u&&(u|=0);var m=i.__ownerID||new OwnerID,v=i._origin,_=i._capacity,M=v+s,W=void 0===u?_:u<0?_+u:v+u;if(M===v&&W===_)return i;if(M>=W)return i.clear();for(var X=i._level,Y=i._root,Z=0;M+Z<0;)Y=new VNode(Y&&Y.array.length?[void 0,Y]:[],m),Z+=1<<(X+=j);Z&&(M+=Z,v+=Z,W+=Z,_+=Z);for(var ee=getTailOffset(_),ae=getTailOffset(W);ae>=1<ee?new VNode([],m):ie;if(ie&&ae>ee&&M<_&&ie.array.length){for(var ce=Y=editableVNode(Y,m),pe=X;pe>j;pe-=j){var de=ee>>>pe&$;ce=ce.array[de]=editableVNode(ce.array[de],m)}ce.array[ee>>>j&$]=ie}if(W<_&&(le=le&&le.removeAfter(m,0,W)),M>=ae)M-=ae,W-=ae,X=j,Y=null,le=le&&le.removeBefore(m,0,M);else if(M>v||ae>>X&$;if(fe!==ae>>>X&$)break;fe&&(Z+=(1<v&&(Y=Y.removeBefore(m,X,M-Z)),Y&&aev&&(v=M.size),isIterable(j)||(M=M.map((function(i){return fromJS(i)}))),m.push(M)}return v>i.size&&(i=i.setSize(v)),mergeIntoCollectionWith(i,s,m)}function getTailOffset(i){return i>>j<=M&&j.size>=2*_.size?(m=(v=j.filter((function(i,s){return void 0!==i&&$!==s}))).toKeyedSeq().map((function(i){return i[0]})).flip().toMap(),i.__ownerID&&(m.__ownerID=v.__ownerID=i.__ownerID)):(m=_.remove(s),v=$===j.size-1?j.pop():j.set($,void 0))}else if(X){if(u===j.get($)[1])return i;m=_,v=j.set($,[s,u])}else m=_.set(s,j.size),v=j.set(j.size,[s,u]);return i.__ownerID?(i.size=m.size,i._map=m,i._list=v,i.__hash=void 0,i):makeOrderedMap(m,v)}function ToKeyedSequence(i,s){this._iter=i,this._useKeys=s,this.size=i.size}function ToIndexedSequence(i){this._iter=i,this.size=i.size}function ToSetSequence(i){this._iter=i,this.size=i.size}function FromEntriesSequence(i){this._iter=i,this.size=i.size}function flipFactory(i){var s=makeSequence(i);return s._iter=i,s.size=i.size,s.flip=function(){return i},s.reverse=function(){var s=i.reverse.apply(this);return s.flip=function(){return i.reverse()},s},s.has=function(s){return i.includes(s)},s.includes=function(s){return i.has(s)},s.cacheResult=cacheResultThrough,s.__iterateUncached=function(s,u){var m=this;return i.__iterate((function(i,u){return!1!==s(u,i,m)}),u)},s.__iteratorUncached=function(s,u){if(s===ae){var m=i.__iterator(s,u);return new Iterator((function(){var i=m.next();if(!i.done){var s=i.value[0];i.value[0]=i.value[1],i.value[1]=s}return i}))}return i.__iterator(s===ee?Z:ee,u)},s}function mapFactory(i,s,u){var m=makeSequence(i);return m.size=i.size,m.has=function(s){return i.has(s)},m.get=function(m,v){var _=i.get(m,W);return _===W?v:s.call(u,_,m,i)},m.__iterateUncached=function(m,v){var _=this;return i.__iterate((function(i,v,j){return!1!==m(s.call(u,i,v,j),v,_)}),v)},m.__iteratorUncached=function(m,v){var _=i.__iterator(ae,v);return new Iterator((function(){var v=_.next();if(v.done)return v;var j=v.value,M=j[0];return iteratorValue(m,M,s.call(u,j[1],M,i),v)}))},m}function reverseFactory(i,s){var u=makeSequence(i);return u._iter=i,u.size=i.size,u.reverse=function(){return i},i.flip&&(u.flip=function(){var s=flipFactory(i);return s.reverse=function(){return i.flip()},s}),u.get=function(u,m){return i.get(s?u:-1-u,m)},u.has=function(u){return i.has(s?u:-1-u)},u.includes=function(s){return i.includes(s)},u.cacheResult=cacheResultThrough,u.__iterate=function(s,u){var m=this;return i.__iterate((function(i,u){return s(i,u,m)}),!u)},u.__iterator=function(s,u){return i.__iterator(s,!u)},u}function filterFactory(i,s,u,m){var v=makeSequence(i);return m&&(v.has=function(m){var v=i.get(m,W);return v!==W&&!!s.call(u,v,m,i)},v.get=function(m,v){var _=i.get(m,W);return _!==W&&s.call(u,_,m,i)?_:v}),v.__iterateUncached=function(v,_){var j=this,M=0;return i.__iterate((function(i,_,$){if(s.call(u,i,_,$))return M++,v(i,m?_:M-1,j)}),_),M},v.__iteratorUncached=function(v,_){var j=i.__iterator(ae,_),M=0;return new Iterator((function(){for(;;){var _=j.next();if(_.done)return _;var $=_.value,W=$[0],X=$[1];if(s.call(u,X,W,i))return iteratorValue(v,m?W:M++,X,_)}}))},v}function countByFactory(i,s,u){var m=Map().asMutable();return i.__iterate((function(v,_){m.update(s.call(u,v,_,i),0,(function(i){return i+1}))})),m.asImmutable()}function groupByFactory(i,s,u){var m=isKeyed(i),v=(isOrdered(i)?OrderedMap():Map()).asMutable();i.__iterate((function(_,j){v.update(s.call(u,_,j,i),(function(i){return(i=i||[]).push(m?[j,_]:_),i}))}));var _=iterableClass(i);return v.map((function(s){return reify(i,_(s))}))}function sliceFactory(i,s,u,m){var v=i.size;if(void 0!==s&&(s|=0),void 0!==u&&(u===1/0?u=v:u|=0),wholeSlice(s,u,v))return i;var _=resolveBegin(s,v),j=resolveEnd(u,v);if(_!=_||j!=j)return sliceFactory(i.toSeq().cacheResult(),s,u,m);var M,$=j-_;$==$&&(M=$<0?0:$);var W=makeSequence(i);return W.size=0===M?M:i.size&&M||void 0,!m&&isSeq(i)&&M>=0&&(W.get=function(s,u){return(s=wrapIndex(this,s))>=0&&sM)return iteratorDone();var i=v.next();return m||s===ee?i:iteratorValue(s,$-1,s===Z?void 0:i.value[1],i)}))},W}function takeWhileFactory(i,s,u){var m=makeSequence(i);return m.__iterateUncached=function(m,v){var _=this;if(v)return this.cacheResult().__iterate(m,v);var j=0;return i.__iterate((function(i,v,M){return s.call(u,i,v,M)&&++j&&m(i,v,_)})),j},m.__iteratorUncached=function(m,v){var _=this;if(v)return this.cacheResult().__iterator(m,v);var j=i.__iterator(ae,v),M=!0;return new Iterator((function(){if(!M)return iteratorDone();var i=j.next();if(i.done)return i;var v=i.value,$=v[0],W=v[1];return s.call(u,W,$,_)?m===ae?i:iteratorValue(m,$,W,i):(M=!1,iteratorDone())}))},m}function skipWhileFactory(i,s,u,m){var v=makeSequence(i);return v.__iterateUncached=function(v,_){var j=this;if(_)return this.cacheResult().__iterate(v,_);var M=!0,$=0;return i.__iterate((function(i,_,W){if(!M||!(M=s.call(u,i,_,W)))return $++,v(i,m?_:$-1,j)})),$},v.__iteratorUncached=function(v,_){var j=this;if(_)return this.cacheResult().__iterator(v,_);var M=i.__iterator(ae,_),$=!0,W=0;return new Iterator((function(){var i,_,X;do{if((i=M.next()).done)return m||v===ee?i:iteratorValue(v,W++,v===Z?void 0:i.value[1],i);var Y=i.value;_=Y[0],X=Y[1],$&&($=s.call(u,X,_,j))}while($);return v===ae?i:iteratorValue(v,_,X,i)}))},v}function concatFactory(i,s){var u=isKeyed(i),m=[i].concat(s).map((function(i){return isIterable(i)?u&&(i=KeyedIterable(i)):i=u?keyedSeqFromValue(i):indexedSeqFromValue(Array.isArray(i)?i:[i]),i})).filter((function(i){return 0!==i.size}));if(0===m.length)return i;if(1===m.length){var v=m[0];if(v===i||u&&isKeyed(v)||isIndexed(i)&&isIndexed(v))return v}var _=new ArraySeq(m);return u?_=_.toKeyedSeq():isIndexed(i)||(_=_.toSetSeq()),(_=_.flatten(!0)).size=m.reduce((function(i,s){if(void 0!==i){var u=s.size;if(void 0!==u)return i+u}}),0),_}function flattenFactory(i,s,u){var m=makeSequence(i);return m.__iterateUncached=function(m,v){var _=0,j=!1;function flatDeep(i,M){var $=this;i.__iterate((function(i,v){return(!s||M0}function zipWithFactory(i,s,u){var m=makeSequence(i);return m.size=new ArraySeq(u).map((function(i){return i.size})).min(),m.__iterate=function(i,s){for(var u,m=this.__iterator(ee,s),v=0;!(u=m.next()).done&&!1!==i(u.value,v++,this););return v},m.__iteratorUncached=function(i,m){var v=u.map((function(i){return i=Iterable(i),getIterator(m?i.reverse():i)})),_=0,j=!1;return new Iterator((function(){var u;return j||(u=v.map((function(i){return i.next()})),j=u.some((function(i){return i.done}))),j?iteratorDone():iteratorValue(i,_++,s.apply(null,u.map((function(i){return i.value}))))}))},m}function reify(i,s){return isSeq(i)?s:i.constructor(s)}function validateEntry(i){if(i!==Object(i))throw new TypeError("Expected [K, V] tuple: "+i)}function resolveSize(i){return assertNotInfinite(i.size),ensureSize(i)}function iterableClass(i){return isKeyed(i)?KeyedIterable:isIndexed(i)?IndexedIterable:SetIterable}function makeSequence(i){return Object.create((isKeyed(i)?KeyedSeq:isIndexed(i)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(i,s){return i>s?1:i=0;u--)s={value:arguments[u],next:s};return this.__ownerID?(this.size=i,this._head=s,this.__hash=void 0,this.__altered=!0,this):makeStack(i,s)},Stack.prototype.pushAll=function(i){if(0===(i=IndexedIterable(i)).size)return this;assertNotInfinite(i.size);var s=this.size,u=this._head;return i.reverse().forEach((function(i){s++,u={value:i,next:u}})),this.__ownerID?(this.size=s,this._head=u,this.__hash=void 0,this.__altered=!0,this):makeStack(s,u)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(i){return this.pushAll(i)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(i,s){if(wholeSlice(i,s,this.size))return this;var u=resolveBegin(i,this.size);if(resolveEnd(s,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,i,s);for(var m=this.size-u,v=this._head;u--;)v=v.next;return this.__ownerID?(this.size=m,this._head=v,this.__hash=void 0,this.__altered=!0,this):makeStack(m,v)},Stack.prototype.__ensureOwner=function(i){return i===this.__ownerID?this:i?makeStack(this.size,this._head,i,this.__hash):(this.__ownerID=i,this.__altered=!1,this)},Stack.prototype.__iterate=function(i,s){if(s)return this.reverse().__iterate(i);for(var u=0,m=this._head;m&&!1!==i(m.value,u++,this);)m=m.next;return u},Stack.prototype.__iterator=function(i,s){if(s)return this.reverse().__iterator(i);var u=0,m=this._head;return new Iterator((function(){if(m){var s=m.value;return m=m.next,iteratorValue(i,u++,s)}return iteratorDone()}))},Stack.isStack=isStack;var pt,ht="@@__IMMUTABLE_STACK__@@",dt=Stack.prototype;function makeStack(i,s,u,m){var v=Object.create(dt);return v.size=i,v._head=s,v.__ownerID=u,v.__hash=m,v.__altered=!1,v}function emptyStack(){return pt||(pt=makeStack(0))}function mixin(i,s){var keyCopier=function(u){i.prototype[u]=s[u]};return Object.keys(s).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(s).forEach(keyCopier),i}dt[ht]=!0,dt.withMutations=He.withMutations,dt.asMutable=He.asMutable,dt.asImmutable=He.asImmutable,dt.wasAltered=He.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var i=new Array(this.size||0);return this.valueSeq().__iterate((function(s,u){i[u]=s})),i},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(i){return i&&"function"==typeof i.toJS?i.toJS():i})).__toJS()},toJSON:function(){return this.toSeq().map((function(i){return i&&"function"==typeof i.toJSON?i.toJSON():i})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var i={};return this.__iterate((function(s,u){i[u]=s})),i},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(i,s){return 0===this.size?i+s:i+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+s},concat:function(){return reify(this,concatFactory(this,i.call(arguments,0)))},includes:function(i){return this.some((function(s){return is(s,i)}))},entries:function(){return this.__iterator(ae)},every:function(i,s){assertNotInfinite(this.size);var u=!0;return this.__iterate((function(m,v,_){if(!i.call(s,m,v,_))return u=!1,!1})),u},filter:function(i,s){return reify(this,filterFactory(this,i,s,!0))},find:function(i,s,u){var m=this.findEntry(i,s);return m?m[1]:u},forEach:function(i,s){return assertNotInfinite(this.size),this.__iterate(s?i.bind(s):i)},join:function(i){assertNotInfinite(this.size),i=void 0!==i?""+i:",";var s="",u=!0;return this.__iterate((function(m){u?u=!1:s+=i,s+=null!=m?m.toString():""})),s},keys:function(){return this.__iterator(Z)},map:function(i,s){return reify(this,mapFactory(this,i,s))},reduce:function(i,s,u){var m,v;return assertNotInfinite(this.size),arguments.length<2?v=!0:m=s,this.__iterate((function(s,_,j){v?(v=!1,m=s):m=i.call(u,m,s,_,j)})),m},reduceRight:function(i,s,u){var m=this.toKeyedSeq().reverse();return m.reduce.apply(m,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(i,s){return reify(this,sliceFactory(this,i,s,!0))},some:function(i,s){return!this.every(not(i),s)},sort:function(i){return reify(this,sortFactory(this,i))},values:function(){return this.__iterator(ee)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(i,s){return ensureSize(i?this.toSeq().filter(i,s):this)},countBy:function(i,s){return countByFactory(this,i,s)},equals:function(i){return deepEqual(this,i)},entrySeq:function(){var i=this;if(i._cache)return new ArraySeq(i._cache);var s=i.toSeq().map(entryMapper).toIndexedSeq();return s.fromEntrySeq=function(){return i.toSeq()},s},filterNot:function(i,s){return this.filter(not(i),s)},findEntry:function(i,s,u){var m=u;return this.__iterate((function(u,v,_){if(i.call(s,u,v,_))return m=[v,u],!1})),m},findKey:function(i,s){var u=this.findEntry(i,s);return u&&u[0]},findLast:function(i,s,u){return this.toKeyedSeq().reverse().find(i,s,u)},findLastEntry:function(i,s,u){return this.toKeyedSeq().reverse().findEntry(i,s,u)},findLastKey:function(i,s){return this.toKeyedSeq().reverse().findKey(i,s)},first:function(){return this.find(returnTrue)},flatMap:function(i,s){return reify(this,flatMapFactory(this,i,s))},flatten:function(i){return reify(this,flattenFactory(this,i,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(i,s){return this.find((function(s,u){return is(u,i)}),void 0,s)},getIn:function(i,s){for(var u,m=this,v=forceIterator(i);!(u=v.next()).done;){var _=u.value;if((m=m&&m.get?m.get(_,W):W)===W)return s}return m},groupBy:function(i,s){return groupByFactory(this,i,s)},has:function(i){return this.get(i,W)!==W},hasIn:function(i){return this.getIn(i,W)!==W},isSubset:function(i){return i="function"==typeof i.includes?i:Iterable(i),this.every((function(s){return i.includes(s)}))},isSuperset:function(i){return(i="function"==typeof i.isSubset?i:Iterable(i)).isSubset(this)},keyOf:function(i){return this.findKey((function(s){return is(s,i)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(i){return this.toKeyedSeq().reverse().keyOf(i)},max:function(i){return maxFactory(this,i)},maxBy:function(i,s){return maxFactory(this,s,i)},min:function(i){return maxFactory(this,i?neg(i):defaultNegComparator)},minBy:function(i,s){return maxFactory(this,s?neg(s):defaultNegComparator,i)},rest:function(){return this.slice(1)},skip:function(i){return this.slice(Math.max(0,i))},skipLast:function(i){return reify(this,this.toSeq().reverse().skip(i).reverse())},skipWhile:function(i,s){return reify(this,skipWhileFactory(this,i,s,!0))},skipUntil:function(i,s){return this.skipWhile(not(i),s)},sortBy:function(i,s){return reify(this,sortFactory(this,s,i))},take:function(i){return this.slice(0,Math.max(0,i))},takeLast:function(i){return reify(this,this.toSeq().reverse().take(i).reverse())},takeWhile:function(i,s){return reify(this,takeWhileFactory(this,i,s))},takeUntil:function(i,s){return this.takeWhile(not(i),s)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var mt=Iterable.prototype;mt[s]=!0,mt[ce]=mt.values,mt.__toJS=mt.toArray,mt.__toStringMapper=quoteString,mt.inspect=mt.toSource=function(){return this.toString()},mt.chain=mt.flatMap,mt.contains=mt.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(i,s){var u=this,m=0;return reify(this,this.toSeq().map((function(v,_){return i.call(s,[_,v],m++,u)})).fromEntrySeq())},mapKeys:function(i,s){var u=this;return reify(this,this.toSeq().flip().map((function(m,v){return i.call(s,m,v,u)})).flip())}});var gt=KeyedIterable.prototype;function keyMapper(i,s){return s}function entryMapper(i,s){return[s,i]}function not(i){return function(){return!i.apply(this,arguments)}}function neg(i){return function(){return-i.apply(this,arguments)}}function quoteString(i){return"string"==typeof i?JSON.stringify(i):String(i)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(i,s){return is?-1:0}function hashIterable(i){if(i.size===1/0)return 0;var s=isOrdered(i),u=isKeyed(i),m=s?1:0;return murmurHashOfSize(i.__iterate(u?s?function(i,s){m=31*m+hashMerge(hash(i),hash(s))|0}:function(i,s){m=m+hashMerge(hash(i),hash(s))|0}:s?function(i){m=31*m+hash(i)|0}:function(i){m=m+hash(i)|0}),m)}function murmurHashOfSize(i,s){return s=be(s,3432918353),s=be(s<<15|s>>>-15,461845907),s=be(s<<13|s>>>-13,5),s=be((s=(s+3864292196|0)^i)^s>>>16,2246822507),s=smi((s=be(s^s>>>13,3266489909))^s>>>16)}function hashMerge(i,s){return i^s+2654435769+(i<<6)+(i>>2)|0}return gt[u]=!0,gt[ce]=mt.entries,gt.__toJS=mt.toObject,gt.__toStringMapper=function(i,s){return JSON.stringify(s)+": "+quoteString(i)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(i,s){return reify(this,filterFactory(this,i,s,!1))},findIndex:function(i,s){var u=this.findEntry(i,s);return u?u[0]:-1},indexOf:function(i){var s=this.keyOf(i);return void 0===s?-1:s},lastIndexOf:function(i){var s=this.lastKeyOf(i);return void 0===s?-1:s},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(i,s){return reify(this,sliceFactory(this,i,s,!1))},splice:function(i,s){var u=arguments.length;if(s=Math.max(0|s,0),0===u||2===u&&!s)return this;i=resolveBegin(i,i<0?this.count():this.size);var m=this.slice(0,i);return reify(this,1===u?m:m.concat(arrCopy(arguments,2),this.slice(i+s)))},findLastIndex:function(i,s){var u=this.findLastEntry(i,s);return u?u[0]:-1},first:function(){return this.get(0)},flatten:function(i){return reify(this,flattenFactory(this,i,!1))},get:function(i,s){return(i=wrapIndex(this,i))<0||this.size===1/0||void 0!==this.size&&i>this.size?s:this.find((function(s,u){return u===i}),void 0,s)},has:function(i){return(i=wrapIndex(this,i))>=0&&(void 0!==this.size?this.size===1/0||i{"function"==typeof Object.create?i.exports=function inherits(i,s){s&&(i.super_=s,i.prototype=Object.create(s.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}))}:i.exports=function inherits(i,s){if(s){i.super_=s;var TempCtor=function(){};TempCtor.prototype=s.prototype,i.prototype=new TempCtor,i.prototype.constructor=i}}},35823:i=>{i.exports=function(i,s,u,m){var v=new Blob(void 0!==m?[m,i]:[i],{type:u||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(v,s);else{var _=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(v):window.webkitURL.createObjectURL(v),j=document.createElement("a");j.style.display="none",j.href=_,j.setAttribute("download",s),void 0===j.download&&j.setAttribute("target","_blank"),document.body.appendChild(j),j.click(),setTimeout((function(){document.body.removeChild(j),window.URL.revokeObjectURL(_)}),200)}}},91296:(i,s,u)=>{var m=NaN,v="[object Symbol]",_=/^\s+|\s+$/g,j=/^[-+]0x[0-9a-f]+$/i,M=/^0b[01]+$/i,$=/^0o[0-7]+$/i,W=parseInt,X="object"==typeof u.g&&u.g&&u.g.Object===Object&&u.g,Y="object"==typeof self&&self&&self.Object===Object&&self,Z=X||Y||Function("return this")(),ee=Object.prototype.toString,ae=Math.max,ie=Math.min,now=function(){return Z.Date.now()};function isObject(i){var s=typeof i;return!!i&&("object"==s||"function"==s)}function toNumber(i){if("number"==typeof i)return i;if(function isSymbol(i){return"symbol"==typeof i||function isObjectLike(i){return!!i&&"object"==typeof i}(i)&&ee.call(i)==v}(i))return m;if(isObject(i)){var s="function"==typeof i.valueOf?i.valueOf():i;i=isObject(s)?s+"":s}if("string"!=typeof i)return 0===i?i:+i;i=i.replace(_,"");var u=M.test(i);return u||$.test(i)?W(i.slice(2),u?2:8):j.test(i)?m:+i}i.exports=function debounce(i,s,u){var m,v,_,j,M,$,W=0,X=!1,Y=!1,Z=!0;if("function"!=typeof i)throw new TypeError("Expected a function");function invokeFunc(s){var u=m,_=v;return m=v=void 0,W=s,j=i.apply(_,u)}function shouldInvoke(i){var u=i-$;return void 0===$||u>=s||u<0||Y&&i-W>=_}function timerExpired(){var i=now();if(shouldInvoke(i))return trailingEdge(i);M=setTimeout(timerExpired,function remainingWait(i){var u=s-(i-$);return Y?ie(u,_-(i-W)):u}(i))}function trailingEdge(i){return M=void 0,Z&&m?invokeFunc(i):(m=v=void 0,j)}function debounced(){var i=now(),u=shouldInvoke(i);if(m=arguments,v=this,$=i,u){if(void 0===M)return function leadingEdge(i){return W=i,M=setTimeout(timerExpired,s),X?invokeFunc(i):j}($);if(Y)return M=setTimeout(timerExpired,s),invokeFunc($)}return void 0===M&&(M=setTimeout(timerExpired,s)),j}return s=toNumber(s)||0,isObject(u)&&(X=!!u.leading,_=(Y="maxWait"in u)?ae(toNumber(u.maxWait)||0,s):_,Z="trailing"in u?!!u.trailing:Z),debounced.cancel=function cancel(){void 0!==M&&clearTimeout(M),W=0,m=$=v=M=void 0},debounced.flush=function flush(){return void 0===M?j:trailingEdge(now())},debounced}},18552:(i,s,u)=>{var m=u(10852)(u(55639),"DataView");i.exports=m},1989:(i,s,u)=>{var m=u(51789),v=u(80401),_=u(57667),j=u(21327),M=u(81866);function Hash(i){var s=-1,u=null==i?0:i.length;for(this.clear();++s{var m=u(3118),v=u(9435);function LazyWrapper(i){this.__wrapped__=i,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}LazyWrapper.prototype=m(v.prototype),LazyWrapper.prototype.constructor=LazyWrapper,i.exports=LazyWrapper},38407:(i,s,u)=>{var m=u(27040),v=u(14125),_=u(82117),j=u(67518),M=u(54705);function ListCache(i){var s=-1,u=null==i?0:i.length;for(this.clear();++s{var m=u(3118),v=u(9435);function LodashWrapper(i,s){this.__wrapped__=i,this.__actions__=[],this.__chain__=!!s,this.__index__=0,this.__values__=void 0}LodashWrapper.prototype=m(v.prototype),LodashWrapper.prototype.constructor=LodashWrapper,i.exports=LodashWrapper},57071:(i,s,u)=>{var m=u(10852)(u(55639),"Map");i.exports=m},83369:(i,s,u)=>{var m=u(24785),v=u(11285),_=u(96e3),j=u(49916),M=u(95265);function MapCache(i){var s=-1,u=null==i?0:i.length;for(this.clear();++s{var m=u(10852)(u(55639),"Promise");i.exports=m},58525:(i,s,u)=>{var m=u(10852)(u(55639),"Set");i.exports=m},88668:(i,s,u)=>{var m=u(83369),v=u(90619),_=u(72385);function SetCache(i){var s=-1,u=null==i?0:i.length;for(this.__data__=new m;++s{var m=u(38407),v=u(37465),_=u(63779),j=u(67599),M=u(44758),$=u(34309);function Stack(i){var s=this.__data__=new m(i);this.size=s.size}Stack.prototype.clear=v,Stack.prototype.delete=_,Stack.prototype.get=j,Stack.prototype.has=M,Stack.prototype.set=$,i.exports=Stack},62705:(i,s,u)=>{var m=u(55639).Symbol;i.exports=m},11149:(i,s,u)=>{var m=u(55639).Uint8Array;i.exports=m},70577:(i,s,u)=>{var m=u(10852)(u(55639),"WeakMap");i.exports=m},96874:i=>{i.exports=function apply(i,s,u){switch(u.length){case 0:return i.call(s);case 1:return i.call(s,u[0]);case 2:return i.call(s,u[0],u[1]);case 3:return i.call(s,u[0],u[1],u[2])}return i.apply(s,u)}},77412:i=>{i.exports=function arrayEach(i,s){for(var u=-1,m=null==i?0:i.length;++u{i.exports=function arrayFilter(i,s){for(var u=-1,m=null==i?0:i.length,v=0,_=[];++u{var m=u(42118);i.exports=function arrayIncludes(i,s){return!!(null==i?0:i.length)&&m(i,s,0)>-1}},14636:(i,s,u)=>{var m=u(22545),v=u(35694),_=u(1469),j=u(44144),M=u(65776),$=u(36719),W=Object.prototype.hasOwnProperty;i.exports=function arrayLikeKeys(i,s){var u=_(i),X=!u&&v(i),Y=!u&&!X&&j(i),Z=!u&&!X&&!Y&&$(i),ee=u||X||Y||Z,ae=ee?m(i.length,String):[],ie=ae.length;for(var le in i)!s&&!W.call(i,le)||ee&&("length"==le||Y&&("offset"==le||"parent"==le)||Z&&("buffer"==le||"byteLength"==le||"byteOffset"==le)||M(le,ie))||ae.push(le);return ae}},29932:i=>{i.exports=function arrayMap(i,s){for(var u=-1,m=null==i?0:i.length,v=Array(m);++u{i.exports=function arrayPush(i,s){for(var u=-1,m=s.length,v=i.length;++u{i.exports=function arrayReduce(i,s,u,m){var v=-1,_=null==i?0:i.length;for(m&&_&&(u=i[++v]);++v<_;)u=s(u,i[v],v,i);return u}},82908:i=>{i.exports=function arraySome(i,s){for(var u=-1,m=null==i?0:i.length;++u{i.exports=function asciiToArray(i){return i.split("")}},49029:i=>{var s=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;i.exports=function asciiWords(i){return i.match(s)||[]}},86556:(i,s,u)=>{var m=u(89465),v=u(77813);i.exports=function assignMergeValue(i,s,u){(void 0!==u&&!v(i[s],u)||void 0===u&&!(s in i))&&m(i,s,u)}},34865:(i,s,u)=>{var m=u(89465),v=u(77813),_=Object.prototype.hasOwnProperty;i.exports=function assignValue(i,s,u){var j=i[s];_.call(i,s)&&v(j,u)&&(void 0!==u||s in i)||m(i,s,u)}},18470:(i,s,u)=>{var m=u(77813);i.exports=function assocIndexOf(i,s){for(var u=i.length;u--;)if(m(i[u][0],s))return u;return-1}},44037:(i,s,u)=>{var m=u(98363),v=u(3674);i.exports=function baseAssign(i,s){return i&&m(s,v(s),i)}},63886:(i,s,u)=>{var m=u(98363),v=u(81704);i.exports=function baseAssignIn(i,s){return i&&m(s,v(s),i)}},89465:(i,s,u)=>{var m=u(38777);i.exports=function baseAssignValue(i,s,u){"__proto__"==s&&m?m(i,s,{configurable:!0,enumerable:!0,value:u,writable:!0}):i[s]=u}},85990:(i,s,u)=>{var m=u(46384),v=u(77412),_=u(34865),j=u(44037),M=u(63886),$=u(64626),W=u(278),X=u(18805),Y=u(1911),Z=u(58234),ee=u(46904),ae=u(64160),ie=u(43824),le=u(29148),ce=u(38517),pe=u(1469),de=u(44144),fe=u(56688),ye=u(13218),be=u(72928),_e=u(3674),we=u(81704),Se="[object Arguments]",xe="[object Function]",Pe="[object Object]",Ie={};Ie[Se]=Ie["[object Array]"]=Ie["[object ArrayBuffer]"]=Ie["[object DataView]"]=Ie["[object Boolean]"]=Ie["[object Date]"]=Ie["[object Float32Array]"]=Ie["[object Float64Array]"]=Ie["[object Int8Array]"]=Ie["[object Int16Array]"]=Ie["[object Int32Array]"]=Ie["[object Map]"]=Ie["[object Number]"]=Ie[Pe]=Ie["[object RegExp]"]=Ie["[object Set]"]=Ie["[object String]"]=Ie["[object Symbol]"]=Ie["[object Uint8Array]"]=Ie["[object Uint8ClampedArray]"]=Ie["[object Uint16Array]"]=Ie["[object Uint32Array]"]=!0,Ie["[object Error]"]=Ie[xe]=Ie["[object WeakMap]"]=!1,i.exports=function baseClone(i,s,u,Te,Re,qe){var ze,Ve=1&s,We=2&s,He=4&s;if(u&&(ze=Re?u(i,Te,Re,qe):u(i)),void 0!==ze)return ze;if(!ye(i))return i;var Xe=pe(i);if(Xe){if(ze=ie(i),!Ve)return W(i,ze)}else{var Ye=ae(i),Qe=Ye==xe||"[object GeneratorFunction]"==Ye;if(de(i))return $(i,Ve);if(Ye==Pe||Ye==Se||Qe&&!Re){if(ze=We||Qe?{}:ce(i),!Ve)return We?Y(i,M(ze,i)):X(i,j(ze,i))}else{if(!Ie[Ye])return Re?i:{};ze=le(i,Ye,Ve)}}qe||(qe=new m);var et=qe.get(i);if(et)return et;qe.set(i,ze),be(i)?i.forEach((function(m){ze.add(baseClone(m,s,u,m,i,qe))})):fe(i)&&i.forEach((function(m,v){ze.set(v,baseClone(m,s,u,v,i,qe))}));var tt=Xe?void 0:(He?We?ee:Z:We?we:_e)(i);return v(tt||i,(function(m,v){tt&&(m=i[v=m]),_(ze,v,baseClone(m,s,u,v,i,qe))})),ze}},3118:(i,s,u)=>{var m=u(13218),v=Object.create,_=function(){function object(){}return function(i){if(!m(i))return{};if(v)return v(i);object.prototype=i;var s=new object;return object.prototype=void 0,s}}();i.exports=_},89881:(i,s,u)=>{var m=u(47816),v=u(99291)(m);i.exports=v},41848:i=>{i.exports=function baseFindIndex(i,s,u,m){for(var v=i.length,_=u+(m?1:-1);m?_--:++_{var m=u(62488),v=u(37285);i.exports=function baseFlatten(i,s,u,_,j){var M=-1,$=i.length;for(u||(u=v),j||(j=[]);++M<$;){var W=i[M];s>0&&u(W)?s>1?baseFlatten(W,s-1,u,_,j):m(j,W):_||(j[j.length]=W)}return j}},28483:(i,s,u)=>{var m=u(25063)();i.exports=m},47816:(i,s,u)=>{var m=u(28483),v=u(3674);i.exports=function baseForOwn(i,s){return i&&m(i,s,v)}},97786:(i,s,u)=>{var m=u(71811),v=u(40327);i.exports=function baseGet(i,s){for(var u=0,_=(s=m(s,i)).length;null!=i&&u<_;)i=i[v(s[u++])];return u&&u==_?i:void 0}},68866:(i,s,u)=>{var m=u(62488),v=u(1469);i.exports=function baseGetAllKeys(i,s,u){var _=s(i);return v(i)?_:m(_,u(i))}},44239:(i,s,u)=>{var m=u(62705),v=u(89607),_=u(2333),j=m?m.toStringTag:void 0;i.exports=function baseGetTag(i){return null==i?void 0===i?"[object Undefined]":"[object Null]":j&&j in Object(i)?v(i):_(i)}},13:i=>{i.exports=function baseHasIn(i,s){return null!=i&&s in Object(i)}},42118:(i,s,u)=>{var m=u(41848),v=u(62722),_=u(42351);i.exports=function baseIndexOf(i,s,u){return s==s?_(i,s,u):m(i,v,u)}},9454:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function baseIsArguments(i){return v(i)&&"[object Arguments]"==m(i)}},90939:(i,s,u)=>{var m=u(2492),v=u(37005);i.exports=function baseIsEqual(i,s,u,_,j){return i===s||(null==i||null==s||!v(i)&&!v(s)?i!=i&&s!=s:m(i,s,u,_,baseIsEqual,j))}},2492:(i,s,u)=>{var m=u(46384),v=u(67114),_=u(18351),j=u(16096),M=u(64160),$=u(1469),W=u(44144),X=u(36719),Y="[object Arguments]",Z="[object Array]",ee="[object Object]",ae=Object.prototype.hasOwnProperty;i.exports=function baseIsEqualDeep(i,s,u,ie,le,ce){var pe=$(i),de=$(s),fe=pe?Z:M(i),ye=de?Z:M(s),be=(fe=fe==Y?ee:fe)==ee,_e=(ye=ye==Y?ee:ye)==ee,we=fe==ye;if(we&&W(i)){if(!W(s))return!1;pe=!0,be=!1}if(we&&!be)return ce||(ce=new m),pe||X(i)?v(i,s,u,ie,le,ce):_(i,s,fe,u,ie,le,ce);if(!(1&u)){var Se=be&&ae.call(i,"__wrapped__"),xe=_e&&ae.call(s,"__wrapped__");if(Se||xe){var Pe=Se?i.value():i,Ie=xe?s.value():s;return ce||(ce=new m),le(Pe,Ie,u,ie,ce)}}return!!we&&(ce||(ce=new m),j(i,s,u,ie,le,ce))}},25588:(i,s,u)=>{var m=u(64160),v=u(37005);i.exports=function baseIsMap(i){return v(i)&&"[object Map]"==m(i)}},2958:(i,s,u)=>{var m=u(46384),v=u(90939);i.exports=function baseIsMatch(i,s,u,_){var j=u.length,M=j,$=!_;if(null==i)return!M;for(i=Object(i);j--;){var W=u[j];if($&&W[2]?W[1]!==i[W[0]]:!(W[0]in i))return!1}for(;++j{i.exports=function baseIsNaN(i){return i!=i}},28458:(i,s,u)=>{var m=u(23560),v=u(15346),_=u(13218),j=u(80346),M=/^\[object .+?Constructor\]$/,$=Function.prototype,W=Object.prototype,X=$.toString,Y=W.hasOwnProperty,Z=RegExp("^"+X.call(Y).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");i.exports=function baseIsNative(i){return!(!_(i)||v(i))&&(m(i)?Z:M).test(j(i))}},29221:(i,s,u)=>{var m=u(64160),v=u(37005);i.exports=function baseIsSet(i){return v(i)&&"[object Set]"==m(i)}},38749:(i,s,u)=>{var m=u(44239),v=u(41780),_=u(37005),j={};j["[object Float32Array]"]=j["[object Float64Array]"]=j["[object Int8Array]"]=j["[object Int16Array]"]=j["[object Int32Array]"]=j["[object Uint8Array]"]=j["[object Uint8ClampedArray]"]=j["[object Uint16Array]"]=j["[object Uint32Array]"]=!0,j["[object Arguments]"]=j["[object Array]"]=j["[object ArrayBuffer]"]=j["[object Boolean]"]=j["[object DataView]"]=j["[object Date]"]=j["[object Error]"]=j["[object Function]"]=j["[object Map]"]=j["[object Number]"]=j["[object Object]"]=j["[object RegExp]"]=j["[object Set]"]=j["[object String]"]=j["[object WeakMap]"]=!1,i.exports=function baseIsTypedArray(i){return _(i)&&v(i.length)&&!!j[m(i)]}},67206:(i,s,u)=>{var m=u(91573),v=u(16432),_=u(6557),j=u(1469),M=u(39601);i.exports=function baseIteratee(i){return"function"==typeof i?i:null==i?_:"object"==typeof i?j(i)?v(i[0],i[1]):m(i):M(i)}},280:(i,s,u)=>{var m=u(25726),v=u(86916),_=Object.prototype.hasOwnProperty;i.exports=function baseKeys(i){if(!m(i))return v(i);var s=[];for(var u in Object(i))_.call(i,u)&&"constructor"!=u&&s.push(u);return s}},10313:(i,s,u)=>{var m=u(13218),v=u(25726),_=u(33498),j=Object.prototype.hasOwnProperty;i.exports=function baseKeysIn(i){if(!m(i))return _(i);var s=v(i),u=[];for(var M in i)("constructor"!=M||!s&&j.call(i,M))&&u.push(M);return u}},9435:i=>{i.exports=function baseLodash(){}},91573:(i,s,u)=>{var m=u(2958),v=u(1499),_=u(42634);i.exports=function baseMatches(i){var s=v(i);return 1==s.length&&s[0][2]?_(s[0][0],s[0][1]):function(u){return u===i||m(u,i,s)}}},16432:(i,s,u)=>{var m=u(90939),v=u(27361),_=u(79095),j=u(15403),M=u(89162),$=u(42634),W=u(40327);i.exports=function baseMatchesProperty(i,s){return j(i)&&M(s)?$(W(i),s):function(u){var j=v(u,i);return void 0===j&&j===s?_(u,i):m(s,j,3)}}},42980:(i,s,u)=>{var m=u(46384),v=u(86556),_=u(28483),j=u(59783),M=u(13218),$=u(81704),W=u(36390);i.exports=function baseMerge(i,s,u,X,Y){i!==s&&_(s,(function(_,$){if(Y||(Y=new m),M(_))j(i,s,$,u,baseMerge,X,Y);else{var Z=X?X(W(i,$),_,$+"",i,s,Y):void 0;void 0===Z&&(Z=_),v(i,$,Z)}}),$)}},59783:(i,s,u)=>{var m=u(86556),v=u(64626),_=u(77133),j=u(278),M=u(38517),$=u(35694),W=u(1469),X=u(29246),Y=u(44144),Z=u(23560),ee=u(13218),ae=u(68630),ie=u(36719),le=u(36390),ce=u(59881);i.exports=function baseMergeDeep(i,s,u,pe,de,fe,ye){var be=le(i,u),_e=le(s,u),we=ye.get(_e);if(we)m(i,u,we);else{var Se=fe?fe(be,_e,u+"",i,s,ye):void 0,xe=void 0===Se;if(xe){var Pe=W(_e),Ie=!Pe&&Y(_e),Te=!Pe&&!Ie&&ie(_e);Se=_e,Pe||Ie||Te?W(be)?Se=be:X(be)?Se=j(be):Ie?(xe=!1,Se=v(_e,!0)):Te?(xe=!1,Se=_(_e,!0)):Se=[]:ae(_e)||$(_e)?(Se=be,$(be)?Se=ce(be):ee(be)&&!Z(be)||(Se=M(_e))):xe=!1}xe&&(ye.set(_e,Se),de(Se,_e,pe,fe,ye),ye.delete(_e)),m(i,u,Se)}}},40371:i=>{i.exports=function baseProperty(i){return function(s){return null==s?void 0:s[i]}}},79152:(i,s,u)=>{var m=u(97786);i.exports=function basePropertyDeep(i){return function(s){return m(s,i)}}},18674:i=>{i.exports=function basePropertyOf(i){return function(s){return null==i?void 0:i[s]}}},10107:i=>{i.exports=function baseReduce(i,s,u,m,v){return v(i,(function(i,v,_){u=m?(m=!1,i):s(u,i,v,_)})),u}},5976:(i,s,u)=>{var m=u(6557),v=u(45357),_=u(30061);i.exports=function baseRest(i,s){return _(v(i,s,m),i+"")}},10611:(i,s,u)=>{var m=u(34865),v=u(71811),_=u(65776),j=u(13218),M=u(40327);i.exports=function baseSet(i,s,u,$){if(!j(i))return i;for(var W=-1,X=(s=v(s,i)).length,Y=X-1,Z=i;null!=Z&&++W{var m=u(6557),v=u(89250),_=v?function(i,s){return v.set(i,s),i}:m;i.exports=_},56560:(i,s,u)=>{var m=u(75703),v=u(38777),_=u(6557),j=v?function(i,s){return v(i,"toString",{configurable:!0,enumerable:!1,value:m(s),writable:!0})}:_;i.exports=j},14259:i=>{i.exports=function baseSlice(i,s,u){var m=-1,v=i.length;s<0&&(s=-s>v?0:v+s),(u=u>v?v:u)<0&&(u+=v),v=s>u?0:u-s>>>0,s>>>=0;for(var _=Array(v);++m{var m=u(89881);i.exports=function baseSome(i,s){var u;return m(i,(function(i,m,v){return!(u=s(i,m,v))})),!!u}},22545:i=>{i.exports=function baseTimes(i,s){for(var u=-1,m=Array(i);++u{var m=u(62705),v=u(29932),_=u(1469),j=u(33448),M=m?m.prototype:void 0,$=M?M.toString:void 0;i.exports=function baseToString(i){if("string"==typeof i)return i;if(_(i))return v(i,baseToString)+"";if(j(i))return $?$.call(i):"";var s=i+"";return"0"==s&&1/i==-Infinity?"-0":s}},27561:(i,s,u)=>{var m=u(67990),v=/^\s+/;i.exports=function baseTrim(i){return i?i.slice(0,m(i)+1).replace(v,""):i}},7518:i=>{i.exports=function baseUnary(i){return function(s){return i(s)}}},57406:(i,s,u)=>{var m=u(71811),v=u(10928),_=u(40292),j=u(40327);i.exports=function baseUnset(i,s){return s=m(s,i),null==(i=_(i,s))||delete i[j(v(s))]}},1757:i=>{i.exports=function baseZipObject(i,s,u){for(var m=-1,v=i.length,_=s.length,j={};++m{i.exports=function cacheHas(i,s){return i.has(s)}},71811:(i,s,u)=>{var m=u(1469),v=u(15403),_=u(55514),j=u(79833);i.exports=function castPath(i,s){return m(i)?i:v(i,s)?[i]:_(j(i))}},40180:(i,s,u)=>{var m=u(14259);i.exports=function castSlice(i,s,u){var v=i.length;return u=void 0===u?v:u,!s&&u>=v?i:m(i,s,u)}},74318:(i,s,u)=>{var m=u(11149);i.exports=function cloneArrayBuffer(i){var s=new i.constructor(i.byteLength);return new m(s).set(new m(i)),s}},64626:(i,s,u)=>{i=u.nmd(i);var m=u(55639),v=s&&!s.nodeType&&s,_=v&&i&&!i.nodeType&&i,j=_&&_.exports===v?m.Buffer:void 0,M=j?j.allocUnsafe:void 0;i.exports=function cloneBuffer(i,s){if(s)return i.slice();var u=i.length,m=M?M(u):new i.constructor(u);return i.copy(m),m}},57157:(i,s,u)=>{var m=u(74318);i.exports=function cloneDataView(i,s){var u=s?m(i.buffer):i.buffer;return new i.constructor(u,i.byteOffset,i.byteLength)}},93147:i=>{var s=/\w*$/;i.exports=function cloneRegExp(i){var u=new i.constructor(i.source,s.exec(i));return u.lastIndex=i.lastIndex,u}},40419:(i,s,u)=>{var m=u(62705),v=m?m.prototype:void 0,_=v?v.valueOf:void 0;i.exports=function cloneSymbol(i){return _?Object(_.call(i)):{}}},77133:(i,s,u)=>{var m=u(74318);i.exports=function cloneTypedArray(i,s){var u=s?m(i.buffer):i.buffer;return new i.constructor(u,i.byteOffset,i.length)}},52157:i=>{var s=Math.max;i.exports=function composeArgs(i,u,m,v){for(var _=-1,j=i.length,M=m.length,$=-1,W=u.length,X=s(j-M,0),Y=Array(W+X),Z=!v;++${var s=Math.max;i.exports=function composeArgsRight(i,u,m,v){for(var _=-1,j=i.length,M=-1,$=m.length,W=-1,X=u.length,Y=s(j-$,0),Z=Array(Y+X),ee=!v;++_{i.exports=function copyArray(i,s){var u=-1,m=i.length;for(s||(s=Array(m));++u{var m=u(34865),v=u(89465);i.exports=function copyObject(i,s,u,_){var j=!u;u||(u={});for(var M=-1,$=s.length;++M<$;){var W=s[M],X=_?_(u[W],i[W],W,u,i):void 0;void 0===X&&(X=i[W]),j?v(u,W,X):m(u,W,X)}return u}},18805:(i,s,u)=>{var m=u(98363),v=u(99551);i.exports=function copySymbols(i,s){return m(i,v(i),s)}},1911:(i,s,u)=>{var m=u(98363),v=u(51442);i.exports=function copySymbolsIn(i,s){return m(i,v(i),s)}},14429:(i,s,u)=>{var m=u(55639)["__core-js_shared__"];i.exports=m},97991:i=>{i.exports=function countHolders(i,s){for(var u=i.length,m=0;u--;)i[u]===s&&++m;return m}},21463:(i,s,u)=>{var m=u(5976),v=u(16612);i.exports=function createAssigner(i){return m((function(s,u){var m=-1,_=u.length,j=_>1?u[_-1]:void 0,M=_>2?u[2]:void 0;for(j=i.length>3&&"function"==typeof j?(_--,j):void 0,M&&v(u[0],u[1],M)&&(j=_<3?void 0:j,_=1),s=Object(s);++m<_;){var $=u[m];$&&i(s,$,m,j)}return s}))}},99291:(i,s,u)=>{var m=u(98612);i.exports=function createBaseEach(i,s){return function(u,v){if(null==u)return u;if(!m(u))return i(u,v);for(var _=u.length,j=s?_:-1,M=Object(u);(s?j--:++j<_)&&!1!==v(M[j],j,M););return u}}},25063:i=>{i.exports=function createBaseFor(i){return function(s,u,m){for(var v=-1,_=Object(s),j=m(s),M=j.length;M--;){var $=j[i?M:++v];if(!1===u(_[$],$,_))break}return s}}},22402:(i,s,u)=>{var m=u(71774),v=u(55639);i.exports=function createBind(i,s,u){var _=1&s,j=m(i);return function wrapper(){return(this&&this!==v&&this instanceof wrapper?j:i).apply(_?u:this,arguments)}}},98805:(i,s,u)=>{var m=u(40180),v=u(62689),_=u(83140),j=u(79833);i.exports=function createCaseFirst(i){return function(s){s=j(s);var u=v(s)?_(s):void 0,M=u?u[0]:s.charAt(0),$=u?m(u,1).join(""):s.slice(1);return M[i]()+$}}},35393:(i,s,u)=>{var m=u(62663),v=u(53816),_=u(58748),j=RegExp("['’]","g");i.exports=function createCompounder(i){return function(s){return m(_(v(s).replace(j,"")),i,"")}}},71774:(i,s,u)=>{var m=u(3118),v=u(13218);i.exports=function createCtor(i){return function(){var s=arguments;switch(s.length){case 0:return new i;case 1:return new i(s[0]);case 2:return new i(s[0],s[1]);case 3:return new i(s[0],s[1],s[2]);case 4:return new i(s[0],s[1],s[2],s[3]);case 5:return new i(s[0],s[1],s[2],s[3],s[4]);case 6:return new i(s[0],s[1],s[2],s[3],s[4],s[5]);case 7:return new i(s[0],s[1],s[2],s[3],s[4],s[5],s[6])}var u=m(i.prototype),_=i.apply(u,s);return v(_)?_:u}}},46347:(i,s,u)=>{var m=u(96874),v=u(71774),_=u(86935),j=u(94487),M=u(20893),$=u(46460),W=u(55639);i.exports=function createCurry(i,s,u){var X=v(i);return function wrapper(){for(var v=arguments.length,Y=Array(v),Z=v,ee=M(wrapper);Z--;)Y[Z]=arguments[Z];var ae=v<3&&Y[0]!==ee&&Y[v-1]!==ee?[]:$(Y,ee);return(v-=ae.length){var m=u(67206),v=u(98612),_=u(3674);i.exports=function createFind(i){return function(s,u,j){var M=Object(s);if(!v(s)){var $=m(u,3);s=_(s),u=function(i){return $(M[i],i,M)}}var W=i(s,u,j);return W>-1?M[$?s[W]:W]:void 0}}},86935:(i,s,u)=>{var m=u(52157),v=u(14054),_=u(97991),j=u(71774),M=u(94487),$=u(20893),W=u(90451),X=u(46460),Y=u(55639);i.exports=function createHybrid(i,s,u,Z,ee,ae,ie,le,ce,pe){var de=128&s,fe=1&s,ye=2&s,be=24&s,_e=512&s,we=ye?void 0:j(i);return function wrapper(){for(var Se=arguments.length,xe=Array(Se),Pe=Se;Pe--;)xe[Pe]=arguments[Pe];if(be)var Ie=$(wrapper),Te=_(xe,Ie);if(Z&&(xe=m(xe,Z,ee,be)),ae&&(xe=v(xe,ae,ie,be)),Se-=Te,be&&Se1&&xe.reverse(),de&&ce{var m=u(96874),v=u(71774),_=u(55639);i.exports=function createPartial(i,s,u,j){var M=1&s,$=v(i);return function wrapper(){for(var s=-1,v=arguments.length,W=-1,X=j.length,Y=Array(X+v),Z=this&&this!==_&&this instanceof wrapper?$:i;++W{var m=u(86528),v=u(258),_=u(69255);i.exports=function createRecurry(i,s,u,j,M,$,W,X,Y,Z){var ee=8&s;s|=ee?32:64,4&(s&=~(ee?64:32))||(s&=-4);var ae=[i,s,M,ee?$:void 0,ee?W:void 0,ee?void 0:$,ee?void 0:W,X,Y,Z],ie=u.apply(void 0,ae);return m(i)&&v(ie,ae),ie.placeholder=j,_(ie,i,s)}},97727:(i,s,u)=>{var m=u(28045),v=u(22402),_=u(46347),j=u(86935),M=u(84375),$=u(66833),W=u(63833),X=u(258),Y=u(69255),Z=u(40554),ee=Math.max;i.exports=function createWrap(i,s,u,ae,ie,le,ce,pe){var de=2&s;if(!de&&"function"!=typeof i)throw new TypeError("Expected a function");var fe=ae?ae.length:0;if(fe||(s&=-97,ae=ie=void 0),ce=void 0===ce?ce:ee(Z(ce),0),pe=void 0===pe?pe:Z(pe),fe-=ie?ie.length:0,64&s){var ye=ae,be=ie;ae=ie=void 0}var _e=de?void 0:$(i),we=[i,s,u,ae,ie,ye,be,le,ce,pe];if(_e&&W(we,_e),i=we[0],s=we[1],u=we[2],ae=we[3],ie=we[4],!(pe=we[9]=void 0===we[9]?de?0:i.length:ee(we[9]-fe,0))&&24&s&&(s&=-25),s&&1!=s)Se=8==s||16==s?_(i,s,pe):32!=s&&33!=s||ie.length?j.apply(void 0,we):M(i,s,u,ae);else var Se=v(i,s,u);return Y((_e?m:X)(Se,we),i,s)}},60696:(i,s,u)=>{var m=u(68630);i.exports=function customOmitClone(i){return m(i)?void 0:i}},69389:(i,s,u)=>{var m=u(18674)({À:"A",Á:"A",Â:"A",Ã:"A",Ä:"A",Å:"A",à:"a",á:"a",â:"a",ã:"a",ä:"a",å:"a",Ç:"C",ç:"c",Ð:"D",ð:"d",È:"E",É:"E",Ê:"E",Ë:"E",è:"e",é:"e",ê:"e",ë:"e",Ì:"I",Í:"I",Î:"I",Ï:"I",ì:"i",í:"i",î:"i",ï:"i",Ñ:"N",ñ:"n",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"O",Ø:"O",ò:"o",ó:"o",ô:"o",õ:"o",ö:"o",ø:"o",Ù:"U",Ú:"U",Û:"U",Ü:"U",ù:"u",ú:"u",û:"u",ü:"u",Ý:"Y",ý:"y",ÿ:"y",Æ:"Ae",æ:"ae",Þ:"Th",þ:"th",ß:"ss",Ā:"A",Ă:"A",Ą:"A",ā:"a",ă:"a",ą:"a",Ć:"C",Ĉ:"C",Ċ:"C",Č:"C",ć:"c",ĉ:"c",ċ:"c",č:"c",Ď:"D",Đ:"D",ď:"d",đ:"d",Ē:"E",Ĕ:"E",Ė:"E",Ę:"E",Ě:"E",ē:"e",ĕ:"e",ė:"e",ę:"e",ě:"e",Ĝ:"G",Ğ:"G",Ġ:"G",Ģ:"G",ĝ:"g",ğ:"g",ġ:"g",ģ:"g",Ĥ:"H",Ħ:"H",ĥ:"h",ħ:"h",Ĩ:"I",Ī:"I",Ĭ:"I",Į:"I",İ:"I",ĩ:"i",ī:"i",ĭ:"i",į:"i",ı:"i",Ĵ:"J",ĵ:"j",Ķ:"K",ķ:"k",ĸ:"k",Ĺ:"L",Ļ:"L",Ľ:"L",Ŀ:"L",Ł:"L",ĺ:"l",ļ:"l",ľ:"l",ŀ:"l",ł:"l",Ń:"N",Ņ:"N",Ň:"N",Ŋ:"N",ń:"n",ņ:"n",ň:"n",ŋ:"n",Ō:"O",Ŏ:"O",Ő:"O",ō:"o",ŏ:"o",ő:"o",Ŕ:"R",Ŗ:"R",Ř:"R",ŕ:"r",ŗ:"r",ř:"r",Ś:"S",Ŝ:"S",Ş:"S",Š:"S",ś:"s",ŝ:"s",ş:"s",š:"s",Ţ:"T",Ť:"T",Ŧ:"T",ţ:"t",ť:"t",ŧ:"t",Ũ:"U",Ū:"U",Ŭ:"U",Ů:"U",Ű:"U",Ų:"U",ũ:"u",ū:"u",ŭ:"u",ů:"u",ű:"u",ų:"u",Ŵ:"W",ŵ:"w",Ŷ:"Y",ŷ:"y",Ÿ:"Y",Ź:"Z",Ż:"Z",Ž:"Z",ź:"z",ż:"z",ž:"z",IJ:"IJ",ij:"ij",Œ:"Oe",œ:"oe",ʼn:"'n",ſ:"s"});i.exports=m},38777:(i,s,u)=>{var m=u(10852),v=function(){try{var i=m(Object,"defineProperty");return i({},"",{}),i}catch(i){}}();i.exports=v},67114:(i,s,u)=>{var m=u(88668),v=u(82908),_=u(74757);i.exports=function equalArrays(i,s,u,j,M,$){var W=1&u,X=i.length,Y=s.length;if(X!=Y&&!(W&&Y>X))return!1;var Z=$.get(i),ee=$.get(s);if(Z&&ee)return Z==s&&ee==i;var ae=-1,ie=!0,le=2&u?new m:void 0;for($.set(i,s),$.set(s,i);++ae{var m=u(62705),v=u(11149),_=u(77813),j=u(67114),M=u(68776),$=u(21814),W=m?m.prototype:void 0,X=W?W.valueOf:void 0;i.exports=function equalByTag(i,s,u,m,W,Y,Z){switch(u){case"[object DataView]":if(i.byteLength!=s.byteLength||i.byteOffset!=s.byteOffset)return!1;i=i.buffer,s=s.buffer;case"[object ArrayBuffer]":return!(i.byteLength!=s.byteLength||!Y(new v(i),new v(s)));case"[object Boolean]":case"[object Date]":case"[object Number]":return _(+i,+s);case"[object Error]":return i.name==s.name&&i.message==s.message;case"[object RegExp]":case"[object String]":return i==s+"";case"[object Map]":var ee=M;case"[object Set]":var ae=1&m;if(ee||(ee=$),i.size!=s.size&&!ae)return!1;var ie=Z.get(i);if(ie)return ie==s;m|=2,Z.set(i,s);var le=j(ee(i),ee(s),m,W,Y,Z);return Z.delete(i),le;case"[object Symbol]":if(X)return X.call(i)==X.call(s)}return!1}},16096:(i,s,u)=>{var m=u(58234),v=Object.prototype.hasOwnProperty;i.exports=function equalObjects(i,s,u,_,j,M){var $=1&u,W=m(i),X=W.length;if(X!=m(s).length&&!$)return!1;for(var Y=X;Y--;){var Z=W[Y];if(!($?Z in s:v.call(s,Z)))return!1}var ee=M.get(i),ae=M.get(s);if(ee&&ae)return ee==s&&ae==i;var ie=!0;M.set(i,s),M.set(s,i);for(var le=$;++Y{var m=u(85564),v=u(45357),_=u(30061);i.exports=function flatRest(i){return _(v(i,void 0,m),i+"")}},31957:(i,s,u)=>{var m="object"==typeof u.g&&u.g&&u.g.Object===Object&&u.g;i.exports=m},58234:(i,s,u)=>{var m=u(68866),v=u(99551),_=u(3674);i.exports=function getAllKeys(i){return m(i,_,v)}},46904:(i,s,u)=>{var m=u(68866),v=u(51442),_=u(81704);i.exports=function getAllKeysIn(i){return m(i,_,v)}},66833:(i,s,u)=>{var m=u(89250),v=u(50308),_=m?function(i){return m.get(i)}:v;i.exports=_},97658:(i,s,u)=>{var m=u(52060),v=Object.prototype.hasOwnProperty;i.exports=function getFuncName(i){for(var s=i.name+"",u=m[s],_=v.call(m,s)?u.length:0;_--;){var j=u[_],M=j.func;if(null==M||M==i)return j.name}return s}},20893:i=>{i.exports=function getHolder(i){return i.placeholder}},45050:(i,s,u)=>{var m=u(37019);i.exports=function getMapData(i,s){var u=i.__data__;return m(s)?u["string"==typeof s?"string":"hash"]:u.map}},1499:(i,s,u)=>{var m=u(89162),v=u(3674);i.exports=function getMatchData(i){for(var s=v(i),u=s.length;u--;){var _=s[u],j=i[_];s[u]=[_,j,m(j)]}return s}},10852:(i,s,u)=>{var m=u(28458),v=u(47801);i.exports=function getNative(i,s){var u=v(i,s);return m(u)?u:void 0}},85924:(i,s,u)=>{var m=u(5569)(Object.getPrototypeOf,Object);i.exports=m},89607:(i,s,u)=>{var m=u(62705),v=Object.prototype,_=v.hasOwnProperty,j=v.toString,M=m?m.toStringTag:void 0;i.exports=function getRawTag(i){var s=_.call(i,M),u=i[M];try{i[M]=void 0;var m=!0}catch(i){}var v=j.call(i);return m&&(s?i[M]=u:delete i[M]),v}},99551:(i,s,u)=>{var m=u(34963),v=u(70479),_=Object.prototype.propertyIsEnumerable,j=Object.getOwnPropertySymbols,M=j?function(i){return null==i?[]:(i=Object(i),m(j(i),(function(s){return _.call(i,s)})))}:v;i.exports=M},51442:(i,s,u)=>{var m=u(62488),v=u(85924),_=u(99551),j=u(70479),M=Object.getOwnPropertySymbols?function(i){for(var s=[];i;)m(s,_(i)),i=v(i);return s}:j;i.exports=M},64160:(i,s,u)=>{var m=u(18552),v=u(57071),_=u(53818),j=u(58525),M=u(70577),$=u(44239),W=u(80346),X="[object Map]",Y="[object Promise]",Z="[object Set]",ee="[object WeakMap]",ae="[object DataView]",ie=W(m),le=W(v),ce=W(_),pe=W(j),de=W(M),fe=$;(m&&fe(new m(new ArrayBuffer(1)))!=ae||v&&fe(new v)!=X||_&&fe(_.resolve())!=Y||j&&fe(new j)!=Z||M&&fe(new M)!=ee)&&(fe=function(i){var s=$(i),u="[object Object]"==s?i.constructor:void 0,m=u?W(u):"";if(m)switch(m){case ie:return ae;case le:return X;case ce:return Y;case pe:return Z;case de:return ee}return s}),i.exports=fe},47801:i=>{i.exports=function getValue(i,s){return null==i?void 0:i[s]}},58775:i=>{var s=/\{\n\/\* \[wrapped with (.+)\] \*/,u=/,? & /;i.exports=function getWrapDetails(i){var m=i.match(s);return m?m[1].split(u):[]}},222:(i,s,u)=>{var m=u(71811),v=u(35694),_=u(1469),j=u(65776),M=u(41780),$=u(40327);i.exports=function hasPath(i,s,u){for(var W=-1,X=(s=m(s,i)).length,Y=!1;++W{var s=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");i.exports=function hasUnicode(i){return s.test(i)}},93157:i=>{var s=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;i.exports=function hasUnicodeWord(i){return s.test(i)}},51789:(i,s,u)=>{var m=u(94536);i.exports=function hashClear(){this.__data__=m?m(null):{},this.size=0}},80401:i=>{i.exports=function hashDelete(i){var s=this.has(i)&&delete this.__data__[i];return this.size-=s?1:0,s}},57667:(i,s,u)=>{var m=u(94536),v=Object.prototype.hasOwnProperty;i.exports=function hashGet(i){var s=this.__data__;if(m){var u=s[i];return"__lodash_hash_undefined__"===u?void 0:u}return v.call(s,i)?s[i]:void 0}},21327:(i,s,u)=>{var m=u(94536),v=Object.prototype.hasOwnProperty;i.exports=function hashHas(i){var s=this.__data__;return m?void 0!==s[i]:v.call(s,i)}},81866:(i,s,u)=>{var m=u(94536);i.exports=function hashSet(i,s){var u=this.__data__;return this.size+=this.has(i)?0:1,u[i]=m&&void 0===s?"__lodash_hash_undefined__":s,this}},43824:i=>{var s=Object.prototype.hasOwnProperty;i.exports=function initCloneArray(i){var u=i.length,m=new i.constructor(u);return u&&"string"==typeof i[0]&&s.call(i,"index")&&(m.index=i.index,m.input=i.input),m}},29148:(i,s,u)=>{var m=u(74318),v=u(57157),_=u(93147),j=u(40419),M=u(77133);i.exports=function initCloneByTag(i,s,u){var $=i.constructor;switch(s){case"[object ArrayBuffer]":return m(i);case"[object Boolean]":case"[object Date]":return new $(+i);case"[object DataView]":return v(i,u);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return M(i,u);case"[object Map]":case"[object Set]":return new $;case"[object Number]":case"[object String]":return new $(i);case"[object RegExp]":return _(i);case"[object Symbol]":return j(i)}}},38517:(i,s,u)=>{var m=u(3118),v=u(85924),_=u(25726);i.exports=function initCloneObject(i){return"function"!=typeof i.constructor||_(i)?{}:m(v(i))}},83112:i=>{var s=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/;i.exports=function insertWrapDetails(i,u){var m=u.length;if(!m)return i;var v=m-1;return u[v]=(m>1?"& ":"")+u[v],u=u.join(m>2?", ":" "),i.replace(s,"{\n/* [wrapped with "+u+"] */\n")}},37285:(i,s,u)=>{var m=u(62705),v=u(35694),_=u(1469),j=m?m.isConcatSpreadable:void 0;i.exports=function isFlattenable(i){return _(i)||v(i)||!!(j&&i&&i[j])}},65776:i=>{var s=/^(?:0|[1-9]\d*)$/;i.exports=function isIndex(i,u){var m=typeof i;return!!(u=null==u?9007199254740991:u)&&("number"==m||"symbol"!=m&&s.test(i))&&i>-1&&i%1==0&&i{var m=u(77813),v=u(98612),_=u(65776),j=u(13218);i.exports=function isIterateeCall(i,s,u){if(!j(u))return!1;var M=typeof s;return!!("number"==M?v(u)&&_(s,u.length):"string"==M&&s in u)&&m(u[s],i)}},15403:(i,s,u)=>{var m=u(1469),v=u(33448),_=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,j=/^\w*$/;i.exports=function isKey(i,s){if(m(i))return!1;var u=typeof i;return!("number"!=u&&"symbol"!=u&&"boolean"!=u&&null!=i&&!v(i))||(j.test(i)||!_.test(i)||null!=s&&i in Object(s))}},37019:i=>{i.exports=function isKeyable(i){var s=typeof i;return"string"==s||"number"==s||"symbol"==s||"boolean"==s?"__proto__"!==i:null===i}},86528:(i,s,u)=>{var m=u(96425),v=u(66833),_=u(97658),j=u(8111);i.exports=function isLaziable(i){var s=_(i),u=j[s];if("function"!=typeof u||!(s in m.prototype))return!1;if(i===u)return!0;var M=v(u);return!!M&&i===M[0]}},15346:(i,s,u)=>{var m,v=u(14429),_=(m=/[^.]+$/.exec(v&&v.keys&&v.keys.IE_PROTO||""))?"Symbol(src)_1."+m:"";i.exports=function isMasked(i){return!!_&&_ in i}},25726:i=>{var s=Object.prototype;i.exports=function isPrototype(i){var u=i&&i.constructor;return i===("function"==typeof u&&u.prototype||s)}},89162:(i,s,u)=>{var m=u(13218);i.exports=function isStrictComparable(i){return i==i&&!m(i)}},27040:i=>{i.exports=function listCacheClear(){this.__data__=[],this.size=0}},14125:(i,s,u)=>{var m=u(18470),v=Array.prototype.splice;i.exports=function listCacheDelete(i){var s=this.__data__,u=m(s,i);return!(u<0)&&(u==s.length-1?s.pop():v.call(s,u,1),--this.size,!0)}},82117:(i,s,u)=>{var m=u(18470);i.exports=function listCacheGet(i){var s=this.__data__,u=m(s,i);return u<0?void 0:s[u][1]}},67518:(i,s,u)=>{var m=u(18470);i.exports=function listCacheHas(i){return m(this.__data__,i)>-1}},54705:(i,s,u)=>{var m=u(18470);i.exports=function listCacheSet(i,s){var u=this.__data__,v=m(u,i);return v<0?(++this.size,u.push([i,s])):u[v][1]=s,this}},24785:(i,s,u)=>{var m=u(1989),v=u(38407),_=u(57071);i.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new m,map:new(_||v),string:new m}}},11285:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheDelete(i){var s=m(this,i).delete(i);return this.size-=s?1:0,s}},96e3:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheGet(i){return m(this,i).get(i)}},49916:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheHas(i){return m(this,i).has(i)}},95265:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheSet(i,s){var u=m(this,i),v=u.size;return u.set(i,s),this.size+=u.size==v?0:1,this}},68776:i=>{i.exports=function mapToArray(i){var s=-1,u=Array(i.size);return i.forEach((function(i,m){u[++s]=[m,i]})),u}},42634:i=>{i.exports=function matchesStrictComparable(i,s){return function(u){return null!=u&&(u[i]===s&&(void 0!==s||i in Object(u)))}}},24523:(i,s,u)=>{var m=u(88306);i.exports=function memoizeCapped(i){var s=m(i,(function(i){return 500===u.size&&u.clear(),i})),u=s.cache;return s}},63833:(i,s,u)=>{var m=u(52157),v=u(14054),_=u(46460),j="__lodash_placeholder__",M=128,$=Math.min;i.exports=function mergeData(i,s){var u=i[1],W=s[1],X=u|W,Y=X<131,Z=W==M&&8==u||W==M&&256==u&&i[7].length<=s[8]||384==W&&s[7].length<=s[8]&&8==u;if(!Y&&!Z)return i;1&W&&(i[2]=s[2],X|=1&u?0:4);var ee=s[3];if(ee){var ae=i[3];i[3]=ae?m(ae,ee,s[4]):ee,i[4]=ae?_(i[3],j):s[4]}return(ee=s[5])&&(ae=i[5],i[5]=ae?v(ae,ee,s[6]):ee,i[6]=ae?_(i[5],j):s[6]),(ee=s[7])&&(i[7]=ee),W&M&&(i[8]=null==i[8]?s[8]:$(i[8],s[8])),null==i[9]&&(i[9]=s[9]),i[0]=s[0],i[1]=X,i}},89250:(i,s,u)=>{var m=u(70577),v=m&&new m;i.exports=v},94536:(i,s,u)=>{var m=u(10852)(Object,"create");i.exports=m},86916:(i,s,u)=>{var m=u(5569)(Object.keys,Object);i.exports=m},33498:i=>{i.exports=function nativeKeysIn(i){var s=[];if(null!=i)for(var u in Object(i))s.push(u);return s}},31167:(i,s,u)=>{i=u.nmd(i);var m=u(31957),v=s&&!s.nodeType&&s,_=v&&i&&!i.nodeType&&i,j=_&&_.exports===v&&m.process,M=function(){try{var i=_&&_.require&&_.require("util").types;return i||j&&j.binding&&j.binding("util")}catch(i){}}();i.exports=M},2333:i=>{var s=Object.prototype.toString;i.exports=function objectToString(i){return s.call(i)}},5569:i=>{i.exports=function overArg(i,s){return function(u){return i(s(u))}}},45357:(i,s,u)=>{var m=u(96874),v=Math.max;i.exports=function overRest(i,s,u){return s=v(void 0===s?i.length-1:s,0),function(){for(var _=arguments,j=-1,M=v(_.length-s,0),$=Array(M);++j{var m=u(97786),v=u(14259);i.exports=function parent(i,s){return s.length<2?i:m(i,v(s,0,-1))}},52060:i=>{i.exports={}},90451:(i,s,u)=>{var m=u(278),v=u(65776),_=Math.min;i.exports=function reorder(i,s){for(var u=i.length,j=_(s.length,u),M=m(i);j--;){var $=s[j];i[j]=v($,u)?M[$]:void 0}return i}},46460:i=>{var s="__lodash_placeholder__";i.exports=function replaceHolders(i,u){for(var m=-1,v=i.length,_=0,j=[];++m{var m=u(31957),v="object"==typeof self&&self&&self.Object===Object&&self,_=m||v||Function("return this")();i.exports=_},36390:i=>{i.exports=function safeGet(i,s){if(("constructor"!==s||"function"!=typeof i[s])&&"__proto__"!=s)return i[s]}},90619:i=>{i.exports=function setCacheAdd(i){return this.__data__.set(i,"__lodash_hash_undefined__"),this}},72385:i=>{i.exports=function setCacheHas(i){return this.__data__.has(i)}},258:(i,s,u)=>{var m=u(28045),v=u(21275)(m);i.exports=v},21814:i=>{i.exports=function setToArray(i){var s=-1,u=Array(i.size);return i.forEach((function(i){u[++s]=i})),u}},30061:(i,s,u)=>{var m=u(56560),v=u(21275)(m);i.exports=v},69255:(i,s,u)=>{var m=u(58775),v=u(83112),_=u(30061),j=u(87241);i.exports=function setWrapToString(i,s,u){var M=s+"";return _(i,v(M,j(m(M),u)))}},21275:i=>{var s=Date.now;i.exports=function shortOut(i){var u=0,m=0;return function(){var v=s(),_=16-(v-m);if(m=v,_>0){if(++u>=800)return arguments[0]}else u=0;return i.apply(void 0,arguments)}}},37465:(i,s,u)=>{var m=u(38407);i.exports=function stackClear(){this.__data__=new m,this.size=0}},63779:i=>{i.exports=function stackDelete(i){var s=this.__data__,u=s.delete(i);return this.size=s.size,u}},67599:i=>{i.exports=function stackGet(i){return this.__data__.get(i)}},44758:i=>{i.exports=function stackHas(i){return this.__data__.has(i)}},34309:(i,s,u)=>{var m=u(38407),v=u(57071),_=u(83369);i.exports=function stackSet(i,s){var u=this.__data__;if(u instanceof m){var j=u.__data__;if(!v||j.length<199)return j.push([i,s]),this.size=++u.size,this;u=this.__data__=new _(j)}return u.set(i,s),this.size=u.size,this}},42351:i=>{i.exports=function strictIndexOf(i,s,u){for(var m=u-1,v=i.length;++m{var m=u(44286),v=u(62689),_=u(676);i.exports=function stringToArray(i){return v(i)?_(i):m(i)}},55514:(i,s,u)=>{var m=u(24523),v=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,_=/\\(\\)?/g,j=m((function(i){var s=[];return 46===i.charCodeAt(0)&&s.push(""),i.replace(v,(function(i,u,m,v){s.push(m?v.replace(_,"$1"):u||i)})),s}));i.exports=j},40327:(i,s,u)=>{var m=u(33448);i.exports=function toKey(i){if("string"==typeof i||m(i))return i;var s=i+"";return"0"==s&&1/i==-Infinity?"-0":s}},80346:i=>{var s=Function.prototype.toString;i.exports=function toSource(i){if(null!=i){try{return s.call(i)}catch(i){}try{return i+""}catch(i){}}return""}},67990:i=>{var s=/\s/;i.exports=function trimmedEndIndex(i){for(var u=i.length;u--&&s.test(i.charAt(u)););return u}},676:i=>{var s="\\ud800-\\udfff",u="["+s+"]",m="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",v="\\ud83c[\\udffb-\\udfff]",_="[^"+s+"]",j="(?:\\ud83c[\\udde6-\\uddff]){2}",M="[\\ud800-\\udbff][\\udc00-\\udfff]",$="(?:"+m+"|"+v+")"+"?",W="[\\ufe0e\\ufe0f]?",X=W+$+("(?:\\u200d(?:"+[_,j,M].join("|")+")"+W+$+")*"),Y="(?:"+[_+m+"?",m,j,M,u].join("|")+")",Z=RegExp(v+"(?="+v+")|"+Y+X,"g");i.exports=function unicodeToArray(i){return i.match(Z)||[]}},2757:i=>{var s="\\ud800-\\udfff",u="\\u2700-\\u27bf",m="a-z\\xdf-\\xf6\\xf8-\\xff",v="A-Z\\xc0-\\xd6\\xd8-\\xde",_="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",j="["+_+"]",M="\\d+",$="["+u+"]",W="["+m+"]",X="[^"+s+_+M+u+m+v+"]",Y="(?:\\ud83c[\\udde6-\\uddff]){2}",Z="[\\ud800-\\udbff][\\udc00-\\udfff]",ee="["+v+"]",ae="(?:"+W+"|"+X+")",ie="(?:"+ee+"|"+X+")",le="(?:['’](?:d|ll|m|re|s|t|ve))?",ce="(?:['’](?:D|LL|M|RE|S|T|VE))?",pe="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",de="[\\ufe0e\\ufe0f]?",fe=de+pe+("(?:\\u200d(?:"+["[^"+s+"]",Y,Z].join("|")+")"+de+pe+")*"),ye="(?:"+[$,Y,Z].join("|")+")"+fe,be=RegExp([ee+"?"+W+"+"+le+"(?="+[j,ee,"$"].join("|")+")",ie+"+"+ce+"(?="+[j,ee+ae,"$"].join("|")+")",ee+"?"+ae+"+"+le,ee+"+"+ce,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",M,ye].join("|"),"g");i.exports=function unicodeWords(i){return i.match(be)||[]}},87241:(i,s,u)=>{var m=u(77412),v=u(47443),_=[["ary",128],["bind",1],["bindKey",2],["curry",8],["curryRight",16],["flip",512],["partial",32],["partialRight",64],["rearg",256]];i.exports=function updateWrapDetails(i,s){return m(_,(function(u){var m="_."+u[0];s&u[1]&&!v(i,m)&&i.push(m)})),i.sort()}},21913:(i,s,u)=>{var m=u(96425),v=u(7548),_=u(278);i.exports=function wrapperClone(i){if(i instanceof m)return i.clone();var s=new v(i.__wrapped__,i.__chain__);return s.__actions__=_(i.__actions__),s.__index__=i.__index__,s.__values__=i.__values__,s}},39514:(i,s,u)=>{var m=u(97727);i.exports=function ary(i,s,u){return s=u?void 0:s,s=i&&null==s?i.length:s,m(i,128,void 0,void 0,void 0,void 0,s)}},68929:(i,s,u)=>{var m=u(48403),v=u(35393)((function(i,s,u){return s=s.toLowerCase(),i+(u?m(s):s)}));i.exports=v},48403:(i,s,u)=>{var m=u(79833),v=u(11700);i.exports=function capitalize(i){return v(m(i).toLowerCase())}},66678:(i,s,u)=>{var m=u(85990);i.exports=function clone(i){return m(i,4)}},75703:i=>{i.exports=function constant(i){return function(){return i}}},40087:(i,s,u)=>{var m=u(97727);function curry(i,s,u){var v=m(i,8,void 0,void 0,void 0,void 0,void 0,s=u?void 0:s);return v.placeholder=curry.placeholder,v}curry.placeholder={},i.exports=curry},23279:(i,s,u)=>{var m=u(13218),v=u(7771),_=u(14841),j=Math.max,M=Math.min;i.exports=function debounce(i,s,u){var $,W,X,Y,Z,ee,ae=0,ie=!1,le=!1,ce=!0;if("function"!=typeof i)throw new TypeError("Expected a function");function invokeFunc(s){var u=$,m=W;return $=W=void 0,ae=s,Y=i.apply(m,u)}function shouldInvoke(i){var u=i-ee;return void 0===ee||u>=s||u<0||le&&i-ae>=X}function timerExpired(){var i=v();if(shouldInvoke(i))return trailingEdge(i);Z=setTimeout(timerExpired,function remainingWait(i){var u=s-(i-ee);return le?M(u,X-(i-ae)):u}(i))}function trailingEdge(i){return Z=void 0,ce&&$?invokeFunc(i):($=W=void 0,Y)}function debounced(){var i=v(),u=shouldInvoke(i);if($=arguments,W=this,ee=i,u){if(void 0===Z)return function leadingEdge(i){return ae=i,Z=setTimeout(timerExpired,s),ie?invokeFunc(i):Y}(ee);if(le)return clearTimeout(Z),Z=setTimeout(timerExpired,s),invokeFunc(ee)}return void 0===Z&&(Z=setTimeout(timerExpired,s)),Y}return s=_(s)||0,m(u)&&(ie=!!u.leading,X=(le="maxWait"in u)?j(_(u.maxWait)||0,s):X,ce="trailing"in u?!!u.trailing:ce),debounced.cancel=function cancel(){void 0!==Z&&clearTimeout(Z),ae=0,$=ee=W=Z=void 0},debounced.flush=function flush(){return void 0===Z?Y:trailingEdge(v())},debounced}},53816:(i,s,u)=>{var m=u(69389),v=u(79833),_=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,j=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");i.exports=function deburr(i){return(i=v(i))&&i.replace(_,m).replace(j,"")}},77813:i=>{i.exports=function eq(i,s){return i===s||i!=i&&s!=s}},13311:(i,s,u)=>{var m=u(67740)(u(30998));i.exports=m},30998:(i,s,u)=>{var m=u(41848),v=u(67206),_=u(40554),j=Math.max;i.exports=function findIndex(i,s,u){var M=null==i?0:i.length;if(!M)return-1;var $=null==u?0:_(u);return $<0&&($=j(M+$,0)),m(i,v(s,3),$)}},85564:(i,s,u)=>{var m=u(21078);i.exports=function flatten(i){return(null==i?0:i.length)?m(i,1):[]}},84599:(i,s,u)=>{var m=u(68836),v=u(69306),_=Array.prototype.push;function baseAry(i,s){return 2==s?function(s,u){return i(s,u)}:function(s){return i(s)}}function cloneArray(i){for(var s=i?i.length:0,u=Array(s);s--;)u[s]=i[s];return u}function wrapImmutable(i,s){return function(){var u=arguments.length;if(u){for(var m=Array(u);u--;)m[u]=arguments[u];var v=m[0]=s.apply(void 0,m);return i.apply(void 0,m),v}}}i.exports=function baseConvert(i,s,u,j){var M="function"==typeof s,$=s===Object(s);if($&&(j=u,u=s,s=void 0),null==u)throw new TypeError;j||(j={});var W={cap:!("cap"in j)||j.cap,curry:!("curry"in j)||j.curry,fixed:!("fixed"in j)||j.fixed,immutable:!("immutable"in j)||j.immutable,rearg:!("rearg"in j)||j.rearg},X=M?u:v,Y="curry"in j&&j.curry,Z="fixed"in j&&j.fixed,ee="rearg"in j&&j.rearg,ae=M?u.runInContext():void 0,ie=M?u:{ary:i.ary,assign:i.assign,clone:i.clone,curry:i.curry,forEach:i.forEach,isArray:i.isArray,isError:i.isError,isFunction:i.isFunction,isWeakMap:i.isWeakMap,iteratee:i.iteratee,keys:i.keys,rearg:i.rearg,toInteger:i.toInteger,toPath:i.toPath},le=ie.ary,ce=ie.assign,pe=ie.clone,de=ie.curry,fe=ie.forEach,ye=ie.isArray,be=ie.isError,_e=ie.isFunction,we=ie.isWeakMap,Se=ie.keys,xe=ie.rearg,Pe=ie.toInteger,Ie=ie.toPath,Te=Se(m.aryMethod),Re={castArray:function(i){return function(){var s=arguments[0];return ye(s)?i(cloneArray(s)):i.apply(void 0,arguments)}},iteratee:function(i){return function(){var s=arguments[1],u=i(arguments[0],s),m=u.length;return W.cap&&"number"==typeof s?(s=s>2?s-2:1,m&&m<=s?u:baseAry(u,s)):u}},mixin:function(i){return function(s){var u=this;if(!_e(u))return i(u,Object(s));var m=[];return fe(Se(s),(function(i){_e(s[i])&&m.push([i,u.prototype[i]])})),i(u,Object(s)),fe(m,(function(i){var s=i[1];_e(s)?u.prototype[i[0]]=s:delete u.prototype[i[0]]})),u}},nthArg:function(i){return function(s){var u=s<0?1:Pe(s)+1;return de(i(s),u)}},rearg:function(i){return function(s,u){var m=u?u.length:0;return de(i(s,u),m)}},runInContext:function(s){return function(u){return baseConvert(i,s(u),j)}}};function castCap(i,s){if(W.cap){var u=m.iterateeRearg[i];if(u)return function iterateeRearg(i,s){return overArg(i,(function(i){var u=s.length;return function baseArity(i,s){return 2==s?function(s,u){return i.apply(void 0,arguments)}:function(s){return i.apply(void 0,arguments)}}(xe(baseAry(i,u),s),u)}))}(s,u);var v=!M&&m.iterateeAry[i];if(v)return function iterateeAry(i,s){return overArg(i,(function(i){return"function"==typeof i?baseAry(i,s):i}))}(s,v)}return s}function castFixed(i,s,u){if(W.fixed&&(Z||!m.skipFixed[i])){var v=m.methodSpread[i],j=v&&v.start;return void 0===j?le(s,u):function flatSpread(i,s){return function(){for(var u=arguments.length,m=u-1,v=Array(u);u--;)v[u]=arguments[u];var j=v[s],M=v.slice(0,s);return j&&_.apply(M,j),s!=m&&_.apply(M,v.slice(s+1)),i.apply(this,M)}}(s,j)}return s}function castRearg(i,s,u){return W.rearg&&u>1&&(ee||!m.skipRearg[i])?xe(s,m.methodRearg[i]||m.aryRearg[u]):s}function cloneByPath(i,s){for(var u=-1,m=(s=Ie(s)).length,v=m-1,_=pe(Object(i)),j=_;null!=j&&++u1?de(s,u):s}(0,v=castCap(_,v),i),!1}})),!v})),v||(v=j),v==s&&(v=Y?de(v,1):function(){return s.apply(this,arguments)}),v.convert=createConverter(_,s),v.placeholder=s.placeholder=u,v}if(!$)return wrap(s,u,X);var qe=u,ze=[];return fe(Te,(function(i){fe(m.aryMethod[i],(function(i){var s=qe[m.remap[i]||i];s&&ze.push([i,wrap(i,s,qe)])}))})),fe(Se(qe),(function(i){var s=qe[i];if("function"==typeof s){for(var u=ze.length;u--;)if(ze[u][0]==i)return;s.convert=createConverter(i,s),ze.push([i,s])}})),fe(ze,(function(i){qe[i[0]]=i[1]})),qe.convert=function convertLib(i){return qe.runInContext.convert(i)(void 0)},qe.placeholder=qe,fe(Se(qe),(function(i){fe(m.realToAlias[i]||[],(function(s){qe[s]=qe[i]}))})),qe}},68836:(i,s)=>{s.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},s.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},s.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},s.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},s.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},s.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},s.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},s.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},s.realToAlias=function(){var i=Object.prototype.hasOwnProperty,u=s.aliasToReal,m={};for(var v in u){var _=u[v];i.call(m,_)?m[_].push(v):m[_]=[v]}return m}(),s.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},s.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},s.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},4269:(i,s,u)=>{i.exports={ary:u(39514),assign:u(44037),clone:u(66678),curry:u(40087),forEach:u(77412),isArray:u(1469),isError:u(64647),isFunction:u(23560),isWeakMap:u(81018),iteratee:u(72594),keys:u(280),rearg:u(4963),toInteger:u(40554),toPath:u(30084)}},72700:(i,s,u)=>{i.exports=u(28252)},92822:(i,s,u)=>{var m=u(84599),v=u(4269);i.exports=function convert(i,s,u){return m(v,i,s,u)}},69306:i=>{i.exports={}},28252:(i,s,u)=>{var m=u(92822)("set",u(36968));m.placeholder=u(69306),i.exports=m},27361:(i,s,u)=>{var m=u(97786);i.exports=function get(i,s,u){var v=null==i?void 0:m(i,s);return void 0===v?u:v}},79095:(i,s,u)=>{var m=u(13),v=u(222);i.exports=function hasIn(i,s){return null!=i&&v(i,s,m)}},6557:i=>{i.exports=function identity(i){return i}},35694:(i,s,u)=>{var m=u(9454),v=u(37005),_=Object.prototype,j=_.hasOwnProperty,M=_.propertyIsEnumerable,$=m(function(){return arguments}())?m:function(i){return v(i)&&j.call(i,"callee")&&!M.call(i,"callee")};i.exports=$},1469:i=>{var s=Array.isArray;i.exports=s},98612:(i,s,u)=>{var m=u(23560),v=u(41780);i.exports=function isArrayLike(i){return null!=i&&v(i.length)&&!m(i)}},29246:(i,s,u)=>{var m=u(98612),v=u(37005);i.exports=function isArrayLikeObject(i){return v(i)&&m(i)}},51584:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function isBoolean(i){return!0===i||!1===i||v(i)&&"[object Boolean]"==m(i)}},44144:(i,s,u)=>{i=u.nmd(i);var m=u(55639),v=u(95062),_=s&&!s.nodeType&&s,j=_&&i&&!i.nodeType&&i,M=j&&j.exports===_?m.Buffer:void 0,$=(M?M.isBuffer:void 0)||v;i.exports=$},41609:(i,s,u)=>{var m=u(280),v=u(64160),_=u(35694),j=u(1469),M=u(98612),$=u(44144),W=u(25726),X=u(36719),Y=Object.prototype.hasOwnProperty;i.exports=function isEmpty(i){if(null==i)return!0;if(M(i)&&(j(i)||"string"==typeof i||"function"==typeof i.splice||$(i)||X(i)||_(i)))return!i.length;var s=v(i);if("[object Map]"==s||"[object Set]"==s)return!i.size;if(W(i))return!m(i).length;for(var u in i)if(Y.call(i,u))return!1;return!0}},18446:(i,s,u)=>{var m=u(90939);i.exports=function isEqual(i,s){return m(i,s)}},64647:(i,s,u)=>{var m=u(44239),v=u(37005),_=u(68630);i.exports=function isError(i){if(!v(i))return!1;var s=m(i);return"[object Error]"==s||"[object DOMException]"==s||"string"==typeof i.message&&"string"==typeof i.name&&!_(i)}},23560:(i,s,u)=>{var m=u(44239),v=u(13218);i.exports=function isFunction(i){if(!v(i))return!1;var s=m(i);return"[object Function]"==s||"[object GeneratorFunction]"==s||"[object AsyncFunction]"==s||"[object Proxy]"==s}},41780:i=>{i.exports=function isLength(i){return"number"==typeof i&&i>-1&&i%1==0&&i<=9007199254740991}},56688:(i,s,u)=>{var m=u(25588),v=u(7518),_=u(31167),j=_&&_.isMap,M=j?v(j):m;i.exports=M},45220:i=>{i.exports=function isNull(i){return null===i}},81763:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function isNumber(i){return"number"==typeof i||v(i)&&"[object Number]"==m(i)}},13218:i=>{i.exports=function isObject(i){var s=typeof i;return null!=i&&("object"==s||"function"==s)}},37005:i=>{i.exports=function isObjectLike(i){return null!=i&&"object"==typeof i}},68630:(i,s,u)=>{var m=u(44239),v=u(85924),_=u(37005),j=Function.prototype,M=Object.prototype,$=j.toString,W=M.hasOwnProperty,X=$.call(Object);i.exports=function isPlainObject(i){if(!_(i)||"[object Object]"!=m(i))return!1;var s=v(i);if(null===s)return!0;var u=W.call(s,"constructor")&&s.constructor;return"function"==typeof u&&u instanceof u&&$.call(u)==X}},72928:(i,s,u)=>{var m=u(29221),v=u(7518),_=u(31167),j=_&&_.isSet,M=j?v(j):m;i.exports=M},47037:(i,s,u)=>{var m=u(44239),v=u(1469),_=u(37005);i.exports=function isString(i){return"string"==typeof i||!v(i)&&_(i)&&"[object String]"==m(i)}},33448:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function isSymbol(i){return"symbol"==typeof i||v(i)&&"[object Symbol]"==m(i)}},36719:(i,s,u)=>{var m=u(38749),v=u(7518),_=u(31167),j=_&&_.isTypedArray,M=j?v(j):m;i.exports=M},81018:(i,s,u)=>{var m=u(64160),v=u(37005);i.exports=function isWeakMap(i){return v(i)&&"[object WeakMap]"==m(i)}},72594:(i,s,u)=>{var m=u(85990),v=u(67206);i.exports=function iteratee(i){return v("function"==typeof i?i:m(i,1))}},3674:(i,s,u)=>{var m=u(14636),v=u(280),_=u(98612);i.exports=function keys(i){return _(i)?m(i):v(i)}},81704:(i,s,u)=>{var m=u(14636),v=u(10313),_=u(98612);i.exports=function keysIn(i){return _(i)?m(i,!0):v(i)}},10928:i=>{i.exports=function last(i){var s=null==i?0:i.length;return s?i[s-1]:void 0}},88306:(i,s,u)=>{var m=u(83369);function memoize(i,s){if("function"!=typeof i||null!=s&&"function"!=typeof s)throw new TypeError("Expected a function");var memoized=function(){var u=arguments,m=s?s.apply(this,u):u[0],v=memoized.cache;if(v.has(m))return v.get(m);var _=i.apply(this,u);return memoized.cache=v.set(m,_)||v,_};return memoized.cache=new(memoize.Cache||m),memoized}memoize.Cache=m,i.exports=memoize},82492:(i,s,u)=>{var m=u(42980),v=u(21463)((function(i,s,u){m(i,s,u)}));i.exports=v},94885:i=>{i.exports=function negate(i){if("function"!=typeof i)throw new TypeError("Expected a function");return function(){var s=arguments;switch(s.length){case 0:return!i.call(this);case 1:return!i.call(this,s[0]);case 2:return!i.call(this,s[0],s[1]);case 3:return!i.call(this,s[0],s[1],s[2])}return!i.apply(this,s)}}},50308:i=>{i.exports=function noop(){}},7771:(i,s,u)=>{var m=u(55639);i.exports=function(){return m.Date.now()}},57557:(i,s,u)=>{var m=u(29932),v=u(85990),_=u(57406),j=u(71811),M=u(98363),$=u(60696),W=u(99021),X=u(46904),Y=W((function(i,s){var u={};if(null==i)return u;var W=!1;s=m(s,(function(s){return s=j(s,i),W||(W=s.length>1),s})),M(i,X(i),u),W&&(u=v(u,7,$));for(var Y=s.length;Y--;)_(u,s[Y]);return u}));i.exports=Y},39601:(i,s,u)=>{var m=u(40371),v=u(79152),_=u(15403),j=u(40327);i.exports=function property(i){return _(i)?m(j(i)):v(i)}},4963:(i,s,u)=>{var m=u(97727),v=u(99021),_=v((function(i,s){return m(i,256,void 0,void 0,void 0,s)}));i.exports=_},54061:(i,s,u)=>{var m=u(62663),v=u(89881),_=u(67206),j=u(10107),M=u(1469);i.exports=function reduce(i,s,u){var $=M(i)?m:j,W=arguments.length<3;return $(i,_(s,4),u,W,v)}},36968:(i,s,u)=>{var m=u(10611);i.exports=function set(i,s,u){return null==i?i:m(i,s,u)}},59704:(i,s,u)=>{var m=u(82908),v=u(67206),_=u(5076),j=u(1469),M=u(16612);i.exports=function some(i,s,u){var $=j(i)?m:_;return u&&M(i,s,u)&&(s=void 0),$(i,v(s,3))}},70479:i=>{i.exports=function stubArray(){return[]}},95062:i=>{i.exports=function stubFalse(){return!1}},18601:(i,s,u)=>{var m=u(14841),v=1/0;i.exports=function toFinite(i){return i?(i=m(i))===v||i===-1/0?17976931348623157e292*(i<0?-1:1):i==i?i:0:0===i?i:0}},40554:(i,s,u)=>{var m=u(18601);i.exports=function toInteger(i){var s=m(i),u=s%1;return s==s?u?s-u:s:0}},7334:(i,s,u)=>{var m=u(79833);i.exports=function toLower(i){return m(i).toLowerCase()}},14841:(i,s,u)=>{var m=u(27561),v=u(13218),_=u(33448),j=/^[-+]0x[0-9a-f]+$/i,M=/^0b[01]+$/i,$=/^0o[0-7]+$/i,W=parseInt;i.exports=function toNumber(i){if("number"==typeof i)return i;if(_(i))return NaN;if(v(i)){var s="function"==typeof i.valueOf?i.valueOf():i;i=v(s)?s+"":s}if("string"!=typeof i)return 0===i?i:+i;i=m(i);var u=M.test(i);return u||$.test(i)?W(i.slice(2),u?2:8):j.test(i)?NaN:+i}},30084:(i,s,u)=>{var m=u(29932),v=u(278),_=u(1469),j=u(33448),M=u(55514),$=u(40327),W=u(79833);i.exports=function toPath(i){return _(i)?m(i,$):j(i)?[i]:v(M(W(i)))}},59881:(i,s,u)=>{var m=u(98363),v=u(81704);i.exports=function toPlainObject(i){return m(i,v(i))}},79833:(i,s,u)=>{var m=u(80531);i.exports=function toString(i){return null==i?"":m(i)}},11700:(i,s,u)=>{var m=u(98805)("toUpperCase");i.exports=m},58748:(i,s,u)=>{var m=u(49029),v=u(93157),_=u(79833),j=u(2757);i.exports=function words(i,s,u){return i=_(i),void 0===(s=u?void 0:s)?v(i)?j(i):m(i):i.match(s)||[]}},8111:(i,s,u)=>{var m=u(96425),v=u(7548),_=u(9435),j=u(1469),M=u(37005),$=u(21913),W=Object.prototype.hasOwnProperty;function lodash(i){if(M(i)&&!j(i)&&!(i instanceof m)){if(i instanceof v)return i;if(W.call(i,"__wrapped__"))return $(i)}return new v(i)}lodash.prototype=_.prototype,lodash.prototype.constructor=lodash,i.exports=lodash},7287:(i,s,u)=>{var m=u(34865),v=u(1757);i.exports=function zipObject(i,s){return v(i||[],s||[],m)}},96470:(i,s,u)=>{"use strict";var m=u(47802),v=u(21102);s.highlight=highlight,s.highlightAuto=function highlightAuto(i,s){var u,j,M,$,W=s||{},X=W.subset||m.listLanguages(),Y=W.prefix,Z=X.length,ee=-1;null==Y&&(Y=_);if("string"!=typeof i)throw v("Expected `string` for value, got `%s`",i);j={relevance:0,language:null,value:[]},u={relevance:0,language:null,value:[]};for(;++eej.relevance&&(j=M),M.relevance>u.relevance&&(j=u,u=M));j.language&&(u.secondBest=j);return u},s.registerLanguage=function registerLanguage(i,s){m.registerLanguage(i,s)},s.listLanguages=function listLanguages(){return m.listLanguages()},s.registerAlias=function registerAlias(i,s){var u,v=i;s&&((v={})[i]=s);for(u in v)m.registerAliases(v[u],{languageName:u})},Emitter.prototype.addText=function text(i){var s,u,m=this.stack;if(""===i)return;s=m[m.length-1],(u=s.children[s.children.length-1])&&"text"===u.type?u.value+=i:s.children.push({type:"text",value:i})},Emitter.prototype.addKeyword=function addKeyword(i,s){this.openNode(s),this.addText(i),this.closeNode()},Emitter.prototype.addSublanguage=function addSublanguage(i,s){var u=this.stack,m=u[u.length-1],v=i.rootNode.children,_=s?{type:"element",tagName:"span",properties:{className:[s]},children:v}:v;m.children=m.children.concat(_)},Emitter.prototype.openNode=function open(i){var s=this.stack,u=this.options.classPrefix+i,m=s[s.length-1],v={type:"element",tagName:"span",properties:{className:[u]},children:[]};m.children.push(v),s.push(v)},Emitter.prototype.closeNode=function close(){this.stack.pop()},Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=function toHtmlNoop(){return""};var _="hljs-";function highlight(i,s,u){var j,M=m.configure({}),$=(u||{}).prefix;if("string"!=typeof i)throw v("Expected `string` for name, got `%s`",i);if(!m.getLanguage(i))throw v("Unknown language: `%s` is not registered",i);if("string"!=typeof s)throw v("Expected `string` for value, got `%s`",s);if(null==$&&($=_),m.configure({__emitter:Emitter,classPrefix:$}),j=m.highlight(s,{language:i,ignoreIllegals:!0}),m.configure(M||{}),j.errorRaised)throw j.errorRaised;return{relevance:j.relevance,language:j.language,value:j.emitter.rootNode.children}}function Emitter(i){this.options=i,this.rootNode={children:[]},this.stack=[this.rootNode]}function noop(){}},42566:(i,s,u)=>{const m=u(94885);function coerceElementMatchingCallback(i){return"string"==typeof i?s=>s.element===i:i.constructor&&i.extend?s=>s instanceof i:i}class ArraySlice{constructor(i){this.elements=i||[]}toValue(){return this.elements.map((i=>i.toValue()))}map(i,s){return this.elements.map(i,s)}flatMap(i,s){return this.map(i,s).reduce(((i,s)=>i.concat(s)),[])}compactMap(i,s){const u=[];return this.forEach((m=>{const v=i.bind(s)(m);v&&u.push(v)})),u}filter(i,s){return i=coerceElementMatchingCallback(i),new ArraySlice(this.elements.filter(i,s))}reject(i,s){return i=coerceElementMatchingCallback(i),new ArraySlice(this.elements.filter(m(i),s))}find(i,s){return i=coerceElementMatchingCallback(i),this.elements.find(i,s)}forEach(i,s){this.elements.forEach(i,s)}reduce(i,s){return this.elements.reduce(i,s)}includes(i){return this.elements.some((s=>s.equals(i)))}shift(){return this.elements.shift()}unshift(i){this.elements.unshift(this.refract(i))}push(i){return this.elements.push(this.refract(i)),this}add(i){this.push(i)}get(i){return this.elements[i]}getValue(i){const s=this.elements[i];if(s)return s.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}"undefined"!=typeof Symbol&&(ArraySlice.prototype[Symbol.iterator]=function symbol(){return this.elements[Symbol.iterator]()}),i.exports=ArraySlice},17645:i=>{class KeyValuePair{constructor(i,s){this.key=i,this.value=s}clone(){const i=new KeyValuePair;return this.key&&(i.key=this.key.clone()),this.value&&(i.value=this.value.clone()),i}}i.exports=KeyValuePair},78520:(i,s,u)=>{const m=u(45220),v=u(47037),_=u(81763),j=u(51584),M=u(13218),$=u(28219),W=u(99829);class Namespace{constructor(i){this.elementMap={},this.elementDetection=[],this.Element=W.Element,this.KeyValuePair=W.KeyValuePair,i&&i.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(i){return i.namespace&&i.namespace({base:this}),i.load&&i.load({base:this}),this}useDefault(){return this.register("null",W.NullElement).register("string",W.StringElement).register("number",W.NumberElement).register("boolean",W.BooleanElement).register("array",W.ArrayElement).register("object",W.ObjectElement).register("member",W.MemberElement).register("ref",W.RefElement).register("link",W.LinkElement),this.detect(m,W.NullElement,!1).detect(v,W.StringElement,!1).detect(_,W.NumberElement,!1).detect(j,W.BooleanElement,!1).detect(Array.isArray,W.ArrayElement,!1).detect(M,W.ObjectElement,!1),this}register(i,s){return this._elements=void 0,this.elementMap[i]=s,this}unregister(i){return this._elements=void 0,delete this.elementMap[i],this}detect(i,s,u){return void 0===u||u?this.elementDetection.unshift([i,s]):this.elementDetection.push([i,s]),this}toElement(i){if(i instanceof this.Element)return i;let s;for(let u=0;u{const s=i[0].toUpperCase()+i.substr(1);this._elements[s]=this.elementMap[i]}))),this._elements}get serialiser(){return new $(this)}}$.prototype.Namespace=Namespace,i.exports=Namespace},87526:(i,s,u)=>{const m=u(94885),v=u(42566);class ObjectSlice extends v{map(i,s){return this.elements.map((u=>i.bind(s)(u.value,u.key,u)))}filter(i,s){return new ObjectSlice(this.elements.filter((u=>i.bind(s)(u.value,u.key,u))))}reject(i,s){return this.filter(m(i.bind(s)))}forEach(i,s){return this.elements.forEach(((u,m)=>{i.bind(s)(u.value,u.key,u,m)}))}keys(){return this.map(((i,s)=>s.toValue()))}values(){return this.map((i=>i.toValue()))}}i.exports=ObjectSlice},99829:(i,s,u)=>{const m=u(3079),v=u(96295),_=u(16036),j=u(91090),M=u(18866),$=u(35804),W=u(5946),X=u(76735),Y=u(59964),Z=u(38588),ee=u(42566),ae=u(87526),ie=u(17645);function refract(i){if(i instanceof m)return i;if("string"==typeof i)return new _(i);if("number"==typeof i)return new j(i);if("boolean"==typeof i)return new M(i);if(null===i)return new v;if(Array.isArray(i))return new $(i.map(refract));if("object"==typeof i){return new X(i)}return i}m.prototype.ObjectElement=X,m.prototype.RefElement=Z,m.prototype.MemberElement=W,m.prototype.refract=refract,ee.prototype.refract=refract,i.exports={Element:m,NullElement:v,StringElement:_,NumberElement:j,BooleanElement:M,ArrayElement:$,MemberElement:W,ObjectElement:X,LinkElement:Y,RefElement:Z,refract,ArraySlice:ee,ObjectSlice:ae,KeyValuePair:ie}},59964:(i,s,u)=>{const m=u(3079);i.exports=class LinkElement extends m{constructor(i,s,u){super(i||[],s,u),this.element="link"}get relation(){return this.attributes.get("relation")}set relation(i){this.attributes.set("relation",i)}get href(){return this.attributes.get("href")}set href(i){this.attributes.set("href",i)}}},38588:(i,s,u)=>{const m=u(3079);i.exports=class RefElement extends m{constructor(i,s,u){super(i||[],s,u),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(i){this.attributes.set("path",i)}}},43500:(i,s,u)=>{const m=u(78520),v=u(99829);s.lS=m,u(17645),s.O4=v.ArraySlice,v.ObjectSlice,s.W_=v.Element,s.RP=v.StringElement,s.VL=v.NumberElement,s.hh=v.BooleanElement,s.zr=v.NullElement,s.ON=v.ArrayElement,s.Sb=v.ObjectElement,s.c6=v.MemberElement,s.tK=v.RefElement,s.EA=v.LinkElement,s.Qc=v.refract,u(28219),u(3414)},35804:(i,s,u)=>{const m=u(94885),v=u(3079),_=u(42566);class ArrayElement extends v{constructor(i,s,u){super(i||[],s,u),this.element="array"}primitive(){return"array"}get(i){return this.content[i]}getValue(i){const s=this.get(i);if(s)return s.toValue()}getIndex(i){return this.content[i]}set(i,s){return this.content[i]=this.refract(s),this}remove(i){const s=this.content.splice(i,1);return s.length?s[0]:null}map(i,s){return this.content.map(i,s)}flatMap(i,s){return this.map(i,s).reduce(((i,s)=>i.concat(s)),[])}compactMap(i,s){const u=[];return this.forEach((m=>{const v=i.bind(s)(m);v&&u.push(v)})),u}filter(i,s){return new _(this.content.filter(i,s))}reject(i,s){return this.filter(m(i),s)}reduce(i,s){let u,m;void 0!==s?(u=0,m=this.refract(s)):(u=1,m="object"===this.primitive()?this.first.value:this.first);for(let s=u;s{i.bind(s)(u,this.refract(m))}))}shift(){return this.content.shift()}unshift(i){this.content.unshift(this.refract(i))}push(i){return this.content.push(this.refract(i)),this}add(i){this.push(i)}findElements(i,s){const u=s||{},m=!!u.recursive,v=void 0===u.results?[]:u.results;return this.forEach(((s,u,_)=>{m&&void 0!==s.findElements&&s.findElements(i,{results:v,recursive:m}),i(s,u,_)&&v.push(s)})),v}find(i){return new _(this.findElements(i,{recursive:!0}))}findByElement(i){return this.find((s=>s.element===i))}findByClass(i){return this.find((s=>s.classes.includes(i)))}getById(i){return this.find((s=>s.id.toValue()===i)).first}includes(i){return this.content.some((s=>s.equals(i)))}contains(i){return this.includes(i)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(i){return new this.constructor(this.content.concat(i.content))}"fantasy-land/concat"(i){return this.concat(i)}"fantasy-land/map"(i){return new this.constructor(this.map(i))}"fantasy-land/chain"(i){return this.map((s=>i(s)),this).reduce(((i,s)=>i.concat(s)),this.empty())}"fantasy-land/filter"(i){return new this.constructor(this.content.filter(i))}"fantasy-land/reduce"(i,s){return this.content.reduce(i,s)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),i.exports=ArrayElement},18866:(i,s,u)=>{const m=u(3079);i.exports=class BooleanElement extends m{constructor(i,s,u){super(i,s,u),this.element="boolean"}primitive(){return"boolean"}}},3079:(i,s,u)=>{const m=u(18446),v=u(17645),_=u(42566);class Element{constructor(i,s,u){s&&(this.meta=s),u&&(this.attributes=u),this.content=i}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((i=>{i.parent=this,i.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const i=new this.constructor;return i.element=this.element,this.meta.length&&(i._meta=this.meta.clone()),this.attributes.length&&(i._attributes=this.attributes.clone()),this.content?this.content.clone?i.content=this.content.clone():Array.isArray(this.content)?i.content=this.content.map((i=>i.clone())):i.content=this.content:i.content=this.content,i}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof v?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((i=>i.toValue()),this):this.content}toRef(i){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const s=new this.RefElement(this.id.toValue());return i&&(s.path=i),s}findRecursive(...i){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const s=i.pop();let u=new _;const append=(i,s)=>(i.push(s),i),checkElement=(i,u)=>{u.element===s&&i.push(u);const m=u.findRecursive(s);return m&&m.reduce(append,i),u.content instanceof v&&(u.content.key&&checkElement(i,u.content.key),u.content.value&&checkElement(i,u.content.value)),i};return this.content&&(this.content.element&&checkElement(u,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,u)),i.isEmpty||(u=u.filter((s=>{let u=s.parents.map((i=>i.element));for(const s in i){const m=i[s],v=u.indexOf(m);if(-1===v)return!1;u=u.splice(0,v)}return!0}))),u}set(i){return this.content=i,this}equals(i){return m(this.toValue(),i)}getMetaProperty(i,s){if(!this.meta.hasKey(i)){if(this.isFrozen){const i=this.refract(s);return i.freeze(),i}this.meta.set(i,s)}return this.meta.get(i)}setMetaProperty(i,s){this.meta.set(i,s)}get element(){return this._storedElement||"element"}set element(i){this._storedElement=i}get content(){return this._content}set content(i){if(i instanceof Element)this._content=i;else if(i instanceof _)this.content=i.elements;else if("string"==typeof i||"number"==typeof i||"boolean"==typeof i||"null"===i||null==i)this._content=i;else if(i instanceof v)this._content=i;else if(Array.isArray(i))this._content=i.map(this.refract);else{if("object"!=typeof i)throw new Error("Cannot set content to given value");this._content=Object.keys(i).map((s=>new this.MemberElement(s,i[s])))}}get meta(){if(!this._meta){if(this.isFrozen){const i=new this.ObjectElement;return i.freeze(),i}this._meta=new this.ObjectElement}return this._meta}set meta(i){i instanceof this.ObjectElement?this._meta=i:this.meta.set(i||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const i=new this.ObjectElement;return i.freeze(),i}this._attributes=new this.ObjectElement}return this._attributes}set attributes(i){i instanceof this.ObjectElement?this._attributes=i:this.attributes.set(i||{})}get id(){return this.getMetaProperty("id","")}set id(i){this.setMetaProperty("id",i)}get classes(){return this.getMetaProperty("classes",[])}set classes(i){this.setMetaProperty("classes",i)}get title(){return this.getMetaProperty("title","")}set title(i){this.setMetaProperty("title",i)}get description(){return this.getMetaProperty("description","")}set description(i){this.setMetaProperty("description",i)}get links(){return this.getMetaProperty("links",[])}set links(i){this.setMetaProperty("links",i)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:i}=this;const s=new _;for(;i;)s.push(i),i=i.parent;return s}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof v){const i=new _([this.content.key]);return this.content.value&&i.push(this.content.value),i}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const i=new _;return this.children.forEach((s=>{i.push(s),s.recursiveChildren.forEach((s=>{i.push(s)}))})),i}}i.exports=Element},5946:(i,s,u)=>{const m=u(17645),v=u(3079);i.exports=class MemberElement extends v{constructor(i,s,u,v){super(new m,u,v),this.element="member",this.key=i,this.value=s}get key(){return this.content.key}set key(i){this.content.key=this.refract(i)}get value(){return this.content.value}set value(i){this.content.value=this.refract(i)}}},96295:(i,s,u)=>{const m=u(3079);i.exports=class NullElement extends m{constructor(i,s,u){super(i||null,s,u),this.element="null"}primitive(){return"null"}set(){return new Error("Cannot set the value of null")}}},91090:(i,s,u)=>{const m=u(3079);i.exports=class NumberElement extends m{constructor(i,s,u){super(i,s,u),this.element="number"}primitive(){return"number"}}},76735:(i,s,u)=>{const m=u(94885),v=u(13218),_=u(35804),j=u(5946),M=u(87526);i.exports=class ObjectElement extends _{constructor(i,s,u){super(i||[],s,u),this.element="object"}primitive(){return"object"}toValue(){return this.content.reduce(((i,s)=>(i[s.key.toValue()]=s.value?s.value.toValue():void 0,i)),{})}get(i){const s=this.getMember(i);if(s)return s.value}getMember(i){if(void 0!==i)return this.content.find((s=>s.key.toValue()===i))}remove(i){let s=null;return this.content=this.content.filter((u=>u.key.toValue()!==i||(s=u,!1))),s}getKey(i){const s=this.getMember(i);if(s)return s.key}set(i,s){if(v(i))return Object.keys(i).forEach((s=>{this.set(s,i[s])})),this;const u=i,m=this.getMember(u);return m?m.value=s:this.content.push(new j(u,s)),this}keys(){return this.content.map((i=>i.key.toValue()))}values(){return this.content.map((i=>i.value.toValue()))}hasKey(i){return this.content.some((s=>s.key.equals(i)))}items(){return this.content.map((i=>[i.key.toValue(),i.value.toValue()]))}map(i,s){return this.content.map((u=>i.bind(s)(u.value,u.key,u)))}compactMap(i,s){const u=[];return this.forEach(((m,v,_)=>{const j=i.bind(s)(m,v,_);j&&u.push(j)})),u}filter(i,s){return new M(this.content).filter(i,s)}reject(i,s){return this.filter(m(i),s)}forEach(i,s){return this.content.forEach((u=>i.bind(s)(u.value,u.key,u)))}}},16036:(i,s,u)=>{const m=u(3079);i.exports=class StringElement extends m{constructor(i,s,u){super(i,s,u),this.element="string"}primitive(){return"string"}get length(){return this.content.length}}},3414:(i,s,u)=>{const m=u(28219);i.exports=class JSON06Serialiser extends m{serialise(i){if(!(i instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${i}\` is not an Element instance`);let s;i._attributes&&i.attributes.get("variable")&&(s=i.attributes.get("variable"));const u={element:i.element};i._meta&&i._meta.length>0&&(u.meta=this.serialiseObject(i.meta));const m="enum"===i.element||-1!==i.attributes.keys().indexOf("enumerations");if(m){const s=this.enumSerialiseAttributes(i);s&&(u.attributes=s)}else if(i._attributes&&i._attributes.length>0){let{attributes:m}=i;m.get("metadata")&&(m=m.clone(),m.set("meta",m.get("metadata")),m.remove("metadata")),"member"===i.element&&s&&(m=m.clone(),m.remove("variable")),m.length>0&&(u.attributes=this.serialiseObject(m))}if(m)u.content=this.enumSerialiseContent(i,u);else if(this[`${i.element}SerialiseContent`])u.content=this[`${i.element}SerialiseContent`](i,u);else if(void 0!==i.content){let m;s&&i.content.key?(m=i.content.clone(),m.key.attributes.set("variable",s),m=this.serialiseContent(m)):m=this.serialiseContent(i.content),this.shouldSerialiseContent(i,m)&&(u.content=m)}else this.shouldSerialiseContent(i,i.content)&&i instanceof this.namespace.elements.Array&&(u.content=[]);return u}shouldSerialiseContent(i,s){return"parseResult"===i.element||"httpRequest"===i.element||"httpResponse"===i.element||"category"===i.element||"link"===i.element||void 0!==s&&(!Array.isArray(s)||0!==s.length)}refSerialiseContent(i,s){return delete s.attributes,{href:i.toValue(),path:i.path.toValue()}}sourceMapSerialiseContent(i){return i.toValue()}dataStructureSerialiseContent(i){return[this.serialiseContent(i.content)]}enumSerialiseAttributes(i){const s=i.attributes.clone(),u=s.remove("enumerations")||new this.namespace.elements.Array([]),m=s.get("default");let v=s.get("samples")||new this.namespace.elements.Array([]);if(m&&m.content&&(m.content.attributes&&m.content.attributes.remove("typeAttributes"),s.set("default",new this.namespace.elements.Array([m.content]))),v.forEach((i=>{i.content&&i.content.element&&i.content.attributes.remove("typeAttributes")})),i.content&&0!==u.length&&v.unshift(i.content),v=v.map((i=>i instanceof this.namespace.elements.Array?[i]:new this.namespace.elements.Array([i.content]))),v.length&&s.set("samples",v),s.length>0)return this.serialiseObject(s)}enumSerialiseContent(i){if(i._attributes){const s=i.attributes.get("enumerations");if(s&&s.length>0)return s.content.map((i=>{const s=i.clone();return s.attributes.remove("typeAttributes"),this.serialise(s)}))}if(i.content){const s=i.content.clone();return s.attributes.remove("typeAttributes"),[this.serialise(s)]}return[]}deserialise(i){if("string"==typeof i)return new this.namespace.elements.String(i);if("number"==typeof i)return new this.namespace.elements.Number(i);if("boolean"==typeof i)return new this.namespace.elements.Boolean(i);if(null===i)return new this.namespace.elements.Null;if(Array.isArray(i))return new this.namespace.elements.Array(i.map(this.deserialise,this));const s=this.namespace.getElementClass(i.element),u=new s;u.element!==i.element&&(u.element=i.element),i.meta&&this.deserialiseObject(i.meta,u.meta),i.attributes&&this.deserialiseObject(i.attributes,u.attributes);const m=this.deserialiseContent(i.content);if(void 0===m&&null!==u.content||(u.content=m),"enum"===u.element){u.content&&u.attributes.set("enumerations",u.content);let i=u.attributes.get("samples");if(u.attributes.remove("samples"),i){const m=i;i=new this.namespace.elements.Array,m.forEach((m=>{m.forEach((m=>{const v=new s(m);v.element=u.element,i.push(v)}))}));const v=i.shift();u.content=v?v.content:void 0,u.attributes.set("samples",i)}else u.content=void 0;let m=u.attributes.get("default");if(m&&m.length>0){m=m.get(0);const i=new s(m);i.element=u.element,u.attributes.set("default",i)}}else if("dataStructure"===u.element&&Array.isArray(u.content))[u.content]=u.content;else if("category"===u.element){const i=u.attributes.get("meta");i&&(u.attributes.set("metadata",i),u.attributes.remove("meta"))}else"member"===u.element&&u.key&&u.key._attributes&&u.key._attributes.getValue("variable")&&(u.attributes.set("variable",u.key.attributes.get("variable")),u.key.attributes.remove("variable"));return u}serialiseContent(i){if(i instanceof this.namespace.elements.Element)return this.serialise(i);if(i instanceof this.namespace.KeyValuePair){const s={key:this.serialise(i.key)};return i.value&&(s.value=this.serialise(i.value)),s}return i&&i.map?i.map(this.serialise,this):i}deserialiseContent(i){if(i){if(i.element)return this.deserialise(i);if(i.key){const s=new this.namespace.KeyValuePair(this.deserialise(i.key));return i.value&&(s.value=this.deserialise(i.value)),s}if(i.map)return i.map(this.deserialise,this)}return i}shouldRefract(i){return!!(i._attributes&&i.attributes.keys().length||i._meta&&i.meta.keys().length)||"enum"!==i.element&&(i.element!==i.primitive()||"member"===i.element)}convertKeyToRefract(i,s){return this.shouldRefract(s)?this.serialise(s):"enum"===s.element?this.serialiseEnum(s):"array"===s.element?s.map((s=>this.shouldRefract(s)||"default"===i?this.serialise(s):"array"===s.element||"object"===s.element||"enum"===s.element?s.children.map((i=>this.serialise(i))):s.toValue())):"object"===s.element?(s.content||[]).map(this.serialise,this):s.toValue()}serialiseEnum(i){return i.children.map((i=>this.serialise(i)))}serialiseObject(i){const s={};return i.forEach(((i,u)=>{if(i){const m=u.toValue();s[m]=this.convertKeyToRefract(m,i)}})),s}deserialiseObject(i,s){Object.keys(i).forEach((u=>{s.set(u,this.deserialise(i[u]))}))}}},28219:i=>{i.exports=class JSONSerialiser{constructor(i){this.namespace=i||new this.Namespace}serialise(i){if(!(i instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${i}\` is not an Element instance`);const s={element:i.element};i._meta&&i._meta.length>0&&(s.meta=this.serialiseObject(i.meta)),i._attributes&&i._attributes.length>0&&(s.attributes=this.serialiseObject(i.attributes));const u=this.serialiseContent(i.content);return void 0!==u&&(s.content=u),s}deserialise(i){if(!i.element)throw new Error("Given value is not an object containing an element name");const s=new(this.namespace.getElementClass(i.element));s.element!==i.element&&(s.element=i.element),i.meta&&this.deserialiseObject(i.meta,s.meta),i.attributes&&this.deserialiseObject(i.attributes,s.attributes);const u=this.deserialiseContent(i.content);return void 0===u&&null!==s.content||(s.content=u),s}serialiseContent(i){if(i instanceof this.namespace.elements.Element)return this.serialise(i);if(i instanceof this.namespace.KeyValuePair){const s={key:this.serialise(i.key)};return i.value&&(s.value=this.serialise(i.value)),s}if(i&&i.map){if(0===i.length)return;return i.map(this.serialise,this)}return i}deserialiseContent(i){if(i){if(i.element)return this.deserialise(i);if(i.key){const s=new this.namespace.KeyValuePair(this.deserialise(i.key));return i.value&&(s.value=this.deserialise(i.value)),s}if(i.map)return i.map(this.deserialise,this)}return i}serialiseObject(i){const s={};if(i.forEach(((i,u)=>{i&&(s[u.toValue()]=this.serialise(i))})),0!==Object.keys(s).length)return s}deserialiseObject(i,s){Object.keys(i).forEach((u=>{s.set(u,this.deserialise(i[u]))}))}}},27418:i=>{"use strict";var s=Object.getOwnPropertySymbols,u=Object.prototype.hasOwnProperty,m=Object.prototype.propertyIsEnumerable;i.exports=function shouldUseNative(){try{if(!Object.assign)return!1;var i=new String("abc");if(i[5]="de","5"===Object.getOwnPropertyNames(i)[0])return!1;for(var s={},u=0;u<10;u++)s["_"+String.fromCharCode(u)]=u;if("0123456789"!==Object.getOwnPropertyNames(s).map((function(i){return s[i]})).join(""))return!1;var m={};return"abcdefghijklmnopqrst".split("").forEach((function(i){m[i]=i})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},m)).join("")}catch(i){return!1}}()?Object.assign:function(i,v){for(var _,j,M=function toObject(i){if(null==i)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(i)}(i),$=1;${var m="function"==typeof Map&&Map.prototype,v=Object.getOwnPropertyDescriptor&&m?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,_=m&&v&&"function"==typeof v.get?v.get:null,j=m&&Map.prototype.forEach,M="function"==typeof Set&&Set.prototype,$=Object.getOwnPropertyDescriptor&&M?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,W=M&&$&&"function"==typeof $.get?$.get:null,X=M&&Set.prototype.forEach,Y="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,Z="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,ee="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,ae=Boolean.prototype.valueOf,ie=Object.prototype.toString,le=Function.prototype.toString,ce=String.prototype.match,pe=String.prototype.slice,de=String.prototype.replace,fe=String.prototype.toUpperCase,ye=String.prototype.toLowerCase,be=RegExp.prototype.test,_e=Array.prototype.concat,we=Array.prototype.join,Se=Array.prototype.slice,xe=Math.floor,Pe="function"==typeof BigInt?BigInt.prototype.valueOf:null,Ie=Object.getOwnPropertySymbols,Te="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,Re="function"==typeof Symbol&&"object"==typeof Symbol.iterator,qe="function"==typeof Symbol&&Symbol.toStringTag&&(typeof Symbol.toStringTag===Re||"symbol")?Symbol.toStringTag:null,ze=Object.prototype.propertyIsEnumerable,Ve=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(i){return i.__proto__}:null);function addNumericSeparator(i,s){if(i===1/0||i===-1/0||i!=i||i&&i>-1e3&&i<1e3||be.call(/e/,s))return s;var u=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof i){var m=i<0?-xe(-i):xe(i);if(m!==i){var v=String(m),_=pe.call(s,v.length+1);return de.call(v,u,"$&_")+"."+de.call(de.call(_,/([0-9]{3})/g,"$&_"),/_$/,"")}}return de.call(s,u,"$&_")}var We=u(24654),He=We.custom,Xe=isSymbol(He)?He:null;function wrapQuotes(i,s,u){var m="double"===(u.quoteStyle||s)?'"':"'";return m+i+m}function quote(i){return de.call(String(i),/"/g,""")}function isArray(i){return!("[object Array]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}function isRegExp(i){return!("[object RegExp]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}function isSymbol(i){if(Re)return i&&"object"==typeof i&&i instanceof Symbol;if("symbol"==typeof i)return!0;if(!i||"object"!=typeof i||!Te)return!1;try{return Te.call(i),!0}catch(i){}return!1}i.exports=function inspect_(i,s,u,m){var v=s||{};if(has(v,"quoteStyle")&&"single"!==v.quoteStyle&&"double"!==v.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(has(v,"maxStringLength")&&("number"==typeof v.maxStringLength?v.maxStringLength<0&&v.maxStringLength!==1/0:null!==v.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var M=!has(v,"customInspect")||v.customInspect;if("boolean"!=typeof M&&"symbol"!==M)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(has(v,"indent")&&null!==v.indent&&"\t"!==v.indent&&!(parseInt(v.indent,10)===v.indent&&v.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(has(v,"numericSeparator")&&"boolean"!=typeof v.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var $=v.numericSeparator;if(void 0===i)return"undefined";if(null===i)return"null";if("boolean"==typeof i)return i?"true":"false";if("string"==typeof i)return inspectString(i,v);if("number"==typeof i){if(0===i)return 1/0/i>0?"0":"-0";var ie=String(i);return $?addNumericSeparator(i,ie):ie}if("bigint"==typeof i){var fe=String(i)+"n";return $?addNumericSeparator(i,fe):fe}var be=void 0===v.depth?5:v.depth;if(void 0===u&&(u=0),u>=be&&be>0&&"object"==typeof i)return isArray(i)?"[Array]":"[Object]";var xe=function getIndent(i,s){var u;if("\t"===i.indent)u="\t";else{if(!("number"==typeof i.indent&&i.indent>0))return null;u=we.call(Array(i.indent+1)," ")}return{base:u,prev:we.call(Array(s+1),u)}}(v,u);if(void 0===m)m=[];else if(indexOf(m,i)>=0)return"[Circular]";function inspect(i,s,_){if(s&&(m=Se.call(m)).push(s),_){var j={depth:v.depth};return has(v,"quoteStyle")&&(j.quoteStyle=v.quoteStyle),inspect_(i,j,u+1,m)}return inspect_(i,v,u+1,m)}if("function"==typeof i&&!isRegExp(i)){var Ie=function nameOf(i){if(i.name)return i.name;var s=ce.call(le.call(i),/^function\s*([\w$]+)/);if(s)return s[1];return null}(i),He=arrObjKeys(i,inspect);return"[Function"+(Ie?": "+Ie:" (anonymous)")+"]"+(He.length>0?" { "+we.call(He,", ")+" }":"")}if(isSymbol(i)){var Ye=Re?de.call(String(i),/^(Symbol\(.*\))_[^)]*$/,"$1"):Te.call(i);return"object"!=typeof i||Re?Ye:markBoxed(Ye)}if(function isElement(i){if(!i||"object"!=typeof i)return!1;if("undefined"!=typeof HTMLElement&&i instanceof HTMLElement)return!0;return"string"==typeof i.nodeName&&"function"==typeof i.getAttribute}(i)){for(var Qe="<"+ye.call(String(i.nodeName)),et=i.attributes||[],tt=0;tt"}if(isArray(i)){if(0===i.length)return"[]";var rt=arrObjKeys(i,inspect);return xe&&!function singleLineValues(i){for(var s=0;s=0)return!1;return!0}(rt)?"["+indentedJoin(rt,xe)+"]":"[ "+we.call(rt,", ")+" ]"}if(function isError(i){return!("[object Error]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i)){var nt=arrObjKeys(i,inspect);return"cause"in Error.prototype||!("cause"in i)||ze.call(i,"cause")?0===nt.length?"["+String(i)+"]":"{ ["+String(i)+"] "+we.call(nt,", ")+" }":"{ ["+String(i)+"] "+we.call(_e.call("[cause]: "+inspect(i.cause),nt),", ")+" }"}if("object"==typeof i&&M){if(Xe&&"function"==typeof i[Xe]&&We)return We(i,{depth:be-u});if("symbol"!==M&&"function"==typeof i.inspect)return i.inspect()}if(function isMap(i){if(!_||!i||"object"!=typeof i)return!1;try{_.call(i);try{W.call(i)}catch(i){return!0}return i instanceof Map}catch(i){}return!1}(i)){var ot=[];return j&&j.call(i,(function(s,u){ot.push(inspect(u,i,!0)+" => "+inspect(s,i))})),collectionOf("Map",_.call(i),ot,xe)}if(function isSet(i){if(!W||!i||"object"!=typeof i)return!1;try{W.call(i);try{_.call(i)}catch(i){return!0}return i instanceof Set}catch(i){}return!1}(i)){var at=[];return X&&X.call(i,(function(s){at.push(inspect(s,i))})),collectionOf("Set",W.call(i),at,xe)}if(function isWeakMap(i){if(!Y||!i||"object"!=typeof i)return!1;try{Y.call(i,Y);try{Z.call(i,Z)}catch(i){return!0}return i instanceof WeakMap}catch(i){}return!1}(i))return weakCollectionOf("WeakMap");if(function isWeakSet(i){if(!Z||!i||"object"!=typeof i)return!1;try{Z.call(i,Z);try{Y.call(i,Y)}catch(i){return!0}return i instanceof WeakSet}catch(i){}return!1}(i))return weakCollectionOf("WeakSet");if(function isWeakRef(i){if(!ee||!i||"object"!=typeof i)return!1;try{return ee.call(i),!0}catch(i){}return!1}(i))return weakCollectionOf("WeakRef");if(function isNumber(i){return!("[object Number]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i))return markBoxed(inspect(Number(i)));if(function isBigInt(i){if(!i||"object"!=typeof i||!Pe)return!1;try{return Pe.call(i),!0}catch(i){}return!1}(i))return markBoxed(inspect(Pe.call(i)));if(function isBoolean(i){return!("[object Boolean]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i))return markBoxed(ae.call(i));if(function isString(i){return!("[object String]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i))return markBoxed(inspect(String(i)));if(!function isDate(i){return!("[object Date]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i)&&!isRegExp(i)){var it=arrObjKeys(i,inspect),st=Ve?Ve(i)===Object.prototype:i instanceof Object||i.constructor===Object,lt=i instanceof Object?"":"null prototype",ct=!st&&qe&&Object(i)===i&&qe in i?pe.call(toStr(i),8,-1):lt?"Object":"",ut=(st||"function"!=typeof i.constructor?"":i.constructor.name?i.constructor.name+" ":"")+(ct||lt?"["+we.call(_e.call([],ct||[],lt||[]),": ")+"] ":"");return 0===it.length?ut+"{}":xe?ut+"{"+indentedJoin(it,xe)+"}":ut+"{ "+we.call(it,", ")+" }"}return String(i)};var Ye=Object.prototype.hasOwnProperty||function(i){return i in this};function has(i,s){return Ye.call(i,s)}function toStr(i){return ie.call(i)}function indexOf(i,s){if(i.indexOf)return i.indexOf(s);for(var u=0,m=i.length;us.maxStringLength){var u=i.length-s.maxStringLength,m="... "+u+" more character"+(u>1?"s":"");return inspectString(pe.call(i,0,s.maxStringLength),s)+m}return wrapQuotes(de.call(de.call(i,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,lowbyte),"single",s)}function lowbyte(i){var s=i.charCodeAt(0),u={8:"b",9:"t",10:"n",12:"f",13:"r"}[s];return u?"\\"+u:"\\x"+(s<16?"0":"")+fe.call(s.toString(16))}function markBoxed(i){return"Object("+i+")"}function weakCollectionOf(i){return i+" { ? }"}function collectionOf(i,s,u,m){return i+" ("+s+") {"+(m?indentedJoin(u,m):we.call(u,", "))+"}"}function indentedJoin(i,s){if(0===i.length)return"";var u="\n"+s.prev+s.base;return u+we.call(i,","+u)+"\n"+s.prev}function arrObjKeys(i,s){var u=isArray(i),m=[];if(u){m.length=i.length;for(var v=0;v{var s,u,m=i.exports={};function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}function runTimeout(i){if(s===setTimeout)return setTimeout(i,0);if((s===defaultSetTimout||!s)&&setTimeout)return s=setTimeout,setTimeout(i,0);try{return s(i,0)}catch(u){try{return s.call(null,i,0)}catch(u){return s.call(this,i,0)}}}!function(){try{s="function"==typeof setTimeout?setTimeout:defaultSetTimout}catch(i){s=defaultSetTimout}try{u="function"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(i){u=defaultClearTimeout}}();var v,_=[],j=!1,M=-1;function cleanUpNextTick(){j&&v&&(j=!1,v.length?_=v.concat(_):M=-1,_.length&&drainQueue())}function drainQueue(){if(!j){var i=runTimeout(cleanUpNextTick);j=!0;for(var s=_.length;s;){for(v=_,_=[];++M1)for(var u=1;u{"use strict";var m=u(50414);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,i.exports=function(){function shim(i,s,u,v,_,j){if(j!==m){var M=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw M.name="Invariant Violation",M}}function getShim(){return shim}shim.isRequired=shim;var i={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:shim,elementType:shim,instanceOf:getShim,node:shim,objectOf:getShim,oneOf:getShim,oneOfType:getShim,shape:getShim,exact:getShim,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return i.PropTypes=i,i}},45697:(i,s,u)=>{i.exports=u(92703)()},50414:i=>{"use strict";i.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},55798:i=>{"use strict";var s=String.prototype.replace,u=/%20/g,m="RFC1738",v="RFC3986";i.exports={default:v,formatters:{RFC1738:function(i){return s.call(i,u,"+")},RFC3986:function(i){return String(i)}},RFC1738:m,RFC3986:v}},80129:(i,s,u)=>{"use strict";var m=u(58261),v=u(55235),_=u(55798);i.exports={formats:_,parse:v,stringify:m}},55235:(i,s,u)=>{"use strict";var m=u(12769),v=Object.prototype.hasOwnProperty,_=Array.isArray,j={allowDots:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:m.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},interpretNumericEntities=function(i){return i.replace(/&#(\d+);/g,(function(i,s){return String.fromCharCode(parseInt(s,10))}))},parseArrayValue=function(i,s){return i&&"string"==typeof i&&s.comma&&i.indexOf(",")>-1?i.split(","):i},M=function parseQueryStringKeys(i,s,u,m){if(i){var _=u.allowDots?i.replace(/\.([^.[]+)/g,"[$1]"):i,j=/(\[[^[\]]*])/g,M=u.depth>0&&/(\[[^[\]]*])/.exec(_),$=M?_.slice(0,M.index):_,W=[];if($){if(!u.plainObjects&&v.call(Object.prototype,$)&&!u.allowPrototypes)return;W.push($)}for(var X=0;u.depth>0&&null!==(M=j.exec(_))&&X=0;--_){var j,M=i[_];if("[]"===M&&u.parseArrays)j=[].concat(v);else{j=u.plainObjects?Object.create(null):{};var $="["===M.charAt(0)&&"]"===M.charAt(M.length-1)?M.slice(1,-1):M,W=parseInt($,10);u.parseArrays||""!==$?!isNaN(W)&&M!==$&&String(W)===$&&W>=0&&u.parseArrays&&W<=u.arrayLimit?(j=[])[W]=v:"__proto__"!==$&&(j[$]=v):j={0:v}}v=j}return v}(W,s,u,m)}};i.exports=function(i,s){var u=function normalizeParseOptions(i){if(!i)return j;if(null!==i.decoder&&void 0!==i.decoder&&"function"!=typeof i.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==i.charset&&"utf-8"!==i.charset&&"iso-8859-1"!==i.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var s=void 0===i.charset?j.charset:i.charset;return{allowDots:void 0===i.allowDots?j.allowDots:!!i.allowDots,allowPrototypes:"boolean"==typeof i.allowPrototypes?i.allowPrototypes:j.allowPrototypes,allowSparse:"boolean"==typeof i.allowSparse?i.allowSparse:j.allowSparse,arrayLimit:"number"==typeof i.arrayLimit?i.arrayLimit:j.arrayLimit,charset:s,charsetSentinel:"boolean"==typeof i.charsetSentinel?i.charsetSentinel:j.charsetSentinel,comma:"boolean"==typeof i.comma?i.comma:j.comma,decoder:"function"==typeof i.decoder?i.decoder:j.decoder,delimiter:"string"==typeof i.delimiter||m.isRegExp(i.delimiter)?i.delimiter:j.delimiter,depth:"number"==typeof i.depth||!1===i.depth?+i.depth:j.depth,ignoreQueryPrefix:!0===i.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof i.interpretNumericEntities?i.interpretNumericEntities:j.interpretNumericEntities,parameterLimit:"number"==typeof i.parameterLimit?i.parameterLimit:j.parameterLimit,parseArrays:!1!==i.parseArrays,plainObjects:"boolean"==typeof i.plainObjects?i.plainObjects:j.plainObjects,strictNullHandling:"boolean"==typeof i.strictNullHandling?i.strictNullHandling:j.strictNullHandling}}(s);if(""===i||null==i)return u.plainObjects?Object.create(null):{};for(var $="string"==typeof i?function parseQueryStringValues(i,s){var u,M={},$=s.ignoreQueryPrefix?i.replace(/^\?/,""):i,W=s.parameterLimit===1/0?void 0:s.parameterLimit,X=$.split(s.delimiter,W),Y=-1,Z=s.charset;if(s.charsetSentinel)for(u=0;u-1&&(ae=_(ae)?[ae]:ae),v.call(M,ee)?M[ee]=m.combine(M[ee],ae):M[ee]=ae}return M}(i,u):i,W=u.plainObjects?Object.create(null):{},X=Object.keys($),Y=0;Y{"use strict";var m=u(37478),v=u(12769),_=u(55798),j=Object.prototype.hasOwnProperty,M={brackets:function brackets(i){return i+"[]"},comma:"comma",indices:function indices(i,s){return i+"["+s+"]"},repeat:function repeat(i){return i}},$=Array.isArray,W=String.prototype.split,X=Array.prototype.push,pushToArray=function(i,s){X.apply(i,$(s)?s:[s])},Y=Date.prototype.toISOString,Z=_.default,ee={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:v.encode,encodeValuesOnly:!1,format:Z,formatter:_.formatters[Z],indices:!1,serializeDate:function serializeDate(i){return Y.call(i)},skipNulls:!1,strictNullHandling:!1},ae={},ie=function stringify(i,s,u,_,j,M,X,Y,Z,ie,le,ce,pe,de,fe,ye){for(var be=i,_e=ye,we=0,Se=!1;void 0!==(_e=_e.get(ae))&&!Se;){var xe=_e.get(i);if(we+=1,void 0!==xe){if(xe===we)throw new RangeError("Cyclic object value");Se=!0}void 0===_e.get(ae)&&(we=0)}if("function"==typeof Y?be=Y(s,be):be instanceof Date?be=le(be):"comma"===u&&$(be)&&(be=v.maybeMap(be,(function(i){return i instanceof Date?le(i):i}))),null===be){if(j)return X&&!de?X(s,ee.encoder,fe,"key",ce):s;be=""}if(function isNonNullishPrimitive(i){return"string"==typeof i||"number"==typeof i||"boolean"==typeof i||"symbol"==typeof i||"bigint"==typeof i}(be)||v.isBuffer(be)){if(X){var Pe=de?s:X(s,ee.encoder,fe,"key",ce);if("comma"===u&&de){for(var Ie=W.call(String(be),","),Te="",Re=0;Re0?be.join(",")||null:void 0}];else if($(Y))qe=Y;else{var Ve=Object.keys(be);qe=Z?Ve.sort(Z):Ve}for(var We=_&&$(be)&&1===be.length?s+"[]":s,He=0;He0?fe+de:""}},12769:(i,s,u)=>{"use strict";var m=u(55798),v=Object.prototype.hasOwnProperty,_=Array.isArray,j=function(){for(var i=[],s=0;s<256;++s)i.push("%"+((s<16?"0":"")+s.toString(16)).toUpperCase());return i}(),M=function arrayToObject(i,s){for(var u=s&&s.plainObjects?Object.create(null):{},m=0;m1;){var s=i.pop(),u=s.obj[s.prop];if(_(u)){for(var m=[],v=0;v=48&&X<=57||X>=65&&X<=90||X>=97&&X<=122||_===m.RFC1738&&(40===X||41===X)?$+=M.charAt(W):X<128?$+=j[X]:X<2048?$+=j[192|X>>6]+j[128|63&X]:X<55296||X>=57344?$+=j[224|X>>12]+j[128|X>>6&63]+j[128|63&X]:(W+=1,X=65536+((1023&X)<<10|1023&M.charCodeAt(W)),$+=j[240|X>>18]+j[128|X>>12&63]+j[128|X>>6&63]+j[128|63&X])}return $},isBuffer:function isBuffer(i){return!(!i||"object"!=typeof i)&&!!(i.constructor&&i.constructor.isBuffer&&i.constructor.isBuffer(i))},isRegExp:function isRegExp(i){return"[object RegExp]"===Object.prototype.toString.call(i)},maybeMap:function maybeMap(i,s){if(_(i)){for(var u=[],m=0;m{"use strict";var u=Object.prototype.hasOwnProperty;function decode(i){try{return decodeURIComponent(i.replace(/\+/g," "))}catch(i){return null}}function encode(i){try{return encodeURIComponent(i)}catch(i){return null}}s.stringify=function querystringify(i,s){s=s||"";var m,v,_=[];for(v in"string"!=typeof s&&(s="?"),i)if(u.call(i,v)){if((m=i[v])||null!=m&&!isNaN(m)||(m=""),v=encode(v),m=encode(m),null===v||null===m)continue;_.push(v+"="+m)}return _.length?s+_.join("&"):""},s.parse=function querystring(i){for(var s,u=/([^=?#&]+)=?([^&]*)/g,m={};s=u.exec(i);){var v=decode(s[1]),_=decode(s[2]);null===v||null===_||v in m||(m[v]=_)}return m}},14419:(i,s,u)=>{const m=u(60697),v=u(69450),_=m.types;i.exports=class RandExp{constructor(i,s){if(this._setDefaults(i),i instanceof RegExp)this.ignoreCase=i.ignoreCase,this.multiline=i.multiline,i=i.source;else{if("string"!=typeof i)throw new Error("Expected a regexp or string");this.ignoreCase=s&&-1!==s.indexOf("i"),this.multiline=s&&-1!==s.indexOf("m")}this.tokens=m(i)}_setDefaults(i){this.max=null!=i.max?i.max:null!=RandExp.prototype.max?RandExp.prototype.max:100,this.defaultRange=i.defaultRange?i.defaultRange:this.defaultRange.clone(),i.randInt&&(this.randInt=i.randInt)}gen(){return this._gen(this.tokens,[])}_gen(i,s){var u,m,v,j,M;switch(i.type){case _.ROOT:case _.GROUP:if(i.followedBy||i.notFollowedBy)return"";for(i.remember&&void 0===i.groupNumber&&(i.groupNumber=s.push(null)-1),m="",j=0,M=(u=i.options?this._randSelect(i.options):i.stack).length;j{"use strict";var m=u(34155),v=65536,_=4294967295;var j=u(89509).Buffer,M=u.g.crypto||u.g.msCrypto;M&&M.getRandomValues?i.exports=function randomBytes(i,s){if(i>_)throw new RangeError("requested too many random bytes");var u=j.allocUnsafe(i);if(i>0)if(i>v)for(var $=0;${"use strict";function _typeof(i){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},_typeof(i)}Object.defineProperty(s,"__esModule",{value:!0}),s.CopyToClipboard=void 0;var m=_interopRequireDefault(u(67294)),v=_interopRequireDefault(u(20640)),_=["text","onCopy","options","children"];function _interopRequireDefault(i){return i&&i.__esModule?i:{default:i}}function ownKeys(i,s){var u=Object.keys(i);if(Object.getOwnPropertySymbols){var m=Object.getOwnPropertySymbols(i);s&&(m=m.filter((function(s){return Object.getOwnPropertyDescriptor(i,s).enumerable}))),u.push.apply(u,m)}return u}function _objectSpread(i){for(var s=1;s=0||(v[u]=i[u]);return v}(i,s);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(i);for(m=0;m<_.length;m++)u=_[m],s.indexOf(u)>=0||Object.prototype.propertyIsEnumerable.call(i,u)&&(v[u]=i[u])}return v}function _defineProperties(i,s){for(var u=0;u{"use strict";var m=u(74300).CopyToClipboard;m.CopyToClipboard=m,i.exports=m},53441:(i,s,u)=>{"use strict";function _typeof(i){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},_typeof(i)}Object.defineProperty(s,"__esModule",{value:!0}),s.DebounceInput=void 0;var m=_interopRequireDefault(u(67294)),v=_interopRequireDefault(u(91296)),_=["element","onChange","value","minLength","debounceTimeout","forceNotifyByEnter","forceNotifyOnBlur","onKeyDown","onBlur","inputRef"];function _interopRequireDefault(i){return i&&i.__esModule?i:{default:i}}function _objectWithoutProperties(i,s){if(null==i)return{};var u,m,v=function _objectWithoutPropertiesLoose(i,s){if(null==i)return{};var u,m,v={},_=Object.keys(i);for(m=0;m<_.length;m++)u=_[m],s.indexOf(u)>=0||(v[u]=i[u]);return v}(i,s);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(i);for(m=0;m<_.length;m++)u=_[m],s.indexOf(u)>=0||Object.prototype.propertyIsEnumerable.call(i,u)&&(v[u]=i[u])}return v}function ownKeys(i,s){var u=Object.keys(i);if(Object.getOwnPropertySymbols){var m=Object.getOwnPropertySymbols(i);s&&(m=m.filter((function(s){return Object.getOwnPropertyDescriptor(i,s).enumerable}))),u.push.apply(u,m)}return u}function _objectSpread(i){for(var s=1;s=m?u.notify(i):s.length>v.length&&u.notify(_objectSpread(_objectSpread({},i),{},{target:_objectSpread(_objectSpread({},i.target),{},{value:""})}))}))})),_defineProperty(_assertThisInitialized(u),"onKeyDown",(function(i){"Enter"===i.key&&u.forceNotify(i);var s=u.props.onKeyDown;s&&(i.persist(),s(i))})),_defineProperty(_assertThisInitialized(u),"onBlur",(function(i){u.forceNotify(i);var s=u.props.onBlur;s&&(i.persist(),s(i))})),_defineProperty(_assertThisInitialized(u),"createNotifier",(function(i){if(i<0)u.notify=function(){return null};else if(0===i)u.notify=u.doNotify;else{var s=(0,v.default)((function(i){u.isDebouncing=!1,u.doNotify(i)}),i);u.notify=function(i){u.isDebouncing=!0,s(i)},u.flush=function(){return s.flush()},u.cancel=function(){u.isDebouncing=!1,s.cancel()}}})),_defineProperty(_assertThisInitialized(u),"doNotify",(function(){u.props.onChange.apply(void 0,arguments)})),_defineProperty(_assertThisInitialized(u),"forceNotify",(function(i){var s=u.props.debounceTimeout;if(u.isDebouncing||!(s>0)){u.cancel&&u.cancel();var m=u.state.value,v=u.props.minLength;m.length>=v?u.doNotify(i):u.doNotify(_objectSpread(_objectSpread({},i),{},{target:_objectSpread(_objectSpread({},i.target),{},{value:m})}))}})),u.isDebouncing=!1,u.state={value:void 0===i.value||null===i.value?"":i.value};var m=u.props.debounceTimeout;return u.createNotifier(m),u}return function _createClass(i,s,u){return s&&_defineProperties(i.prototype,s),u&&_defineProperties(i,u),Object.defineProperty(i,"prototype",{writable:!1}),i}(DebounceInput,[{key:"componentDidUpdate",value:function componentDidUpdate(i){if(!this.isDebouncing){var s=this.props,u=s.value,m=s.debounceTimeout,v=i.debounceTimeout,_=i.value,j=this.state.value;void 0!==u&&_!==u&&j!==u&&this.setState({value:u}),m!==v&&this.createNotifier(m)}}},{key:"componentWillUnmount",value:function componentWillUnmount(){this.flush&&this.flush()}},{key:"render",value:function render(){var i,s,u=this.props,v=u.element,j=(u.onChange,u.value,u.minLength,u.debounceTimeout,u.forceNotifyByEnter),M=u.forceNotifyOnBlur,$=u.onKeyDown,W=u.onBlur,X=u.inputRef,Y=_objectWithoutProperties(u,_),Z=this.state.value;i=j?{onKeyDown:this.onKeyDown}:$?{onKeyDown:$}:{},s=M?{onBlur:this.onBlur}:W?{onBlur:W}:{};var ee=X?{ref:X}:{};return m.default.createElement(v,_objectSpread(_objectSpread(_objectSpread(_objectSpread({},Y),{},{onChange:this.onChange,value:Z},i),s),ee))}}]),DebounceInput}(m.default.PureComponent);s.DebounceInput=j,_defineProperty(j,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},775:(i,s,u)=>{"use strict";var m=u(53441).DebounceInput;m.DebounceInput=m,i.exports=m},64448:(i,s,u)=>{"use strict";var m=u(67294),v=u(27418),_=u(63840);function y(i){for(var s="https://reactjs.org/docs/error-decoder.html?invariant="+i,u=1;u