modify: chongming-cli

This commit is contained in:
2026-08-19 17:28:23 +08:00
parent 679f9e4259
commit 41ae36f34d
10 changed files with 60 additions and 1850 deletions
+9 -8
View File
@@ -50,7 +50,6 @@ chongming-zenoh/
│ │ ├── app/
│ │ │ ├── core/ # WorkerManager, RouteGenerator, MQTTBridge, contracts(契约校验), auth
│ │ │ └── routes/ # 内置路由(文件上传等)
│ │ └── scripts/ # build, gen_models, gen_plugin
│ ├── chongming-web/ # Vue 3 前端
│ │ └── src/
│ │ ├── api/generated/ # 自动生成 API 客户端
@@ -65,6 +64,7 @@ chongming-zenoh/
│ └── demo/ # 生产在独立容器部署,示例见本地目录)
├── utils/ # 独立工具包
│ ├── bytedance/ # 字节跳动安全通信 SDK
│ ├── cli/ # 命令脚本集合(build / gen_models / gen_plugin,chongming-cli)
│ ├── config/ # TOML 配置加载
│ ├── database/ # 异步 SQLite 封装
│ ├── jwt/ # Rust JWT 编解码
@@ -92,6 +92,7 @@ chongming-zenoh/
| ---------------- | ----------------------------------------------- | -------------------------- |
| `src/chongming/` | 本仓库(chongming-core) | 网关核心(FastAPI + Zenoh)|
| `utils/<name>/` | `chongming-utils-*`(9 个,见「工具包列表」) | 工具包,由 `uv sync` 按 git 源安装 |
| `utils/cli/` | `chongming-utils-cli`(规划中,暂以 path 源安装)| 命令脚本集合(build / build_plugin / gen_models / gen_plugin) |
| `src/plugins/` | `chongming-plugins` | Worker 插件源码 |
| `config/` | `chongming-config` | 应用与 Worker 路由配置 |
| `src/chongming-web/` | `chongming-web` | Vue 3 前端 |
@@ -396,7 +397,7 @@ python publisher.py example add '{"a":3,"b":5}' # 调用 add
2. 创建 config/workers/<name>/config.toml,在 [workers] 节注册插件
(type / module / factory / cleanup / count + [workers.config] 运行时配置)
3. 创建 config/workers/<name>/*.toml 端点定义
4. 运行 uv run python -m chongming.scripts.gen_models 生成模型
4. 运行 uv run gen_models 生成模型
5. 实现 router/<op>.py,引用 models/pydantic.py 中的模型
6. 更新 router/__init__.py 导入
```
@@ -409,7 +410,7 @@ python publisher.py example add '{"a":3,"b":5}' # 调用 add
```text
1. 创建 config/workers/<name>/<op>.toml
2. 运行 uv run python -m chongming.scripts.gen_models
2. 运行 uv run gen_models
3. 创建 router/<op>.py → 引用生成的模型
4. 更新 router/__init__.py
5. 重启服务
@@ -420,8 +421,8 @@ python publisher.py example add '{"a":3,"b":5}' # 调用 add
```bash
uv run serve # 启动开发服务
uv run gen_plugin <name> # 生成统一风格插件骨架
uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型
uv run python -m chongming.scripts.build # 构建打包(PyInstaller/Nuitka)
uv run gen_models # 生成 Pydantic 模型
uv run build # 构建打包(PyInstaller/Nuitka)
uv run worker_serve --type <name> # 独立 Worker 进程(本地调试,load_mode=external)
chongming-worker --type <name> # 独立 Worker 进程(容器入口)
uv pip install -e utils/<name> # 安装/更新工具包
@@ -485,10 +486,10 @@ uv pip install -e utils/<name> # 安装/更新工具包
```bash
# PyInstaller 打包
uv run python -m chongming.scripts.build pyinstaller
uv run build pyinstaller
# Nuitka 打包
uv run python -m chongming.scripts.build nuitka
uv run build nuitka
```
构建过程包括:模块加密打包 → 资源复制 → PyInstaller/Nuitka 打包 → 数据库初始化 → 前端编译 → VFS 挂载。
@@ -500,7 +501,7 @@ uv run python -m chongming.scripts.build nuitka
```bash
uv run serve # 启动开发服务器
uv run gen_plugin <name> # 生成统一风格插件骨架
uv run python -m chongming.scripts.gen_models # 生成 Pydantic 模型
uv run gen_models # 生成 Pydantic 模型
uv run build pyinstaller # PyInstaller 打包
uv run build nuitka # Nuitka 打包
uv run build_plugin <name> # 进程插件打包 → dist/<name>.zip
+2 -5
View File
@@ -4,7 +4,6 @@ requires = ["hatchling"]
[project]
dependencies = [
"apscheduler",
"chongming-jwt",
"chongming-request",
"chongming-database",
@@ -39,6 +38,7 @@ version = "0.1.0"
[dependency-groups]
dev = [
"chongming-cli",
"nuitka",
"pyarmor",
"pyinstaller",
@@ -62,12 +62,9 @@ chongming-database = { git = "http://124.71.68.6:3000/chongming/chongming-utils-
chongming-cache = { git = "http://124.71.68.6:3000/chongming/chongming-utils-cache.git" }
chongming-mqtt = { git = "http://124.71.68.6:3000/chongming/chongming-mqtt.git" }
chongming-worker = { git = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git"}
chongming-cli = { git = "http://124.71.68.6:3000/chongming/chongming-utils-cli.git" }
[project.scripts]
build = "chongming.scripts.build:build"
build_plugin = "chongming.scripts.build_plugin:build_plugin"
gen_models = "chongming.scripts.gen_models:gen_models"
gen_plugin = "chongming.scripts.gen_plugin:gen_plugin"
gunicorn = "chongming:gunicorn_serve"
serve = "chongming:serve"
worker_serve = "chongming_worker.process:worker_serve"
-233
View File
@@ -1,233 +0,0 @@
import sys
from pathlib import Path
from typing import TypedDict, Literal, Optional, cast, List
# 根据Python版本选择合适的TOML解析库和Required类型
if sys.version_info >= (3, 11):
import tomllib
from typing import NotRequired, Required
else:
import tomli as tomllib
from typing_extensions import NotRequired, Required
# 设置 env 为 development 或 production
class DefaultApp(TypedDict):
"""应用默认配置结构"""
name: str
version: str
description: str
debug: bool
class Default(TypedDict):
"""系统默认配置结构,包含应用信息、环境、路径等基础配置"""
app: DefaultApp
env: Literal["production", "development"]
prefix: str
upload_path: str
class SqliteConnectionArgs(TypedDict, total=False):
"""SQLite数据库连接参数配置"""
timeout: int
check_same_thread: bool
class DatabaseSqlite(TypedDict, total=False):
"""SQLite数据库连接池及行为配置"""
echo: bool
pool_size: int
max_overflow: int
pool_timeout: int
pool_recycle: int
pool_pre_ping: bool
connect_args: SqliteConnectionArgs
class Database(TypedDict, total=False):
"""数据库配置结构,支持SQLite、MySQL、PostgreSQL三种数据库类型"""
type: Required[Literal["sqlite", "mysql", "postgresql"]]
url: Required[str]
generate_schemas: bool
sqlite: DatabaseSqlite
mysql: dict
postgresql: dict
database_path: str
class Security(TypedDict):
"""安全相关配置,包括JWT令牌和安全策略设置"""
secret_key: str
algorithm: Literal["HS256"]
access_token_expire_minutes: int
refresh_token_expire_days: int
max_sessions_per_user: int
class Server(TypedDict, total=False):
"""服务器运行配置,包含主机、端口、工作进程等设置"""
host: Required[str]
port: Required[int]
reload: bool
workers: int
access_log: bool
class Logging(TypedDict):
"""日志系统配置,定义日志级别、输出格式和文件管理策略"""
level: str
file: str
format: str
max_size: int
backup_count: int
class Cors(TypedDict, total=False):
"""跨域资源共享(CORS)配置"""
allow_origins: List[str]
allow_credentials: bool
allow_methods: List[str]
allow_headers: List[str]
class CustomMQTTTopic(TypedDict, total=False):
"""额外自定义 MQTT 订阅主题的路由配置"""
topic: Required[str] # 订阅主题,支持 +(单级)/ #(多级)通配符
worker: Required[str] # 目标 worker 类型
subject: str # 目标 Zenoh subject,支持 {1} {2}... 占位符;默认 "<worker>.<topic 最后一段>"
response_topic: str # 响应发布主题,支持占位符;默认 "<实际匹配主题>/response"
class MQTTConfig(TypedDict, total=False):
"""MQTT Bridge 配置(按环境区分)"""
enabled: bool
host: str
port: int
broker: dict
custom_topics: List[CustomMQTTTopic]
class ModuleSystem(TypedDict, total=False):
"""模块系统配置,定义模块加载类型和路径"""
type: str
path: List[str]
class FileSystem(TypedDict, total=False):
"""文件系统配置,定义存储类型和路径"""
type: str
path: str
class Env(TypedDict, total=False):
"""环境特定配置结构,用于开发和生产环境的差异化配置"""
debug: Required[bool]
server: Required[Server]
logging: Required[Logging]
cors: Cors
mqtt: MQTTConfig
module_system: ModuleSystem
file_system: FileSystem
class Scheduler(TypedDict):
"""任务调度器配置"""
job_store_path: str
class Cache(TypedDict):
"""缓存系统配置(chongming-cache 双后端:diskcache / redis)"""
backend: NotRequired[str]
cache_store_path: Required[str]
redis_host: NotRequired[str]
redis_port: NotRequired[int]
redis_db: NotRequired[int]
redis_password: NotRequired[str]
redis_prefix: NotRequired[str]
class Config(TypedDict):
"""完整配置结构,整合所有配置项,包含默认配置、数据库、调度器、缓存、安全和环境配置"""
default: Default
database: Database
scheduler: Scheduler
cache: Cache
security: Security
development: Env
production: Env
def load_config(config_path: Path = Path("config.toml")) -> Config:
"""
从TOML配置文件加载配置数据
Args:
config_path: 配置文件路径,默认为当前目录下的config.toml
Returns:
Config: 解析后的配置字典对象
Raises:
FileNotFoundError: 当配置文件不存在时抛出异常
"""
if not config_path.exists():
raise FileNotFoundError(f"配置文件不存在: {config_path}")
with open(config_path, "rb") as f:
return cast(Config, tomllib.load(f))
# 全局配置缓存变量,避免重复加载配置文件
_config: Optional[Config] = None
def get_config(config_path: str = "config.toml") -> Config:
"""
获取应用程序配置(带缓存机制)
首次调用时加载配置文件并缓存,后续调用直接返回缓存的配置对象
Args:
config_path: 配置文件路径,默认为config.toml
Returns:
Config: 配置字典对象
"""
global _config
if _config is None:
_config = load_config(Path(config_path))
return _config
def get_workers_config(config_path: str = "config/workers/config.toml") -> dict:
"""
获取Worker插件配置列表
Args:
config_path: Worker配置文件路径,默认为config/workers/config.toml
Returns:
list: Worker配置字典列表
"""
if not Path(config_path).exists():
raise FileNotFoundError(f"Worker配置文件不存在: {config_path}")
with open(config_path, "rb") as f:
return cast(dict, tomllib.load(f))
-537
View File
@@ -1,537 +0,0 @@
"""
构建脚本 - 将 chongming 应用打包为可执行文件
功能阶段:
1. 模块打包(module-bank): 加密打包 Python 模块
2. 资源准备: 创建 build 目录, 复制必要文件
3. 混淆与打包: pyarmor 混淆 + PyInstaller/Nuitka 打包
4. 数据库初始化: 创建表结构和默认数据
5. 前端构建: 编译 Vue 项目并打包为 VFS
用法:
python -m chongming.scripts.build [pyinstaller|nuitka]
"""
import platform
import shutil
import subprocess
import sys
import asyncio
import time
from dataclasses import dataclass, field
from pathlib import Path
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption
from sqlite_vfs.core import SQLiteVFS
from sqlite_vfs.folder_packer import FolderPacker
from chongming_config import get_config, Config
# ── 常量 ────────────────────────────────────────────────────────────────────
PYINSTALLER = "pyinstaller"
NUITKA = "nuitka"
MODES = (PYINSTALLER, NUITKA)
is_windows = platform.system() == "Windows"
# ── 构建结果数据结构 ─────────────────────────────────────────────────────────
@dataclass
class BuildStepResult:
"""单个构建步骤的执行结果"""
name: str # 步骤名称
success: bool # 是否成功
duration: float # 耗时(秒)
message: str = "" # 附加信息(如包大小)
@dataclass
class BuildReport:
"""构建总结报告"""
mode: str
start_time: float
end_time: float = 0.0
steps: list[BuildStepResult] = field(default_factory=list)
@property
def total_duration(self) -> float:
return self.end_time - self.start_time if self.end_time else 0.0
@property
def all_success(self) -> bool:
return all(s.success for s in self.steps)
def add_step(self, name: str, success: bool, duration: float, message: str = ""):
self.steps.append(BuildStepResult(name, success, duration, message))
def print_summary(self):
"""打印结构化的构建总结"""
sep = "=" * 60
print(f"\n{sep}")
print(" 🏗️ CHONGMING 构建报告")
print(f"{sep}")
print(f" 构建模式 : {self.mode}")
print(f" 总耗时 : {self.total_duration:.2f}s")
print(f" 构建状态 : {'✅ 成功' if self.all_success else '❌ 失败'}")
print(f"{sep}")
print(" 步骤明细:")
for i, step in enumerate(self.steps, 1):
icon = "✅" if step.success else "❌"
msg = f" ({step.message})" if step.message else ""
print(
f" {icon} [{i}/{len(self.steps)}] {step.name:<20s} {step.duration:>7.2f}s{msg}"
)
print(f"{sep}\n")
# ── 工具函数 ────────────────────────────────────────────────────────────────
def _run_cmd(
cmd: list[str],
cwd: str | None = None,
capture: bool = True,
shell: bool | None = None,
echo_on_error: bool = True,
) -> subprocess.CompletedProcess:
"""统一执行外部命令"""
if shell is None:
shell = is_windows
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=capture,
text=True,
shell=shell,
encoding="utf-8",
errors="replace",
)
# 命令失败时自动回显捕获的输出,便于定位失败原因
if echo_on_error and capture and result.returncode != 0:
_echo_captured_output(cmd, result)
return result
def _echo_captured_output(
cmd: list[str], result: subprocess.CompletedProcess
) -> None:
"""命令失败时回显被捕获的输出(截取末尾若干行,避免刷屏)"""
max_lines = 40
print(f"\n ── 命令执行失败 (exit={result.returncode}): {' '.join(cmd)} ──")
for stream_name, stream in (("stdout", result.stdout), ("stderr", result.stderr)):
if not stream or not stream.strip():
continue
lines = stream.rstrip("\n").splitlines()
tail = lines[-max_lines:]
header = f" [{stream_name}]"
if len(lines) > max_lines:
header += f" (共 {len(lines)} 行,显示末尾 {len(tail)} 行)"
print(header)
for line in tail:
print(f" {line}")
print(" ────────────────────────────────────────\n")
def _format_size(path: str | Path) -> str:
"""获取文件可读大小"""
size = Path(path).stat().st_size
for unit in ("B", "KB", "MB", "GB"):
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} TB"
def _ensure_dir(path: str | Path):
"""确保目录存在"""
Path(path).mkdir(parents=True, exist_ok=True)
def _resolve_mode() -> str:
"""解析构建模式"""
mode = PYINSTALLER
if len(sys.argv) >= 2:
mode = sys.argv[1].lower()
if mode not in MODES:
print(f" ❌ 不支持的构建模式: {mode},可选: {', '.join(MODES)}")
sys.exit(1)
return mode
# ── 阶段 1: 模块打包 (module-bank) ──────────────────────────────────────────
def _stage_pack_modules(config: Config) -> str:
"""
使用 module-bank 加密打包 Python 模块为 .mbank 文件。
返回打包过程中的提示信息。
"""
_ensure_dir("resources")
mbank_list = config.get("production", {}).get("module_system", {}).get("path", [])
if not mbank_list:
return "⚠️ 未配置 module_system.path,跳过模块打包"
messages = []
for output_path, source_dir, package_name in mbank_list:
key = Encryption.generate_key()
packer = PythonToSQLite(
output_path,
key,
pyarmor_obfuscate=True,
)
packer.pack_directory(source_dir, package_name)
packer.verify_package_structure()
packer.packer_key(key)
packer.delete_source_code(None)
size = _format_size(output_path)
messages.append(f"{Path(output_path).name} ({size})")
return " - " + "\n - ".join(messages)
def _stage_prepare_build_dir(config: Config, mode: str) -> str:
"""准备 build 目录并复制必要文件"""
build_path = Path("build")
if build_path.exists():
shutil.rmtree(str(build_path))
_ensure_dir("build")
# 复制 utils
utils_src = Path("public/utils")
if utils_src.exists():
shutil.copytree(utils_src, build_path / "utils")
# 复制 resources
resources_src = Path("resources")
if resources_src.exists():
shutil.copytree(resources_src, build_path / "resources")
resources_src = Path("config")
if resources_src.exists():
shutil.copytree(resources_src, build_path / "config")
# 复制核心文件
for f in ("main.py", "server.py", "config.toml"):
shutil.copy(f"public/{f}", f"build/{f}")
# 注入 JWT 密钥
jwt_key = Encryption.generate_key()
cfg_path = build_path / "config.toml"
content = cfg_path.read_text(encoding="utf-8")
content = content.replace("your-secret-key-change-in-production", jwt_key)
cfg_path.write_text(content, encoding="utf-8")
return "资源准备完成"
# ── 阶段 3: PyArmor 混淆 ───────────────────────────────────────────────────
def _stage_pyarmor_obfuscate() -> str:
"""对关键脚本执行 PyArmor 混淆"""
files = [
("main.py", "./"),
("server.py", "./"),
("utils/launch.py", "./utils"),
("utils/config.py", "./utils"),
]
failed = []
for filename, output in files:
result = _run_cmd(
["pyarmor", "gen", f"--output={output}", filename],
cwd="build",
)
if result.returncode != 0:
failed.append(filename)
if failed:
return f"❌ 混淆失败: {', '.join(failed)}"
return "✅ 所有文件混淆成功"
# ── 阶段 4: PyInstaller 打包 ────────────────────────────────────────────────
def _stage_pyinstaller(config: Config) -> tuple[str, str]:
"""执行 PyInstaller 打包,返回 (状态消息, 可执行文件路径)"""
base_imports = config.get("pyinstaller_build_import", {})
build_imports = list(base_imports.get("base", []))
build_imports.extend(
base_imports.get("windows", []) if is_windows else base_imports.get("linux", [])
)
icon = Path("../../public/chongming.ico")
cmd = [
sys.executable,
"-m",
"PyInstaller",
"--onefile",
"--noconfirm",
"--optimize",
"2",
"--name",
"chongming",
"--distpath",
".",
"--workpath",
"./build_temp",
"--specpath",
"./specs",
"--clean",
]
if not is_windows:
cmd.append("--strip")
if icon.exists():
cmd.extend(["--icon", str(icon)])
for imp in build_imports:
cmd.extend(["--hidden-import", imp])
cmd.append("--collect-all")
cmd.append("amqtt")
cmd.append("main.py")
result = _run_cmd(cmd, cwd="build", capture=True)
if result.returncode != 0:
return f"❌ PyInstaller 失败 (exit={result.returncode}),详情见上方输出", ""
exe_path = Path("build/chongming")
if is_windows:
exe_path = exe_path.with_suffix(".exe")
suffix = ".exe" if is_windows else ""
exe = Path("build") / f"chongming{suffix}"
if exe.exists():
size = _format_size(exe)
return f"✅ PyInstaller 打包成功 ({size})", str(exe)
return "✅ PyInstaller 打包完成 (但未找到可执行文件)", ""
# ── 阶段 5: Nuitka 打包 ────────────────────────────────────────────────────
def _stage_nuitka(config: Config) -> tuple[str, str]:
"""执行 Nuitka 打包,返回 (状态消息, 可执行文件路径)"""
base_imports = config.get("pyinstaller_build_import", {}).get("base", [])
extra = config.get("pyinstaller_build_import", {}).get(
"windows" if is_windows else "linux", []
)
packages = [
"module_bank",
"sqlite_vfs",
"fastapi",
"uvicorn",
"gunicorn",
"diskcache",
"chongming_cache",
"chongming_cache.backends",
"chongming_cache.backends.disk",
"apscheduler",
"tomli",
"typing_extensions",
]
cmd = [
sys.executable,
"-m",
"nuitka",
"--standalone",
"--onefile",
"--follow-imports",
"--static-libpython=yes",
"--remove-output",
"--assume-yes-for-downloads",
"--lto=yes",
"--pgo-c",
"--pgo-python",
"--disable-plugin=tk-inter",
"--enable-plugin=numpy",
"--output-dir=.",
"--output-filename=chongming",
"main.py",
"--jobs=4",
]
if shutil.which("upx"):
cmd.append("--enable-plugin=upx")
for pkg in packages:
cmd.append(f"--include-package={pkg}")
for mod in list(base_imports) + extra:
cmd.append(f"--include-module={mod}")
if is_windows:
icon = Path("../../public/chongming.ico")
if icon.exists():
cmd.append(f"--windows-icon-from-ico={icon}")
result = _run_cmd(cmd, cwd="build", capture=False)
if result.returncode != 0:
# 降级重试(移除可能不兼容的参数)
print(
f" ⚠️ Nuitka 首次编译失败 (exit={result.returncode}),"
f"尝试移除不兼容参数重试..."
)
fallback = [
arg for arg in cmd if arg not in ("--lto=yes", "--enable-plugin=upx")
]
_run_cmd(fallback, cwd="build", capture=False)
exe = Path("build/chongming")
if exe.exists():
# 后处理瘦身
if not is_windows:
subprocess.run(["strip", str(exe)], check=False)
if shutil.which("upx"):
subprocess.run(["upx", "--best", str(exe)], check=False)
size = _format_size(exe)
return f"✅ Nuitka 编译完成 ({size})", str(exe)
return "❌ Nuitka 编译失败 (未生成可执行文件)", ""
# ── 阶段 6: 清理临时文件 ────────────────────────────────────────────────────
def _stage_cleanup(mode: str) -> str:
"""清理构建过程中的临时文件"""
build = Path("build")
dirs_to_remove = [
"build_temp",
"specs",
"utils",
"pyarmor_runtime_000000",
"__pycache__",
]
files_to_remove = ["main.py", "server.py"]
for d in dirs_to_remove:
target = build / d
if target.exists():
shutil.rmtree(str(target), ignore_errors=True)
for f in files_to_remove:
target = build / f
if target.exists():
target.unlink(missing_ok=True)
return "临时文件已清理"
# ── 阶段 8: Vue 前端构建 ────────────────────────────────────────────────────
def _stage_build_vue() -> str:
"""构建 Vue 前端并打包为 SQLite VFS"""
dist_dir = "./src/chongming-web/dist"
output_db = "./build/static.svfs"
fs_name = "frontend"
# 1. npm build
result = _run_cmd(
["npm", "run", "build"],
cwd="./src/chongming-web",
capture=True,
)
if result.returncode != 0:
err = (result.stderr or "").strip()[:200]
return f"❌ Vue 构建失败: {err}"
# 2. 检查产物
if not Path(dist_dir).exists():
return "❌ Vue 构建成功但未找到 dist 目录"
# 3. 打包为 VFS
vfs = SQLiteVFS(output_db, compress=False)
packer = FolderPacker(sqlite_vfs=vfs, exclude_patterns=[], fs_name=fs_name)
try:
packer.pack_folder(dist_dir)
finally:
vfs.close()
size = _format_size(output_db)
return f"✅ Vue 打包完成 ({size})"
# ── 主流程 ──────────────────────────────────────────────────────────────────
async def main():
"""规范化构建主流程"""
mode = _resolve_mode()
report = BuildReport(mode=mode, start_time=time.time())
config = get_config()
async def run_step(name: str, fn, *args, **kwargs) -> str:
"""执行一个构建步骤并记录结果(支持同步和异步函数)"""
start = time.time()
try:
result = fn(*args, **kwargs)
if asyncio.iscoroutine(result):
msg = await result
else:
msg = result
elapsed = time.time() - start
report.add_step(name, success=True, duration=elapsed, message=msg or "")
print(f" ✅ {name:<25s} {elapsed:>7.2f}s {msg}")
return msg
except Exception as e:
elapsed = time.time() - start
err_msg = str(e).strip()[:200]
report.add_step(name, success=False, duration=elapsed, message=err_msg)
print(f" ❌ {name:<25s} {elapsed:>7.2f}s {err_msg}")
return ""
print("\n" + "=" * 60)
print(" 🏗️ CHONGMING 构建开始")
print(f" 模式: {mode} | 平台: {platform.system()}")
print("=" * 60 + "\n")
# 阶段 1: 模块打包
await run_step("模块打包(module-bank)", _stage_pack_modules, config)
# 阶段 2: 准备构建目录
await run_step("准备构建目录", _stage_prepare_build_dir, config, mode)
# 阶段 3+4: 混淆 + 打包
if mode == PYINSTALLER:
await run_step("PyArmor 混淆", _stage_pyarmor_obfuscate)
start = time.time()
try:
msg, exe_path = _stage_pyinstaller(config)
elapsed = time.time() - start
success = "❌" not in msg
report.add_step(
"PyInstaller 打包", success, elapsed, msg.replace("✅ PyInstaller ", "")
)
print(
f" {'✅' if success else '❌'} PyInstaller 打包{' ' * 12} {elapsed:>7.2f}s {msg}"
)
except Exception as e:
elapsed = time.time() - start
report.add_step("PyInstaller 打包", False, elapsed, str(e)[:200])
print(f" ❌ PyInstaller 打包 {elapsed:>7.2f}s {str(e)[:200]}")
else:
await run_step("Nuitka 编译", lambda: _stage_nuitka(config)[0])
# 阶段 5: 清理
await run_step("清理临时文件", _stage_cleanup, mode)
# 阶段 7: Vue 前端构建
await run_step("Vue 前端构建", _stage_build_vue)
# 总结
report.end_time = time.time()
report.print_summary()
if not report.all_success:
sys.exit(1)
def build():
"""构建入口"""
asyncio.run(main())
if __name__ == "__main__":
build()
-253
View File
@@ -1,253 +0,0 @@
"""
进程插件打包器 — 将 external 进程插件打包为可部署 zip
用法:
uv run build_plugin <plugin_name> [--output <dir>]
示例:
uv run build_plugin demo # 打包 workers/demo → dist/demo.zip
uv run build_plugin demo --output /tmp/pkg
产物布局(zip 根即容器工作目录,unzip 后直接 ./start.sh 启动):
workers/<name>/ 插件代码(模块 workers.<name>)
config/workers/<name>/ 插件注册 + 端点配置
config/zenoh_config.json5 Zenoh 会话配置
config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需)
pyproject.toml uv 依赖清单(chongming-worker + 插件扩展依赖)
start.sh 容器入口: uv sync + uv run worker_serve --type <name>
说明:
- 插件扩展依赖声明在 workers/<name>/requirements.txt(可选,每行一个包),
打包时自动合并进 pyproject.toml dependencies
- 根 config.toml 仅提取 [zenoh] 节重新生成,避免把网关侧配置(如 JWT 密钥)泄露进插件容器
"""
import argparse
import re
import shutil
import sys
import tomllib
from pathlib import Path
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
# chongming-worker 内网 git 源(与网关 pyproject.toml [tool.uv.sources] 保持一致)
CHONGMING_WORKER_GIT = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git"
PYPI_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
def _find_project_root() -> Path:
"""从本文件所在目录向上查找项目根(含 pyproject.toml 的目录)。"""
here = Path(__file__).resolve().parent
for parent in here.parents:
if (parent / "pyproject.toml").exists():
return parent
return here.parent # 兜底:脚本目录的父目录
ROOT = _find_project_root()
WORKERS_DIR = ROOT / "workers"
PLUGINS_CONFIG_DIR = ROOT / "config" / "workers"
ZENOH_CONFIG = ROOT / "config" / "zenoh_config.json5"
ROOT_CONFIG = ROOT / "config.toml"
# ============================================================
# 产物生成
# ============================================================
def _rel(path: Path) -> Path | str:
"""相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。"""
try:
return path.relative_to(ROOT)
except ValueError:
return path
def _read_plugin_conf(name: str) -> dict:
"""读取 config/workers/<name>/config.toml 完整内容(校验含 [workers] 节)。"""
path = PLUGINS_CONFIG_DIR / name / "config.toml"
with open(path, "rb") as f:
conf = tomllib.load(f)
if not conf.get("workers"):
raise ValueError(f"{path.relative_to(ROOT)} 缺少 [workers] 节")
return conf
def _gen_root_config_toml() -> str:
"""生成最小根 config.toml(仅 [zenoh] 节,worker 进程只读它)。"""
with open(ROOT_CONFIG, "rb") as f:
root_conf = tomllib.load(f)
json5_path = root_conf.get("zenoh", {}).get("json5_path", "config/zenoh_config.json5")
return f"""# 最小根配置(由 build_plugin 自动生成,仅含 worker 进程所需的 [zenoh] 节)
[zenoh]
json5_path = "{json5_path}"
"""
def _gen_pyproject_toml(name: str, version: str, extra_deps: list[str]) -> str:
"""生成插件 uv 依赖清单(chongming-worker + 插件扩展依赖)。"""
deps = [
"chongming-worker",
"orjson", # chongming-worker 运行时依赖(SDK metadata 未声明,容器内需显式安装)
*extra_deps,
]
deps_lines = "\n".join(f' "{d}",' for d in deps)
return f"""# {name} 进程插件依赖清单(由 build_plugin 自动生成)
[project]
name = "{name}-worker"
version = "{version}"
description = "{name} 进程插件(external 模式,独立容器部署)"
requires-python = ">=3.12"
dependencies = [
{deps_lines}
]
[project.scripts]
worker_serve = "chongming_worker.process:worker_serve"
[[tool.uv.index]]
url = "{PYPI_MIRROR}"
[tool.uv.sources]
chongming-worker = {{ git = "{CHONGMING_WORKER_GIT}" }}
"""
def _gen_start_sh(name: str) -> str:
"""生成容器入口脚本:uv 引导依赖 + 启动 worker 进程。"""
return f"""#!/usr/bin/env bash
# {name} 进程插件容器入口(由 build_plugin 自动生成)
# 要求: 镜像预装 uv(https://docs.astral.sh/uv/)
set -e
cd "$(dirname "$0")"
uv sync --no-dev
exec uv run worker_serve --type {name} "$@"
"""
def _read_extra_deps(name: str) -> list[str]:
"""读取 workers/<name>/requirements.txt(可选),每行一个依赖。"""
path = WORKERS_DIR / name / "requirements.txt"
if not path.exists():
return []
deps = []
for line in path.read_text(encoding="utf-8").splitlines():
dep = line.strip()
if dep and not dep.startswith("#"):
deps.append(dep)
return deps
# ============================================================
# 打包主逻辑
# ============================================================
def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path:
"""
打包进程插件为可部署 zip,返回产物路径。
失败(插件/配置缺失、契约不合法)抛 FileNotFoundError / ValueError。
"""
if not NAME_RE.match(name):
raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
workers_src = WORKERS_DIR / name
if not workers_src.is_dir():
raise FileNotFoundError(f"未找到插件代码目录: {_rel(workers_src)}/")
conf = _read_plugin_conf(name) # 缺配置在此抛 FileNotFoundError
workers = conf["workers"]
plugin_type = workers.get("type", name)
load_mode = workers.get("load_mode", "inline")
if load_mode != "external":
print(
f"⚠️ [{name}] load_mode = \"{load_mode}\":网关也会在进程内加载该插件,"
"独立进程将与网关内副本重复服务。进程插件请在 [workers] 节设置 "
"load_mode = \"external\"。"
)
version = str(conf.get("worker", {}).get("version", "0.1.0"))
output = Path(output_dir)
stage = output / name
if stage.exists():
shutil.rmtree(stage)
stage.mkdir(parents=True, exist_ok=True)
# ---- 1. 插件代码 ----
shutil.copytree(
workers_src,
stage / "workers" / name,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
)
# ---- 2. 插件配置 ----
shutil.copytree(
PLUGINS_CONFIG_DIR / name,
stage / "config" / "workers" / name,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
)
# ---- 3. Zenoh 会话配置 ----
if not ZENOH_CONFIG.exists():
raise FileNotFoundError(f"未找到 Zenoh 配置: {_rel(ZENOH_CONFIG)}")
shutil.copy(ZENOH_CONFIG, stage / "config" / "zenoh_config.json5")
# ---- 4. 生成最小根配置 + 依赖清单 + 启动脚本 ----
(stage / "config.toml").write_text(_gen_root_config_toml(), encoding="utf-8")
(stage / "pyproject.toml").write_text(
_gen_pyproject_toml(name, version, _read_extra_deps(name)), encoding="utf-8"
)
start_sh = stage / "start.sh"
start_sh.write_text(_gen_start_sh(plugin_type), encoding="utf-8")
start_sh.chmod(0o755)
# ---- 5. 压缩(zip 根即容器工作目录)----
zip_path = output / f"{name}.zip"
zip_path.unlink(missing_ok=True)
shutil.make_archive(str(output / name), "zip", root_dir=stage)
# 暂存目录保留便于排查,也可直接作为容器工作目录
return zip_path
def _format_size(path: Path) -> str:
size = path.stat().st_size
for unit in ("B", "KB", "MB", "GB"):
if size < 1024:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} TB"
def main() -> None:
parser = argparse.ArgumentParser(description="chongming 进程插件打包器")
parser.add_argument("name", help="插件名(如 demo)")
parser.add_argument("--output", default="dist", help="产物输出目录(默认 dist)")
args = parser.parse_args()
try:
zip_path = build_plugin_package(args.name, output_dir=args.output)
except (FileNotFoundError, ValueError) as e:
print(f"❌ {e}")
sys.exit(1)
print(f"✅ 打包完成: {zip_path} ({_format_size(zip_path)})")
print("📦 产物布局(zip 根即容器工作目录):")
print(f" workers/{args.name}/ 插件代码")
print(f" config/workers/{args.name}/ 插件配置")
print(" config/zenoh_config.json5 Zenoh 会话配置")
print(" config.toml 最小根配置(仅 [zenoh] 节)")
print(" pyproject.toml uv 依赖清单")
print(" start.sh 容器入口")
print(f"\n🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh")
def build_plugin():
"""CLI 入口(供 pyproject.toml [project.scripts] 调用)"""
main()
if __name__ == "__main__":
main()
-47
View File
@@ -1,47 +0,0 @@
import os
import sys
def generate_markdown_from_py_files(directory, output_file):
with open(output_file, "w", encoding="utf-8") as md_file:
for root, dirs, files in os.walk(directory):
# 排除 venv 目录
dirs[:] = [d for d in dirs if d != ".venv"]
dirs[:] = [d for d in dirs if d != ".vscode"]
dirs[:] = [d for d in dirs if d != "node_modules"]
dirs[:] = [d for d in dirs if d != "build"]
dirs[:] = [d for d in dirs if d != "dist"]
dirs[:] = [d for d in dirs if d != "chongming-web"]
for file in files:
if (
file.endswith(".py")
or file.endswith(".rs")
or file.endswith(".toml")
or file.endswith(".md")
or file.endswith(".css")
or file.endswith(".vue")
or file.endswith(".ts")
or file.endswith(".json")
or file.endswith(".html")
or file.endswith(".sh")
or file == "Dockerfile"
):
file_path = os.path.join(root, file)
if file == "output.md" or file == "package-lock.json":
continue
if "generated" in file_path:
continue
md_file.write(f"`{file_path}`\n")
md_file.write("```\n")
with open(file_path, "r", encoding="utf-8") as py_file:
md_file.write(py_file.read())
md_file.write("\n```\n\n")
if __name__ == "__main__":
# 指定目录和输出文件名
target_directory = sys.argv[1] # 替换为你的目标目录
output_markdown_file = "output.md" # 输出的 Markdown 文件名
generate_markdown_from_py_files(target_directory, output_markdown_file)
print(f"Markdown 文件已生成:{output_markdown_file}")
-353
View File
@@ -1,353 +0,0 @@
"""
模型生成脚本 — 从 config/workers/<plugin_name>/*.toml 生成 Pydantic 模型
用法:
uv run gen_models config/workers/example src/plugins/example
工作流:
1. 编写 config/workers/<plugin_name>/*.toml 配置文件
2. 执行 gen_models 生成 Pydantic 模型
3. 在业务代码中 import 生成的模型
功能:
1. 扫描配置目录下的所有 .toml 文件
2. 解析每个路由的 params 和 response_model
3. 生成 Pydantic 请求/响应模型
4. 写入 src/plugins/<plugin_name>/models/__init__.py 和 models/pydantic.py
"""
import sys
import json
from pathlib import Path
from typing import Any
def parse_type_str(type_str: str) -> str:
"""将 TOML 中的类型字符串映射为 Python 类型注解"""
mapping = {
"float": "float",
"int": "int",
"str": "str",
"bool": "bool",
"string": "str",
"integer": "int",
"number": "float",
"boolean": "bool",
"any": "Any",
}
if type_str.startswith("list[") and type_str.endswith("]"):
inner = type_str[5:-1]
inner_py = mapping.get(inner, inner)
return f"list[{inner_py}]"
if type_str == "dict":
return "dict[str, Any]"
if type_str.startswith("dict[") and type_str.endswith("]"):
inner = type_str[5:-1]
if "," in inner:
parts = [p.strip() for p in inner.split(",", 1)]
inner_py = mapping.get(parts[-1], parts[-1])
else:
inner_py = mapping.get(inner, inner)
return f"dict[str, {inner_py}]"
if type_str.startswith("Optional[") and type_str.endswith("]"):
inner = type_str[9:-1]
inner_py = mapping.get(inner, inner)
return f"Optional[{inner_py}]"
return mapping.get(type_str, type_str)
def parse_params(params: list[str]) -> list[dict[str, str]]:
"""解析 params 列表,如 ['a: float', 'b: float']"""
result = []
for p in params:
if ":" in p:
name, type_str = p.split(":", 1)
result.append({"name": name.strip(), "type": parse_type_str(type_str.strip())})
else:
result.append({"name": p.strip(), "type": "str"})
return result
def parse_response_model(response_model: dict[str, Any]) -> list[dict[str, Any]]:
"""解析 response_model 字典,返回字段列表"""
fields: list[dict[str, Any]] = []
for field_name, field_spec in response_model.items():
entry: dict[str, Any] = {"name": field_name}
if isinstance(field_spec, dict):
entry["type"] = parse_type_str(field_spec.get("type", "str"))
entry["required"] = field_spec.get("required", True)
if "default" in field_spec:
entry["default"] = field_spec["default"]
else:
entry["type"] = parse_type_str(str(field_spec))
entry["required"] = True
fields.append(entry)
return fields
def _model_name(base: str, method: str, suffix: str) -> str:
"""生成模型名: e.g. add + GET + Request -> AddGetRequest, add + MQTT + Request -> AddMqttRequest"""
method_title = "Mqtt" if method == "MQTT" else method.title()
return f"{base.title().replace('-', '').replace('_', '')}{method_title}{suffix}"
def generate_pydantic_code(
plugin_name: str,
routes: list[dict[str, Any]],
) -> str:
"""生成 pydantic.py 文件内容"""
# 收集全部字段类型,按需生成 typing 导入(避免未使用导入触发 lint)
type_strings: list[str] = []
has_optional_field = False
for route in routes:
for p in route.get("parsed_params", []):
type_strings.append(p.get("type", "str"))
for f in route.get("parsed_response", []):
type_strings.append(f.get("type", "str"))
# 非必填字段渲染为 Optional[...]
if not f.get("required", True):
has_optional_field = True
typing_imports = []
if has_optional_field or any("Optional" in t for t in type_strings):
typing_imports.append("Optional")
if any("Any" in t for t in type_strings):
typing_imports.append("Any")
lines = []
lines.append('"""')
lines.append(f"自动生成的 Pydantic 模型 — {plugin_name}")
lines.append(f"来源: config/workers/{plugin_name}/")
lines.append('"""')
lines.append("")
lines.append("from __future__ import annotations")
lines.append("")
if typing_imports:
lines.append(f"from typing import {', '.join(typing_imports)}")
lines.append("from pydantic import BaseModel, Field")
lines.append("")
seen_models: set[str] = set()
for route in routes:
route_name = route["name"]
method = route["method"]
summary = route.get("summary", "")
# --- Request Model ---
params = route.get("parsed_params", [])
if params:
model_name = _model_name(route_name, method, "Request")
if model_name not in seen_models:
seen_models.add(model_name)
lines.append(f"class {model_name}(BaseModel):")
lines.append(f' """{summary} — 请求体"""')
for p in params:
lines.append(f" {p['name']}: {p['type']} = Field(...)")
lines.append("")
# --- Response Model ---
response_fields = route.get("parsed_response", [])
if response_fields:
model_name = _model_name(route_name, method, "Response")
if model_name not in seen_models:
seen_models.add(model_name)
lines.append(f"class {model_name}(BaseModel):")
lines.append(f' """{summary} — 响应体"""')
for f in response_fields:
field_type = f["type"]
if f.get("required", True):
if "default" in f:
default_repr = (
json.dumps(f["default"])
if not isinstance(f["default"], str)
else f"'{f['default']}'"
)
lines.append(f" {f['name']}: {field_type} = Field(default={default_repr})")
else:
lines.append(f" {f['name']}: {field_type} = Field(...)")
else:
if "default" in f:
default_repr = (
json.dumps(f["default"])
if not isinstance(f["default"], str)
else f"'{f['default']}'"
)
lines.append(f" {f['name']}: Optional[{field_type}] = Field(default={default_repr})")
else:
lines.append(f" {f['name']}: Optional[{field_type}] = Field(default=None)")
lines.append("")
# --- Union type for all responses ---
response_models = [m for m in seen_models if m.endswith("Response")]
if response_models:
plugin_title = plugin_name.title().replace("-", "").replace("_", "")
lines.append("")
lines.append("# 所有响应模型的联合类型")
lines.append(f"{plugin_title}Response = " + " | ".join(response_models))
lines.append("")
return "\n".join(lines)
def generate_init_code(
plugin_name: str,
routes: list[dict[str, Any]],
) -> str:
"""生成 __init__.py 文件内容"""
lines = []
lines.append('"""')
lines.append(f"模型导出 — {plugin_name}")
lines.append("自动生成,请勿手动修改")
lines.append('"""')
lines.append("")
seen_models: set[str] = set()
for route in routes:
rname = route["name"]
method = route["method"]
if route.get("parsed_params"):
seen_models.add(_model_name(rname, method, "Request"))
if route.get("parsed_response"):
seen_models.add(_model_name(rname, method, "Response"))
response_models = [m for m in seen_models if m.endswith("Response")]
if response_models:
plugin_title = plugin_name.title().replace("-", "").replace("_", "")
seen_models.add(f"{plugin_title}Response")
for model_name in sorted(seen_models):
lines.append(f"from .pydantic import {model_name}")
lines.append("")
lines.append("__all__ = [")
for model_name in sorted(seen_models):
lines.append(f' "{model_name}",')
lines.append("]")
lines.append("")
return "\n".join(lines)
def scan_config_dir(config_dir: Path) -> list[dict[str, Any]]:
"""扫描配置目录,解析所有 .toml 文件中的路由定义"""
import tomli
routes = []
toml_files = sorted(config_dir.glob("*.toml"))
group_config = {}
config_file = config_dir / "config.toml"
if config_file.exists():
with open(config_file, "rb") as f:
group_config = tomli.load(f)
defaults = group_config.get("worker", {}).get("defaults", {})
router_prefix = (
group_config.get("worker", {})
.get("registration", {})
.get("router_prefix", "")
)
for toml_file in toml_files:
if toml_file.name == "config.toml":
continue
with open(toml_file, "rb") as f:
data = tomli.load(f)
route_name = toml_file.stem
for method in ("GET", "POST", "PUT", "DELETE", "PATCH", "MQTT"):
if method not in data:
continue
entry = data[method]
params_raw = entry.get("params", [])
response_raw = entry.get("response_model", {})
route = {
"name": route_name,
"method": method,
"summary": entry.get("summary", ""),
"docstring": entry.get("docstring", ""),
"internal": entry.get("internal", defaults.get("internal", False)),
"params": params_raw,
"response_model": response_raw,
"parsed_params": parse_params(params_raw),
"parsed_response": parse_response_model(response_raw),
"path": entry.get("path", f"{router_prefix}/{route_name}"),
"timeout": entry.get("timeout", defaults.get("timeout", 3.0)),
"auth_required": entry.get("auth_required", defaults.get("auth_required", False)),
}
routes.append(route)
return routes
def main():
if len(sys.argv) < 3:
print("用法: uv run gen_models <config_dir> <plugin_dir>")
print("示例:")
print(" uv run gen_models config/workers/example src/plugins/example")
sys.exit(1)
config_dir = Path(sys.argv[1])
plugin_dir = Path(sys.argv[2])
plugin_name = plugin_dir.name
if not config_dir.exists():
print(f"❌ 配置目录不存在: {config_dir}")
sys.exit(1)
if not plugin_dir.exists():
print(f"❌ 插件目录不存在: {plugin_dir}")
sys.exit(1)
print(f"🔍 扫描配置目录: {config_dir}")
routes = scan_config_dir(config_dir)
print(f" 发现 {len(routes)} 个路由定义")
if not routes:
print("⚠️ 未找到路由定义,跳过模型生成")
return
models_dir = plugin_dir / "models"
models_dir.mkdir(parents=True, exist_ok=True)
pydantic_code = generate_pydantic_code(plugin_name, routes)
pydantic_file = models_dir / "pydantic.py"
pydantic_file.write_text(pydantic_code, encoding="utf-8")
print(f"✅ 生成: {pydantic_file}")
init_code = generate_init_code(plugin_name, routes)
init_file = models_dir / "__init__.py"
init_file.write_text(init_code, encoding="utf-8")
print(f"✅ 生成: {init_file}")
print("\n📋 生成的模型:")
for route in routes:
method = route["method"]
name = route["name"]
params = route.get("parsed_params", [])
response = route.get("parsed_response", [])
if params:
req_model = _model_name(name, method, "Request")
param_strs = [f"{p['name']}: {p['type']}" for p in params]
print(f" 📥 {req_model}: {', '.join(param_strs)}")
if response:
resp_model = _model_name(name, method, "Response")
field_strs = [f"{f['name']}: {f['type']}" for f in response]
print(f" 📤 {resp_model}: {', '.join(field_strs)}")
def gen_models():
"""CLI 入口"""
main()
if __name__ == "__main__":
main()
-327
View File
@@ -1,327 +0,0 @@
"""
插件脚手架生成器 — 生成风格统一的 Worker 插件模板
用法:
uv run gen-plugin <name> [operation ...]
示例:
# 生成插件骨架(含一个示例操作):
uv run gen-plugin myplugin
# 指定初始操作:
uv run gen-plugin myplugin analyze count
# 为已存在的插件增量添加操作:
uv run gen-plugin myplugin add
工作流:
1. uv run gen-plugin myplugin # 生成 config + src 骨架(统一风格)
2. uv run gen_models config/workers/myplugin src/plugins/myplugin
# 生成 Pydantic 模型
3. 编辑 src/plugins/myplugin/router/<operation>.py 实现业务逻辑
4. 编辑 src/plugins/myplugin/__init__.py 的 on_init / on_cleanup
5. uv run serve # 启动验证
生成内容:
config/workers/<name>/config.toml Worker 注册 + 运行时配置
config/workers/<name>/<op>.toml 每个操作的 RPC 端点定义
src/plugins/<name>/__init__.py 基于 Plugin 基类的统一入口
src/plugins/<name>/global_var.py 白名单版全局变量容器
src/plugins/<name>/router/__init__.py 端点模块聚合导出
src/plugins/<name>/router/<op>.py 每个操作的 RPC 实现骨架
src/plugins/<name>/models/ 由 gen_models 生成(见步骤 2)
"""
import re
import sys
from pathlib import Path
def _find_project_root() -> Path:
"""从本文件所在目录向上查找项目根(含 pyproject.toml 的目录)。"""
here = Path(__file__).resolve().parent
for parent in here.parents:
if (parent / "pyproject.toml").exists():
return parent
return here.parent # 兜底:脚本目录的父目录
ROOT = _find_project_root()
CONFIG_DIR = ROOT / "config" / "workers"
PLUGIN_DIR = ROOT / "src" / "plugins"
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
# ============================================================
# 模板
# ============================================================
def snake_to_camel(name: str) -> str:
"""snake_case -> CamelCase(如 analyze_media -> AnalyzeMedia)"""
return "".join(part.title() for part in name.split("_"))
def _gen_config_toml(name: str) -> str:
return f'''# {name} Worker 插件配置
[worker]
[worker.defaults]
timeout = 30
cacheable = false
[worker.registration]
router_prefix = "/{name}"
# Worker 注册(app lifespan 据此发现并加载插件)
[workers]
type = "{name}"
module = "plugins.{name}"
factory = "create_workers"
cleanup = "cleanup"
count = 1
# 运行时配置(展平后传给 create_workers 的 config 参数)
[workers.config]
data_dir = "./data"
'''
def _gen_operation_toml(name: str, operation: str) -> str:
return f'''# {name}.{operation} — {operation} 操作
# 修改后需重新运行: uv run gen_models config/workers/{name} src/plugins/{name}
[GET]
cacheable = true
summary = "{operation} 操作描述"
params = ["value: str"]
[GET.response_model]
result = {{ type = "str", required = true }}
'''
def _gen_plugin_init(name: str) -> str:
cls = snake_to_camel(name)
return f'''"""{name.capitalize()} Worker 插件
统一风格模板 — 基于 chongming_worker.plugin.Plugin 基类。
工作流:
1. 编辑 config/workers/{name}/*.toml 定义 RPC 端点
2. uv run gen_models config/workers/{name} src/plugins/{name}
3. 在 router/<操作>.py 中实现业务逻辑
4. 在 on_init / on_cleanup 中初始化 / 清理资源
"""
import logging
from chongming_worker.plugin import Plugin
from . import router # noqa: F401 触发 @rpc_method 注册
# from .routes import router # 可选: 自定义 FastAPI 路由(网关自动发现)
logger = logging.getLogger(__name__)
class {cls}Plugin(Plugin):
"""{name} 插件 — 统一生命周期。"""
type = "{name}"
db_name = "{name}.db" # 自动在 data_dir 下建 SQLite 库(不需要则置 None)
auto_bind = True # 首次初始化时自动 auto_bind_models(self.type)
async def on_init(self):
"""首次 create_workers 时调用一次:初始化资源、启动后台任务。
数据库已由基类 _bootstrap 自动初始化(self.db)。
"""
self.set_var("db", self.db)
# 需要后台任务时:
# self.spawn_task(self._background_loop())
logger.info("[{name}] 初始化完成")
async def on_cleanup(self):
"""cleanup 时调用:关闭自定义客户端等(后台任务与数据库由基类处理)。"""
logger.info("[{name}] 清理完成")
# 暴露给 app lifespan 的模块级入口(bound method,签名与旧写法完全一致)
_plugin = {cls}Plugin()
create_workers = _plugin.create_workers
cleanup = _plugin.cleanup
'''
def _gen_global_var(name: str) -> str:
return f'''"""{name.capitalize()} 插件全局运行时状态 — 统一容器
约定:
- 模块只做"声明默认值",不产生任何导入期副作用
- 所有值统一在 Plugin.on_init 中通过 set_global_var 显式写入
- set_global_var 仅允许写入 _ALLOWED 白名单,拼写错误会立即抛错
- require() 用于取"必须已初始化"的值,未初始化时抛清晰错误
"""
from typing import Any
from chongming_database import Database
# 允许写入的全局变量白名单(防止拼写错误静默创建无用的新变量)
_ALLOWED = frozenset({{"db"}})
# 显式声明所有运行时状态(默认 None,运行时由 on_init 写入)
db: Database | None = None
def set_global_var(name: str, value: Any) -> None:
"""白名单校验版 setter — 设置模块级运行时状态。"""
if name not in _ALLOWED:
raise KeyError(
f"global_var: 未知变量 '{{name}}'(允许: {{sorted(_ALLOWED)}})"
)
globals()[name] = value
def require(name: str) -> Any:
"""获取必须已初始化的全局变量,未初始化时抛清晰错误。"""
if name not in _ALLOWED:
raise KeyError(f"global_var: 未知变量 '{{name}}'(允许: {{sorted(_ALLOWED)}})")
value = globals().get(name)
if value is None:
raise RuntimeError(
f"global_var: '{{name}}' 未初始化 —— 请确认插件 create_workers 已执行且配置正确"
)
return value
__all__ = sorted(_ALLOWED) + ["set_global_var", "require"]
'''
def _gen_router_init(name: str, operations: list[str]) -> str:
lines = [f'"""{name.capitalize()} RPC 端点"""', ""]
for op in operations:
lines.append(f"from . import {op}")
lines.append("")
lines.append("__all__ = " + repr(operations))
lines.append("")
return "\n".join(lines)
def _gen_router_operation(name: str, operation: str) -> str:
op_camel = snake_to_camel(operation)
return f'''"""RPC 端点: {name}.{operation}
模型由 gen_models 生成(编辑 config/workers/{name}/{operation}.toml 后重新生成)。
"""
from chongming_worker.decorators import rpc_method
from ..models.pydantic import {op_camel}GetRequest, {op_camel}GetResponse
@rpc_method(
subject="{name}.{operation}",
method="GET",
summary="{operation} 操作",
response_model={op_camel}GetResponse,
)
async def {operation}(request: {op_camel}GetRequest) -> {op_camel}GetResponse:
"""业务逻辑实现。"""
return {op_camel}GetResponse(result=f"hello: {{request.value}}")
'''
# ============================================================
# 主逻辑
# ============================================================
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists():
print(f"⏭️ 跳过(已存在): {path.relative_to(ROOT)}")
return
path.write_text(content, encoding="utf-8")
print(f"✅ 生成: {path.relative_to(ROOT)}")
def _existing_operations(plugin_dir: Path) -> list[str]:
"""扫描 router/ 下已有的操作文件(排除 __init__.py)。"""
router_dir = plugin_dir / "router"
if not router_dir.exists():
return []
ops = []
for f in sorted(router_dir.glob("*.py")):
if f.name == "__init__.py":
continue
ops.append(f.stem)
return ops
def generate_plugin(name: str, operations: list[str]) -> int:
"""生成插件骨架。返回 0 表示成功,1 表示失败。"""
if not NAME_RE.match(name):
print(f"❌ 插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
return 1
config_dir = CONFIG_DIR / name
plugin_dir = PLUGIN_DIR / name
is_new = not plugin_dir.exists()
# ---- 首次创建: 完整骨架 ----
if is_new:
_write(config_dir / "config.toml", _gen_config_toml(name))
for op in operations:
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op))
_write(plugin_dir / "__init__.py", _gen_plugin_init(name))
_write(plugin_dir / "global_var.py", _gen_global_var(name))
(plugin_dir / "models" / "__init__.py").parent.mkdir(parents=True, exist_ok=True)
(plugin_dir / "models" / "__init__.py").write_text(
'"""模型导出 — 由 gen_models 生成,请勿手动修改"""\n',
encoding="utf-8",
)
print(f"✅ 生成: {plugin_dir.relative_to(ROOT)}/models/__init__.py")
# ---- 增量: 仅补 operations(已存在文件不覆盖)----
existing = _existing_operations(plugin_dir)
added = [op for op in operations if op not in existing]
if added:
for op in added:
_write(config_dir / f"{op}.toml", _gen_operation_toml(name, op))
_write(plugin_dir / "router" / f"{op}.py", _gen_router_operation(name, op))
# ---- 重写 router/__init__.py(聚合所有操作)----
all_ops = _existing_operations(plugin_dir)
if all_ops:
init_file = plugin_dir / "router" / "__init__.py"
init_file.parent.mkdir(parents=True, exist_ok=True)
init_file.write_text(_gen_router_init(name, all_ops), encoding="utf-8")
print(f"✅ 更新: {init_file.relative_to(ROOT)}")
print(f"\n📋 插件 '{name}' 已就绪,下一步:")
print(f" 1. uv run gen_models config/workers/{name} src/plugins/{name}")
print(f" 2. 编辑 src/plugins/{name}/router/ 实现业务逻辑")
print(" 3. uv run serve")
return 0
def main() -> None:
args = sys.argv[1:]
if not args:
print("用法: uv run gen_plugin <name> [operation ...]")
print("示例:")
print(" uv run gen_plugin myplugin")
print(" uv run gen_plugin myplugin analyze count")
print(" uv run gen_plugin myplugin add # 增量添加操作")
sys.exit(1)
name = args[0]
operations = args[1:] or ["ping"]
sys.exit(generate_plugin(name, operations))
def gen_plugin():
"""CLI 入口(供 pyproject.toml [project.scripts] 调用)"""
main()
if __name__ == "__main__":
main()
+2 -2
View File
@@ -16,9 +16,9 @@ import zipfile
from pathlib import Path
SRC = Path(__file__).parent
sys.path.insert(0, str(SRC / "src"))
sys.path.insert(0, str(SRC / "utils" / "cli" / "src"))
from chongming.scripts import build_plugin as bp # noqa: E402
from chongming_cli import build_plugin as bp # noqa: E402
failures = []
Generated
+47 -85
View File
@@ -83,18 +83,6 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
]
[[package]]
name = "apscheduler"
version = "3.11.3"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "tzlocal" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/6b/eeff360196bb20b312c9e762a820fd1b2c6d809466c755ef57863478e454/apscheduler-3.11.3.tar.gz", hash = "sha256:cd2fcc9330039a81a5893472ad49facf23a6d5604cbe1d918c835c6de7834d5a", size = 110312, upload-time = "2026-06-28T19:39:22.493Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/c9/8638db32514dbb9157b3d82680c6faea89283523edf9ed2415ea3884f2ae/apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30", size = 66024, upload-time = "2026-06-28T19:39:20.982Z" },
]
[[package]]
name = "argon2-cffi"
version = "25.1.0"
@@ -434,6 +422,17 @@ dependencies = [
{ name = "redis" },
]
[[package]]
name = "chongming-cli"
version = "0.1.0"
source = { git = "http://124.71.68.6:3000/chongming/chongming-utils-cli.git#0bf60021cbeb32610dce232eaf0e25be5d8d8b29" }
dependencies = [
{ name = "chongming-config" },
{ name = "module-bank" },
{ name = "sqlite-vfs" },
{ name = "tomli" },
]
[[package]]
name = "chongming-config"
version = "0.1.0"
@@ -501,7 +500,6 @@ source = { editable = "." }
dependencies = [
{ name = "aiofiles" },
{ name = "amqtt" },
{ name = "apscheduler" },
{ name = "bytedance" },
{ name = "chongming-cache" },
{ name = "chongming-config" },
@@ -529,6 +527,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "chongming-cli" },
{ name = "nuitka" },
{ name = "pyarmor" },
{ name = "pyinstaller" },
@@ -539,7 +538,6 @@ dev = [
requires-dist = [
{ name = "aiofiles", specifier = ">=25.1.0" },
{ name = "amqtt", specifier = ">=0.11.3" },
{ name = "apscheduler" },
{ name = "bytedance", git = "http://124.71.68.6:3000/chongming/bytedance.git" },
{ name = "chongming-cache", git = "http://124.71.68.6:3000/chongming/chongming-utils-cache.git" },
{ name = "chongming-config", git = "http://124.71.68.6:3000/chongming/chongming-utils-config.git" },
@@ -567,6 +565,7 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "chongming-cli", git = "http://124.71.68.6:3000/chongming/chongming-utils-cli.git" },
{ name = "nuitka" },
{ name = "pyarmor" },
{ name = "pyinstaller" },
@@ -760,14 +759,11 @@ wheels = [
[[package]]
name = "gunicorn"
version = "26.0.0"
version = "26.1.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/b8/ec4ba3f6cace4091c34e27478b576bb80f2f06fab80fd42c0ecc785b308f/gunicorn-26.1.0.tar.gz", hash = "sha256:1413d777bf99d31ebeb08acd354b01f1ecc44db0aa7b811ae7b86c669232e4f7", size = 755923, upload-time = "2026-08-18T11:49:39.438Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/dc/7a55fc605543fd5cb11c003fbbb21a1911d5e88a582cce6c5e063bf5c176/gunicorn-26.1.0-py3-none-any.whl", hash = "sha256:9f45bcddec5e9dc7a25a3bdccb0c6832f11fd5d4739b1ee36c8d2fec25f1dc86", size = 216237, upload-time = "2026-08-18T11:49:38.001Z" },
]
[[package]]
@@ -794,15 +790,15 @@ wheels = [
[[package]]
name = "httpcore2"
version = "2.10.0"
version = "2.12.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "h11" },
{ name = "truststore" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/83/a896fc59940fc5a6e2aff3a4be1d92fa890112936803b331cae75a993c34/httpcore2-2.10.0.tar.gz", hash = "sha256:13c0cc3d1919d4f28457f60cd2c2abe04113a8af184ccf1142811beba936f9dc", size = 67427, upload-time = "2026-08-09T09:11:32.123Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e5/4f/d149104195a35e2853a2fc203a8e3477747e58c80e17dda686dace174383/httpcore2-2.10.0-py3-none-any.whl", hash = "sha256:7df06cfb34070cae4f7c89be69dc1095eca138e9704ceffb98d25c1912ab6f01", size = 83000, upload-time = "2026-08-09T09:11:29.555Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
]
[[package]]
@@ -822,7 +818,7 @@ wheels = [
[[package]]
name = "httpx2"
version = "2.10.0"
version = "2.12.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
@@ -832,9 +828,9 @@ dependencies = [
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bd/3d/f9a8c07a3884f3e5b26205e8436a18b3af61c5d53192c3bea235574dbbec/httpx2-2.10.0.tar.gz", hash = "sha256:8741d7329fe2c7885fc9ceb61c8217acfb87a85f75723714b89ebf7ad7196338", size = 98749, upload-time = "2026-08-09T09:11:33.24Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/6d/a637d52449d98a6892d9a4dc0262587afdb6a66f201871842dce5a97b1c1/httpx2-2.10.0-py3-none-any.whl", hash = "sha256:5e3194a432701e1cc6f69a8b1b2fa199ef907013fede8d9a09a2c5b7b8141a18", size = 94355, upload-time = "2026-08-09T09:11:30.882Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" },
]
[[package]]
@@ -848,11 +844,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.18"
version = "3.19"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
]
[[package]]
@@ -1002,7 +998,7 @@ sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/d8/bd
[[package]]
name = "openai"
version = "3.1.0"
version = "3.3.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "anyio" },
@@ -1011,12 +1007,11 @@ dependencies = [
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/9b/d45911bf9abfc5a754d800d79fe56e4dcf6e7b679d6ff4b7e9689b56bc02/openai-3.1.0.tar.gz", hash = "sha256:3ae7190da63f718727f9c525740d3f713e85553d2bf1d0cc9247346bd9063a4d", size = 1131016, upload-time = "2026-08-14T23:49:46.325Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e6/6c/670271205379061112bf5ed900b78a68e21bf11733de829772c5322f92aa/openai-3.3.0.tar.gz", hash = "sha256:28f904a9fbff15288e9dd67bc0f7020d4f576d37786383a480db02fe5d8139d3", size = 1166178, upload-time = "2026-08-18T21:17:53.419Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d3/45/84b9e909968c0f4c3112a828bba9aa8be6a5ee322fcb3d781145ee53d88e/openai-3.1.0-py3-none-any.whl", hash = "sha256:f06d5a5c8d06a0aead3a67d33fe5a3c3c31540a0f7a8017b02ba8cd99bc5c736", size = 1670762, upload-time = "2026-08-14T23:49:43.998Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/83/d44980712bf51dddaca090cd772b3102e75377ddac877eaa4b2cb58d886a/openai-3.3.0-py3-none-any.whl", hash = "sha256:ded6b2112e6d299c7a2573ff6f165dc92fb64ceaa4d7daa42345f091157bd373", size = 1690265, upload-time = "2026-08-18T21:17:51.351Z" },
]
[[package]]
@@ -1316,16 +1311,16 @@ wheels = [
[[package]]
name = "pygments"
version = "2.20.0"
version = "2.21.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pyinstaller"
version = "6.22.1"
version = "6.22.2"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "altgraph" },
@@ -1336,19 +1331,19 @@ dependencies = [
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/28/11937d2d139139d5645cbaf905f7dcaa9c4929bd7b740ad9157af730fd39/pyinstaller-6.22.1.tar.gz", hash = "sha256:4e7ed495fccb9974d47cf72ef8cffc92afa05d60bc265c5585a68f3d229ca8d1", size = 4093778, upload-time = "2026-08-15T14:49:48.622Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/2b/836d9def811c02522e0921d8b8cdf0c16b0545a216e97e71041758057859/pyinstaller-6.22.2.tar.gz", hash = "sha256:89b65a3ad07d9dd5832253e37bc45f31872d10d7f9d5c9fd0fdd6088a83829dd", size = 4092631, upload-time = "2026-08-17T20:53:22.231Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/51/82e66bc31b42f4ccb9bfb8a54c7b63e586a8f4668d1b5569c0ebf7810cf3/pyinstaller-6.22.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d519a5549bf560407a9cffa8547f278e79c1093dc1cade6d9658c67b650d66c4", size = 1062734, upload-time = "2026-08-15T14:48:43.181Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/ef/7bcf8c48fbd413f5d83e618c376029625b1db5af8e60b198dedf17af6b9c/pyinstaller-6.22.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f257d6329d90def6b96f3ad9b601f4e10e549017d2ad09e730f4a9741b42138", size = 755696, upload-time = "2026-08-15T14:48:47.871Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0d/92/3d425e8342b57ac0e856e5abba8d52a4c10227d8d364d342e9799a36a855/pyinstaller-6.22.1-py3-none-manylinux2014_i686.whl", hash = "sha256:1df884760a6efc4cc9bdcd48899f1382c072ab499bb16ed0b03606f8f10380c6", size = 768870, upload-time = "2026-08-15T14:48:52.391Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/ab/49a3ad7987e68215f47acbb68352d519cb93934212d1f7d1d2f9ad49afe6/pyinstaller-6.22.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:bc964609e73f32f79b4c967d1c29a05f913629fcf54b3752ec6ec82bf17cffdd", size = 767445, upload-time = "2026-08-15T14:48:56.481Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3b/79/ecf886650e5caafd1958f07913bf23838a5188f9429599d125ac0e837fdc/pyinstaller-6.22.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:3b654721a9fa13abdd8a3a8e266debf750a6f50115d17addc54e3269dc944e75", size = 762114, upload-time = "2026-08-15T14:49:00.881Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/20/0efa9067556893788e59cfe077edbd0ef3af95d06ce020b42795697d9afc/pyinstaller-6.22.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:10d009ac9ddf17b57dbfe22a453363a02be4cb0d5bd8a119ba59ef530b936296", size = 762173, upload-time = "2026-08-15T14:49:05.014Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/0d/0490244f31101e671d75ef0499dcd079d6ea79a5242b1b33cca32606c16a/pyinstaller-6.22.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:ff3c51c1a3b793d1917f75d0f5f31ef8bd889c0accc8478d604bbcc363fcb74b", size = 761673, upload-time = "2026-08-15T14:49:09.022Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a1/ec/f4ca5f2dd787a445d5b57010ad74a83acfd0fff9d6ef40984b05a995a28f/pyinstaller-6.22.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:bf5f4634b18add9381caa73bd1656a1f0736839aa41ca750fbd511116d5ae2ca", size = 761052, upload-time = "2026-08-15T14:49:13.084Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/9e/b53c56c5ace1a5cf8f780067c9f10036cc7f184792e4661a06f68087538f/pyinstaller-6.22.1-py3-none-win32.whl", hash = "sha256:c5d7859b59c17f5c28973eb683e2236abb7a6eed1a7f354dc7f65ee3a7dff8ab", size = 1345562, upload-time = "2026-08-15T14:49:19.058Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/77/894e6815f61022621661a32806ea6e41935ab6171df0408fc4502a9b772e/pyinstaller-6.22.1-py3-none-win_amd64.whl", hash = "sha256:9fe5d028023073b63c3e396e145c68a9c3382ab59889c749c4c248126d313945", size = 1406578, upload-time = "2026-08-15T14:49:25.54Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/79/d3/863ba7ee499664a367d5095b11d5565f8576b87ff873b81a85fc4eb95d2a/pyinstaller-6.22.1-py3-none-win_arm64.whl", hash = "sha256:bb7a405ef0fbea9b20b7312484d2766d2aa157026a0406a8e7ce63a573748eae", size = 1354948, upload-time = "2026-08-15T14:49:31.935Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/39/08cd53632276de70426e7c273820277a48253fee397b4048301ec03c3566/pyinstaller-6.22.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ebd1b1ca932d7cf25d7366ce691aaf79a5ff9425811ed7328b5116e4471b6d6d", size = 1062734, upload-time = "2026-08-17T20:52:17.635Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/22/2d865896782cbb41e2388c7314207c17a98acdcc1b8e5eef668873505c9f/pyinstaller-6.22.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f5ccb847451df4207bce18bf53a57b124c9bb4e7e4bad08c5ecc627bcf00b28c", size = 755697, upload-time = "2026-08-17T20:52:21.676Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/2b/6c11e4d5a76e716ea68da1946ab706a4bdf6d8e68b0e1dea314366d046a0/pyinstaller-6.22.2-py3-none-manylinux2014_i686.whl", hash = "sha256:becb47ad78272bede87acf2ed830d7545a8f65ab11d06a68fe2b99ba1afaac6d", size = 768870, upload-time = "2026-08-17T20:52:25.511Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cf/e2/dbfea6a58b68acf644f7193b11d570476d6ffaed57381b6d1dd9e898a971/pyinstaller-6.22.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:06d7b3827a8049db4a2d47e3ec4ae2f69a1041577d8833b8f169745cde573ab4", size = 767445, upload-time = "2026-08-17T20:52:29.629Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/db/f24a21af2f87ce1df4e07fdad95eec65ef9f284267a7c1e5eeea40f49aa4/pyinstaller-6.22.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:7bee432404eca5dc3ef37c36811c266561b03988f6d3e70ab0fa5352dd5de9e4", size = 762113, upload-time = "2026-08-17T20:52:34.025Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/07/b304ff3f5f8333778065e3658b604b0e108cd934b920f3fbab825a0dd5b7/pyinstaller-6.22.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9622686ecc5d5fa492fe6cde29d47df9dd41138cff8177be9f901ca3260f2096", size = 762173, upload-time = "2026-08-17T20:52:38.477Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/9b/0af69d93dfad2e3d590a5de5828d5bcd903747c0224bd9560ab476904dfe/pyinstaller-6.22.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:0260eaad6be3f6fbc1affffe6dc7b8e5b636dbca51b463224daba971610b6fc1", size = 761674, upload-time = "2026-08-17T20:52:42.646Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d3/4e/28b6094dcbd1e1bbb868daf4f8752999a20c1020ab64569232be42af2be8/pyinstaller-6.22.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c4b14caad38c1f3df8e9bf5276fc265e27fe8b4180cab4a37864288427bc0", size = 761053, upload-time = "2026-08-17T20:52:46.594Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ba/2c/f91b63fd01422111ac04e3b9bf61c039da5a3292acb4fe5248905f0be688/pyinstaller-6.22.2-py3-none-win32.whl", hash = "sha256:9a078877caa3920558a3242d0a7019fe5b825cff4b65ed380c7d1e1bd200ddd9", size = 1344474, upload-time = "2026-08-17T20:52:53.108Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/53/8ba1d0f6159b490f700eac6161a4be5f0d4672608a6dae9fd73679f183ee/pyinstaller-6.22.2-py3-none-win_amd64.whl", hash = "sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7", size = 1405725, upload-time = "2026-08-17T20:52:59.667Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/1b/9a3062cc34c939f694d4262c4754681f6ffe853c21c124209ad2d08b8e84/pyinstaller-6.22.2-py3-none-win_arm64.whl", hash = "sha256:afb6f9a95d19b6dcd3a7decc40d9adb6ba9c4f8802ddd6c972dfb552953f384e", size = 1353806, upload-time = "2026-08-17T20:53:06.222Z" },
]
[[package]]
@@ -1674,18 +1669,6 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
name = "tqdm"
version = "4.70.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
]
[[package]]
name = "transitions"
version = "0.9.3"
@@ -1743,27 +1726,6 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
]
[[package]]
name = "tzdata"
version = "2026.3"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
]
[[package]]
name = "tzlocal"
version = "5.4.4"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
@@ -1775,15 +1737,15 @@ wheels = [
[[package]]
name = "uvicorn"
version = "0.52.3"
version = "0.52.4"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" },
]
[[package]]