From 41ae36f34d8497b672cc822392560228623cda0f Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Wed, 19 Aug 2026 17:28:23 +0800 Subject: [PATCH] modify: chongming-cli --- README.md | 17 +- pyproject.toml | 7 +- src/chongming/app/core/config_.py | 233 ----------- src/chongming/scripts/build.py | 537 -------------------------- src/chongming/scripts/build_plugin.py | 253 ------------ src/chongming/scripts/code_content.py | 47 --- src/chongming/scripts/gen_models.py | 353 ----------------- src/chongming/scripts/gen_plugin.py | 327 ---------------- test_build_plugin.py | 4 +- uv.lock | 132 +++---- 10 files changed, 60 insertions(+), 1850 deletions(-) delete mode 100644 src/chongming/app/core/config_.py delete mode 100644 src/chongming/scripts/build.py delete mode 100644 src/chongming/scripts/build_plugin.py delete mode 100644 src/chongming/scripts/code_content.py delete mode 100644 src/chongming/scripts/gen_models.py delete mode 100644 src/chongming/scripts/gen_plugin.py diff --git a/README.md b/README.md index 0cd41f4..e23e945 100644 --- a/README.md +++ b/README.md @@ -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//` | `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//config.toml,在 [workers] 节注册插件 (type / module / factory / cleanup / count + [workers.config] 运行时配置) 3. 创建 config/workers//*.toml 端点定义 -4. 运行 uv run python -m chongming.scripts.gen_models 生成模型 +4. 运行 uv run gen_models 生成模型 5. 实现 router/.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//.toml -2. 运行 uv run python -m chongming.scripts.gen_models +2. 运行 uv run gen_models 3. 创建 router/.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 # 生成统一风格插件骨架 -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 # 独立 Worker 进程(本地调试,load_mode=external) chongming-worker --type # 独立 Worker 进程(容器入口) uv pip install -e utils/ # 安装/更新工具包 @@ -485,10 +486,10 @@ uv pip install -e utils/ # 安装/更新工具包 ```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 # 生成统一风格插件骨架 -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 # 进程插件打包 → dist/.zip diff --git a/pyproject.toml b/pyproject.toml index 262ac94..246cc3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/chongming/app/core/config_.py b/src/chongming/app/core/config_.py deleted file mode 100644 index b3340c9..0000000 --- a/src/chongming/app/core/config_.py +++ /dev/null @@ -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}... 占位符;默认 "." - 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)) diff --git a/src/chongming/scripts/build.py b/src/chongming/scripts/build.py deleted file mode 100644 index 2fd37da..0000000 --- a/src/chongming/scripts/build.py +++ /dev/null @@ -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() diff --git a/src/chongming/scripts/build_plugin.py b/src/chongming/scripts/build_plugin.py deleted file mode 100644 index 4508612..0000000 --- a/src/chongming/scripts/build_plugin.py +++ /dev/null @@ -1,253 +0,0 @@ -""" -进程插件打包器 — 将 external 进程插件打包为可部署 zip - -用法: - uv run build_plugin [--output ] - -示例: - uv run build_plugin demo # 打包 workers/demo → dist/demo.zip - uv run build_plugin demo --output /tmp/pkg - -产物布局(zip 根即容器工作目录,unzip 后直接 ./start.sh 启动): - workers// 插件代码(模块 workers.) - config/workers// 插件注册 + 端点配置 - config/zenoh_config.json5 Zenoh 会话配置 - config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需) - pyproject.toml uv 依赖清单(chongming-worker + 插件扩展依赖) - start.sh 容器入口: uv sync + uv run worker_serve --type - -说明: - - 插件扩展依赖声明在 workers//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//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//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() diff --git a/src/chongming/scripts/code_content.py b/src/chongming/scripts/code_content.py deleted file mode 100644 index a5e69fc..0000000 --- a/src/chongming/scripts/code_content.py +++ /dev/null @@ -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}") diff --git a/src/chongming/scripts/gen_models.py b/src/chongming/scripts/gen_models.py deleted file mode 100644 index 6c5cabe..0000000 --- a/src/chongming/scripts/gen_models.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -模型生成脚本 — 从 config/workers//*.toml 生成 Pydantic 模型 - -用法: - uv run gen_models config/workers/example src/plugins/example - -工作流: - 1. 编写 config/workers//*.toml 配置文件 - 2. 执行 gen_models 生成 Pydantic 模型 - 3. 在业务代码中 import 生成的模型 - -功能: - 1. 扫描配置目录下的所有 .toml 文件 - 2. 解析每个路由的 params 和 response_model - 3. 生成 Pydantic 请求/响应模型 - 4. 写入 src/plugins//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 ") - 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() diff --git a/src/chongming/scripts/gen_plugin.py b/src/chongming/scripts/gen_plugin.py deleted file mode 100644 index 305b43b..0000000 --- a/src/chongming/scripts/gen_plugin.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -插件脚手架生成器 — 生成风格统一的 Worker 插件模板 - -用法: - uv run gen-plugin [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/.py 实现业务逻辑 - 4. 编辑 src/plugins/myplugin/__init__.py 的 on_init / on_cleanup - 5. uv run serve # 启动验证 - -生成内容: - config/workers//config.toml Worker 注册 + 运行时配置 - config/workers//.toml 每个操作的 RPC 端点定义 - src/plugins//__init__.py 基于 Plugin 基类的统一入口 - src/plugins//global_var.py 白名单版全局变量容器 - src/plugins//router/__init__.py 端点模块聚合导出 - src/plugins//router/.py 每个操作的 RPC 实现骨架 - src/plugins//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 [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() diff --git a/test_build_plugin.py b/test_build_plugin.py index 2c2e1c1..1cbf791 100644 --- a/test_build_plugin.py +++ b/test_build_plugin.py @@ -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 = [] diff --git a/uv.lock b/uv.lock index ac090eb..6ad06be 100644 --- a/uv.lock +++ b/uv.lock @@ -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]]