已合并
refactor: 调整runtime 中代码规范 #85
GYHHelloworld创建于 4月13日
refactor: 调整runtime 中代码规范 #85
已合并
共 29 个文件变更+523-327
| @@ -47,4 +47,3 @@ def setup_error_file_logging() -> Path: | |||
| 47 | lg.setLevel(logging.ERROR) | 47 | lg.setLevel(logging.ERROR) |
| 48 | 48 | ||
| 49 | return log_path | 49 | return log_path |
| 50 | - | ||
| @@ -23,14 +23,22 @@ from typing import Any, Dict, List, Optional | |||
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | _AGUI_TEXT_DELTA_FLUSH_CHARS = max(1, int(os.environ.get("AGUI_TEXT_DELTA_FLUSH_CHARS", "24"))) | 25 | _AGUI_TEXT_DELTA_FLUSH_CHARS = max(1, int(os.environ.get("AGUI_TEXT_DELTA_FLUSH_CHARS", "24"))) |
| 26 | -_AGUI_TEXT_DELTA_FLUSH_ON_TAIL = tuple( | 26 | + |
| 27 | - marker.strip() | 27 | + |
| 28 | - for marker in os.environ.get( | 28 | +def _agui_flush_tail_markers() -> tuple[str, ...]: |
| 29 | + raw = os.environ.get( | ||
| 29 | "AGUI_TEXT_DELTA_FLUSH_ON_TAIL", | 30 | "AGUI_TEXT_DELTA_FLUSH_ON_TAIL", |
| 30 | ".,,,,,。,!,?,!,?,;,;,\n", | 31 | ".,,,,,。,!,?,!,?,;,;,\n", |
| 31 | - ).split(",") | 32 | + ) |
| 32 | - if marker.strip() | 33 | + out: list[str] = [] |
| 33 | -) | 34 | + for marker in raw.split(","): |
| 35 | + m = marker.strip() | ||
| 36 | + if m: | ||
| 37 | + out.append(m) | ||
| 38 | + return tuple(out) | ||
| 39 | + | ||
| 40 | + | ||
| 41 | +_AGUI_TEXT_DELTA_FLUSH_ON_TAIL = _agui_flush_tail_markers() | ||
| 34 | 42 | ||
| 35 | 43 | ||
| 36 | 44 | ||
| @@ -177,7 +177,8 @@ def _setup_logging(): | |||
| 177 | log_dir = os.path.join(venv_path, "logs") | 177 | log_dir = os.path.join(venv_path, "logs") |
| 178 | log_level_name = os.environ.get("LOWCODE_AGENT_LOG_LEVEL", "INFO").upper() | 178 | log_level_name = os.environ.get("LOWCODE_AGENT_LOG_LEVEL", "INFO").upper() |
| 179 | log_level = getattr(logging, log_level_name, logging.INFO) | 179 | log_level = getattr(logging, log_level_name, logging.INFO) |
| 180 | - disable_global_stream_log = os.environ.get("LOWCODE_AGENT_DISABLE_GLOBAL_STREAM_LOG", "1").lower() in ("1", "true", "yes", "on") | 180 | + _disable_raw = os.environ.get("LOWCODE_AGENT_DISABLE_GLOBAL_STREAM_LOG", "1").lower() |
| 181 | + disable_global_stream_log = _disable_raw in ("1", "true", "yes", "on") | ||
| 181 | 182 | ||
| 182 | # 确保日志目录存在 | 183 | # 确保日志目录存在 |
| 183 | os.makedirs(log_dir, exist_ok=True) | 184 | os.makedirs(log_dir, exist_ok=True) |
| @@ -191,17 +192,17 @@ def _setup_logging(): | |||
| 191 | global _ALLOWED_LOG_HANDLER_IDS | 192 | global _ALLOWED_LOG_HANDLER_IDS |
| 192 | 193 | ||
| 193 | # 配置 root logger | 194 | # 配置 root logger |
| 194 | - logger = logging.getLogger("lowcode_agent") | 195 | + agent_logger = logging.getLogger("lowcode_agent") |
| 195 | - logger.setLevel(log_level) | 196 | + agent_logger.setLevel(log_level) |
| 196 | 197 | ||
| 197 | # 避免重复添加 handler | 198 | # 避免重复添加 handler |
| 198 | - if not logger.handlers: | 199 | + if not agent_logger.handlers: |
| 199 | # 文件 handler | 200 | # 文件 handler |
| 200 | file_handler = logging.FileHandler(log_file, encoding='utf-8') | 201 | file_handler = logging.FileHandler(log_file, encoding='utf-8') |
| 201 | file_handler.setLevel(log_level) | 202 | file_handler.setLevel(log_level) |
| 202 | file_formatter = logging.Formatter(log_format, datefmt=date_format) | 203 | file_formatter = logging.Formatter(log_format, datefmt=date_format) |
| 203 | file_handler.setFormatter(file_formatter) | 204 | file_handler.setFormatter(file_formatter) |
| 204 | - logger.addHandler(file_handler) | 205 | + agent_logger.addHandler(file_handler) |
| 205 | _ALLOWED_LOG_HANDLER_IDS = {id(file_handler)} | 206 | _ALLOWED_LOG_HANDLER_IDS = {id(file_handler)} |
| 206 | 207 | ||
| 207 | # ==================== 捕获 openjiuwen 模块日志 ==================== | 208 | # ==================== 捕获 openjiuwen 模块日志 ==================== |
| @@ -235,7 +236,7 @@ def _setup_logging(): | |||
| 235 | def _strict_add_handler(self, hdlr): | 236 | def _strict_add_handler(self, hdlr): |
| 236 | target_name = getattr(self, "name", "") | 237 | target_name = getattr(self, "name", "") |
| 237 | if target_name in _STRICT_LOGGER_HANDLER_NAMES and id(hdlr) not in _ALLOWED_LOG_HANDLER_IDS: | 238 | if target_name in _STRICT_LOGGER_HANDLER_NAMES and id(hdlr) not in _ALLOWED_LOG_HANDLER_IDS: |
| 238 | - return | 239 | + return None |
| 239 | return original_add_handler(self, hdlr) | 240 | return original_add_handler(self, hdlr) |
| 240 | logging.Logger.addHandler = _strict_add_handler | 241 | logging.Logger.addHandler = _strict_add_handler |
| 241 | setattr(logging.Logger, "_lowcode_strict_add_handler_patched", True) | 242 | setattr(logging.Logger, "_lowcode_strict_add_handler_patched", True) |
| @@ -264,15 +265,18 @@ def _setup_logging(): | |||
| 264 | if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler): | 265 | if isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler): |
| 265 | target_logger.removeHandler(h) | 266 | target_logger.removeHandler(h) |
| 266 | 267 | ||
| 267 | - logger.info("=" * 60) | 268 | + agent_logger.info("=" * 60) |
| 268 | - logger.info(f"Lowcode Agent Runner 启动") | 269 | + agent_logger.info("Lowcode Agent Runner 启动") |
| 269 | - logger.info(f"虚拟环境路径: {venv_path}") | 270 | + agent_logger.info("虚拟环境路径: %s", venv_path) |
| 270 | - logger.info(f"日志文件路径: {log_file}") | 271 | + agent_logger.info("日志文件路径: %s", log_file) |
| 271 | - logger.info(f"全局 StreamHandler 移除: {'开启' if disable_global_stream_log else '关闭'}") | 272 | + agent_logger.info( |
| 272 | - logger.info(f"已捕获 openjiuwen 模块日志: {', '.join(openjiuwen_loggers)}") | 273 | + "全局 StreamHandler 移除: %s", |
| 273 | - logger.info("=" * 60) | 274 | + "开启" if disable_global_stream_log else "关闭", |
| 275 | + ) | ||
| 276 | + agent_logger.info("已捕获 openjiuwen 模块日志: %s", ", ".join(openjiuwen_loggers)) | ||
| 277 | + agent_logger.info("=" * 60) | ||
| 274 | 278 | ||
| 275 | - return logger | 279 | + return agent_logger |
| 276 | 280 | ||
| 277 | 281 | ||
| 278 | def _summarize_chunk_for_log(chunk) -> str: | 282 | def _summarize_chunk_for_log(chunk) -> str: |
| @@ -417,7 +421,6 @@ async def init(): | |||
| 417 | 421 | ||
| 418 | # 启动 Runner | 422 | # 启动 Runner |
| 419 | logger.info("启动 Runner...") | 423 | logger.info("启动 Runner...") |
| 420 | - from openjiuwen.core.runner import Runner | ||
| 421 | 424 | ||
| 422 | # 设置工作流超时时间(支持从环境变量获取,默认 5 分钟) | 425 | # 设置工作流超时时间(支持从环境变量获取,默认 5 分钟) |
| 423 | workflow_timeout = os.environ.get("WORKFLOW_EXECUTE_TIMEOUT", "300") | 426 | workflow_timeout = os.environ.get("WORKFLOW_EXECUTE_TIMEOUT", "300") |
Mapplications/lowcode_agent/openjiuwen_runtime/examples/lowcode_agent/runtime_agent_adapters.py+16-3
| @@ -8,7 +8,14 @@ from __future__ import annotations | |||
| 8 | 8 | ||
| 9 | from typing import Any, Dict, Optional | 9 | from typing import Any, Dict, Optional |
| 10 | 10 | ||
| 11 | -from openjiuwen_studio.core.common.dsl import McpConfig, McpTransport, Param, PluginCodeConfig, PluginType, RestfulApiSchema | 11 | +from openjiuwen_studio.core.common.dsl import ( |
| 12 | + McpConfig, | ||
| 13 | + McpTransport, | ||
| 14 | + Param, | ||
| 15 | + PluginCodeConfig, | ||
| 16 | + PluginType, | ||
| 17 | + RestfulApiSchema, | ||
| 18 | +) | ||
| 12 | from openjiuwen_studio.core.executor.plugin.plugin_mgr import PluginManager | 19 | from openjiuwen_studio.core.executor.plugin.plugin_mgr import PluginManager |
| 13 | from openjiuwen_studio.core.executor.plugin.plugin_tools import CodeTool, McpTool, ServiceTool | 20 | from openjiuwen_studio.core.executor.plugin.plugin_tools import CodeTool, McpTool, ServiceTool |
| 14 | 21 | ||
| @@ -99,7 +106,10 @@ def _build_service_tool(plugin_data: Dict[str, Any], tool_data: Dict[str, Any]) | |||
| 99 | params=[_build_param(p) for p in (tool_data.get("request_params") or tool_data.get("inputs") or [])], | 106 | params=[_build_param(p) for p in (tool_data.get("request_params") or tool_data.get("inputs") or [])], |
| 100 | path=path, | 107 | path=path, |
| 101 | headers=dict(tool_data.get("headers") or plugin_data.get("headers") or {}), | 108 | headers=dict(tool_data.get("headers") or plugin_data.get("headers") or {}), |
| 102 | - method=method_map.get(str(tool_data.get("method") or "").lower(), str(tool_data.get("method") or "GET").upper()), | 109 | + method=method_map.get( |
| 110 | + str(tool_data.get("method") or "").lower(), | ||
| 111 | + str(tool_data.get("method") or "GET").upper(), | ||
| 112 | + ), | ||
| 103 | response=[_build_param(p) for p in (tool_data.get("response_params") or tool_data.get("outputs") or [])], | 113 | response=[_build_param(p) for p in (tool_data.get("response_params") or tool_data.get("outputs") or [])], |
| 104 | ) | 114 | ) |
| 105 | ) | 115 | ) |
| @@ -177,7 +187,10 @@ class RuntimePluginManager(PluginManager): | |||
| 177 | if record is None and tool_id == plugin_id: | 187 | if record is None and tool_id == plugin_id: |
| 178 | record = self._plugin_default_tool.get((plugin_id, "")) | 188 | record = self._plugin_default_tool.get((plugin_id, "")) |
| 179 | if record is None: | 189 | if record is None: |
| 180 | - raise ValueError(f"Runtime plugin tool not found: plugin_id={plugin_id}, tool_id={tool_id}, version={version}") | 190 | + raise ValueError( |
| 191 | + f"Runtime plugin tool not found: plugin_id={plugin_id}, " | ||
| 192 | + f"tool_id={tool_id}, version={version}" | ||
| 193 | + ) | ||
| 181 | 194 | ||
| 182 | plugin_data, tool_data, plugin_type = record | 195 | plugin_data, tool_data, plugin_type = record |
| 183 | if plugin_type == PluginType.SERVICE: | 196 | if plugin_type == PluginType.SERVICE: |
| @@ -8,13 +8,17 @@ | |||
| 8 | 8 | ||
| 9 | import asyncio | 9 | import asyncio |
| 10 | import json | 10 | import json |
| 11 | -import sys | ||
| 12 | from pathlib import Path | 11 | from pathlib import Path |
| 13 | 12 | ||
| 14 | import click | 13 | import click |
| 15 | from dotenv import load_dotenv | 14 | from dotenv import load_dotenv |
| 16 | 15 | ||
| 17 | -from openjiuwen_runtime.management.manager import DeploymentManager | 16 | +from openjiuwen_runtime.management import ( |
| 17 | + DeployAgentParams, | ||
| 18 | + DeployPluginParams, | ||
| 19 | + DeploymentManager, | ||
| 20 | + ListDeploymentsParams, | ||
| 21 | +) | ||
| 18 | from openjiuwen_runtime.management.models.enums import DeploymentType, DeploymentStatus | 22 | from openjiuwen_runtime.management.models.enums import DeploymentType, DeploymentStatus |
| 19 | from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler | 23 | from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler |
| 20 | 24 | ||
| @@ -76,27 +80,30 @@ def deploy(ctx, python_file_path, name, port): | |||
| 76 | 80 | ||
| 77 | async def _deploy(): | 81 | async def _deploy(): |
| 78 | result = await manager.deploy_agent( | 82 | result = await manager.deploy_agent( |
| 79 | - name=name, | 83 | + DeployAgentParams( |
| 80 | - version="1.0.0", | 84 | + name=name, |
| 81 | - python_file_path=python_file_path, | 85 | + version="1.0.0", |
| 82 | - port=port, | 86 | + extras={"python_file_path": python_file_path, "port": port}, |
| 87 | + ) | ||
| 83 | ) | 88 | ) |
| 84 | click.echo(json.dumps(result, indent=2, ensure_ascii=False)) | 89 | click.echo(json.dumps(result, indent=2, ensure_ascii=False)) |
| 85 | 90 | ||
| 86 | asyncio.run(_deploy()) | 91 | asyncio.run(_deploy()) |
| 87 | 92 | ||
| 88 | 93 | ||
| 89 | -@agent.command() | 94 | +@agent.command(name="list") |
| 90 | 95 | ||
| 91 | 96 | ||
| 92 | -def list(ctx, status): | 97 | +def list_deployments(ctx, status): |
| 93 | """查询 Agent 列表""" | 98 | """查询 Agent 列表""" |
| 94 | manager = ctx.obj["manager"] | 99 | manager = ctx.obj["manager"] |
| 95 | 100 | ||
| 96 | async def _list(): | 101 | async def _list(): |
| 97 | deployments = await manager.list_deployments( | 102 | deployments = await manager.list_deployments( |
| 98 | - deployment_type=DeploymentType.AGENT, | 103 | + ListDeploymentsParams( |
| 99 | - deployment_status=DeploymentStatus(status) if status else None, | 104 | + deployment_type=DeploymentType.AGENT, |
| 105 | + deployment_status=DeploymentStatus(status) if status else None, | ||
| 106 | + ) | ||
| 100 | ) | 107 | ) |
| 101 | 108 | ||
| 102 | if not deployments: | 109 | if not deployments: |
| @@ -107,11 +114,15 @@ def list(ctx, status): | |||
| 107 | click.echo(f"{'ID':<30} {'Name':<20} {'Status':<10} {'Port':<6} {'Package':<20} {'URL'}") | 114 | click.echo(f"{'ID':<30} {'Name':<20} {'Status':<10} {'Port':<6} {'Package':<20} {'URL'}") |
| 108 | click.echo("-" * 120) | 115 | click.echo("-" * 120) |
| 109 | for dep in deployments: | 116 | for dep in deployments: |
| 110 | - name = dep.get('name') or '-' | 117 | + name = dep.get("name") or "-" |
| 111 | - dep_status = dep['status'].value if hasattr(dep['status'], 'value') else str(dep['status']) | 118 | + dep_status = dep["status"].value if hasattr(dep["status"], "value") else str(dep["status"]) |
| 112 | - package_name = dep.get('package_name') or '-' | 119 | + package_name = dep.get("package_name") or "-" |
| 113 | - url = dep.get('url') or '-' | 120 | + url = dep.get("url") or "-" |
| 114 | - click.echo(f"{dep['deployment_id']:<30} {name:<20} {dep_status:<10} {dep['port']:<6} {package_name:<20} {url:<30}") | 121 | + row = ( |
| 122 | + f"{dep['deployment_id']:<30} {name:<20} {dep_status:<10} " | ||
| 123 | + f"{dep['port']:<6} {package_name:<20} {url:<30}" | ||
| 124 | + ) | ||
| 125 | + click.echo(row) | ||
| 115 | 126 | ||
| 116 | asyncio.run(_list()) | 127 | asyncio.run(_list()) |
| 117 | 128 | ||
| @@ -129,7 +140,7 @@ def get(ctx, deployment_id): | |||
| 129 | click.echo(json.dumps(deployment, indent=2, ensure_ascii=False)) | 140 | click.echo(json.dumps(deployment, indent=2, ensure_ascii=False)) |
| 130 | else: | 141 | else: |
| 131 | click.echo(f"Deployment {deployment_id} not found", err=True) | 142 | click.echo(f"Deployment {deployment_id} not found", err=True) |
| 132 | - sys.exit(1) | 143 | + raise click.Abort() |
| 133 | 144 | ||
| 134 | asyncio.run(_get()) | 145 | asyncio.run(_get()) |
| 135 | 146 | ||
| @@ -147,7 +158,7 @@ def delete(ctx, deployment_id): | |||
| 147 | click.echo(f"Deployment {deployment_id} deleted") | 158 | click.echo(f"Deployment {deployment_id} deleted") |
| 148 | else: | 159 | else: |
| 149 | click.echo(f"Deployment {deployment_id} not found", err=True) | 160 | click.echo(f"Deployment {deployment_id} not found", err=True) |
| 150 | - sys.exit(1) | 161 | + raise click.Abort() |
| 151 | 162 | ||
| 152 | asyncio.run(_delete()) | 163 | asyncio.run(_delete()) |
| 153 | 164 | ||
| @@ -180,27 +191,30 @@ def deploy(ctx, python_file_path, name, port): | |||
| 180 | 191 | ||
| 181 | async def _deploy(): | 192 | async def _deploy(): |
| 182 | result = await manager.deploy_plugin( | 193 | result = await manager.deploy_plugin( |
| 183 | - name=name, | 194 | + DeployPluginParams( |
| 184 | - version="1.0.0", | 195 | + name=name, |
| 185 | - python_file_path=python_file_path, | 196 | + version="1.0.0", |
| 186 | - port=port, | 197 | + extras={"python_file_path": python_file_path, "port": port}, |
| 198 | + ) | ||
| 187 | ) | 199 | ) |
| 188 | click.echo(json.dumps(result, indent=2, ensure_ascii=False)) | 200 | click.echo(json.dumps(result, indent=2, ensure_ascii=False)) |
| 189 | 201 | ||
| 190 | asyncio.run(_deploy()) | 202 | asyncio.run(_deploy()) |
| 191 | 203 | ||
| 192 | 204 | ||
| 193 | -@plugin.command() | 205 | +@plugin.command(name="list") |
| 194 | 206 | ||
| 195 | 207 | ||
| 196 | -def list(ctx, status): | 208 | +def list_plugin_deployments(ctx, status): |
| 197 | """查询 Plugin 列表""" | 209 | """查询 Plugin 列表""" |
| 198 | manager = ctx.obj["manager"] | 210 | manager = ctx.obj["manager"] |
| 199 | 211 | ||
| 200 | async def _list(): | 212 | async def _list(): |
| 201 | deployments = await manager.list_deployments( | 213 | deployments = await manager.list_deployments( |
| 202 | - deployment_type=DeploymentType.PLUGIN, | 214 | + ListDeploymentsParams( |
| 203 | - deployment_status=DeploymentStatus(status) if status else None, | 215 | + deployment_type=DeploymentType.PLUGIN, |
| 216 | + deployment_status=DeploymentStatus(status) if status else None, | ||
| 217 | + ) | ||
| 204 | ) | 218 | ) |
| 205 | 219 | ||
| 206 | if not deployments: | 220 | if not deployments: |
| @@ -211,11 +225,15 @@ def list(ctx, status): | |||
| 211 | click.echo(f"{'ID':<30} {'Name':<20} {'Status':<10} {'Port':<6} {'Package':<20} {'URL'}") | 225 | click.echo(f"{'ID':<30} {'Name':<20} {'Status':<10} {'Port':<6} {'Package':<20} {'URL'}") |
| 212 | click.echo("-" * 120) | 226 | click.echo("-" * 120) |
| 213 | for dep in deployments: | 227 | for dep in deployments: |
| 214 | - name = dep.get('name') or '-' | 228 | + name = dep.get("name") or "-" |
| 215 | - dep_status = dep['status'].value if hasattr(dep['status'], 'value') else str(dep['status']) | 229 | + dep_status = dep["status"].value if hasattr(dep["status"], "value") else str(dep["status"]) |
| 216 | - package_name = dep.get('package_name') or '-' | 230 | + package_name = dep.get("package_name") or "-" |
| 217 | - url = dep.get('url') or '-' | 231 | + url = dep.get("url") or "-" |
| 218 | - click.echo(f"{dep['deployment_id']:<30} {name:<20} {dep_status:<10} {dep['port']:<6} {package_name:<20} {url:<30}") | 232 | + row = ( |
| 233 | + f"{dep['deployment_id']:<30} {name:<20} {dep_status:<10} " | ||
| 234 | + f"{dep['port']:<6} {package_name:<20} {url:<30}" | ||
| 235 | + ) | ||
| 236 | + click.echo(row) | ||
| 219 | 237 | ||
| 220 | asyncio.run(_list()) | 238 | asyncio.run(_list()) |
| 221 | 239 | ||
| @@ -233,7 +251,7 @@ def get(ctx, deployment_id): | |||
| 233 | click.echo(json.dumps(deployment, indent=2, ensure_ascii=False)) | 251 | click.echo(json.dumps(deployment, indent=2, ensure_ascii=False)) |
| 234 | else: | 252 | else: |
| 235 | click.echo(f"Deployment {deployment_id} not found", err=True) | 253 | click.echo(f"Deployment {deployment_id} not found", err=True) |
| 236 | - sys.exit(1) | 254 | + raise click.Abort() |
| 237 | 255 | ||
| 238 | asyncio.run(_get()) | 256 | asyncio.run(_get()) |
| 239 | 257 | ||
| @@ -251,7 +269,7 @@ def delete(ctx, deployment_id): | |||
| 251 | click.echo(f"Deployment {deployment_id} deleted") | 269 | click.echo(f"Deployment {deployment_id} deleted") |
| 252 | else: | 270 | else: |
| 253 | click.echo(f"Deployment {deployment_id} not found", err=True) | 271 | click.echo(f"Deployment {deployment_id} not found", err=True) |
| 254 | - sys.exit(1) | 272 | + raise click.Abort() |
| 255 | 273 | ||
| 256 | asyncio.run(_delete()) | 274 | asyncio.run(_delete()) |
| 257 | 275 | ||
| @@ -10,6 +10,8 @@ from .packaging import ( | |||
| 10 | uninstall_package, | 10 | uninstall_package, |
| 11 | ) | 11 | ) |
| 12 | from .docker_utils import ( | 12 | from .docker_utils import ( |
| 13 | + DockerImageBuildParams, | ||
| 14 | + DockerfileGenerateParams, | ||
| 13 | build_docker_image, | 15 | build_docker_image, |
| 14 | push_docker_image, | 16 | push_docker_image, |
| 15 | tag_docker_image, | 17 | tag_docker_image, |
| @@ -25,6 +27,8 @@ __all__ = [ | |||
| 25 | "get_python_path", | 27 | "get_python_path", |
| 26 | "install_package", | 28 | "install_package", |
| 27 | "uninstall_package", | 29 | "uninstall_package", |
| 30 | + "DockerImageBuildParams", | ||
| 31 | + "DockerfileGenerateParams", | ||
| 28 | "build_docker_image", | 32 | "build_docker_image", |
| 29 | "push_docker_image", | 33 | "push_docker_image", |
| 30 | "tag_docker_image", | 34 | "tag_docker_image", |
| @@ -84,13 +84,22 @@ class Settings(BaseSettings): | |||
| 84 | self.deploy_path = Path(self.DEPLOY_DIR) | 84 | self.deploy_path = Path(self.DEPLOY_DIR) |
| 85 | if not self.deploy_path.is_absolute(): | 85 | if not self.deploy_path.is_absolute(): |
| 86 | self.deploy_path = PROJECT_ROOT / self.deploy_path | 86 | self.deploy_path = PROJECT_ROOT / self.deploy_path |
| 87 | - self.DEPLOY_DIR = str(self.deploy_path.resolve()) | 87 | + # 与 .env 中 DEPLOY_DIR 键名一致,保持大写属性名(Pydantic 字段) |
| 88 | + object.__setattr__( | ||
| 89 | + self, | ||
| 90 | + "DEPLOY_DIR", | ||
| 91 | + str(self.deploy_path.resolve()), | ||
| 92 | + ) | ||
| 88 | 93 | ||
| 89 | # 处理 DIST_DIR | 94 | # 处理 DIST_DIR |
| 90 | self.dist_path = Path(self.DIST_DIR) | 95 | self.dist_path = Path(self.DIST_DIR) |
| 91 | if not self.dist_path.is_absolute(): | 96 | if not self.dist_path.is_absolute(): |
| 92 | self.dist_path = PROJECT_ROOT / self.dist_path | 97 | self.dist_path = PROJECT_ROOT / self.dist_path |
| 93 | - self.DIST_DIR = str(self.dist_path.resolve()) | 98 | + object.__setattr__( |
| 99 | + self, | ||
| 100 | + "DIST_DIR", | ||
| 101 | + str(self.dist_path.resolve()), | ||
| 102 | + ) | ||
| 94 | 103 | ||
| 95 | # 自动创建目录(可选,非常实用) | 104 | # 自动创建目录(可选,非常实用) |
| 96 | self.deploy_path.mkdir(parents=True, exist_ok=True) | 105 | self.deploy_path.mkdir(parents=True, exist_ok=True) |
| @@ -36,6 +36,10 @@ class SQLAlchemyHandler(DBHandler): | |||
| 36 | self._table_models: dict[str, Any] = {} | 36 | self._table_models: dict[str, Any] = {} |
| 37 | logger.debug("SQLAlchemyHandler created") | 37 | logger.debug("SQLAlchemyHandler created") |
| 38 | 38 | ||
| 39 | + def is_table_registered(self, table_name: str) -> bool: | ||
| 40 | + """是否已通过 init_table 注册过对应 ORM 模型(供测试等场景使用)。""" | ||
| 41 | + return table_name in self._table_models | ||
| 42 | + | ||
| 39 | async def connect(self) -> None: | 43 | async def connect(self) -> None: |
| 40 | logger.info("Connecting to database") | 44 | logger.info("Connecting to database") |
| 41 | # 关闭 aiosqlite 的 DEBUG 日志 | 45 | # 关闭 aiosqlite 的 DEBUG 日志 |
| @@ -2,47 +2,57 @@ | |||
| 2 | # Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved | 2 | # Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved |
| 3 | 3 | ||
| 4 | import asyncio | 4 | import asyncio |
| 5 | +from dataclasses import dataclass | ||
| 5 | from pathlib import Path | 6 | from pathlib import Path |
| 6 | from typing import Optional | 7 | from typing import Optional |
| 7 | 8 | ||
| 8 | 9 | ||
| 9 | -async def build_docker_image( | 10 | +@dataclass |
| 10 | - context_path: str, | 11 | +class DockerImageBuildParams: |
| 11 | - dockerfile: str = "Dockerfile", | 12 | + """docker build 参数""" |
| 12 | - image_name: str = None, | 13 | + |
| 13 | - tag: str = "latest", | 14 | + context_path: str |
| 14 | - build_args: Optional[dict] = None, | 15 | + dockerfile: str = "Dockerfile" |
| 15 | - docker_host: Optional[str] = None, | 16 | + image_name: Optional[str] = None |
| 16 | -) -> tuple[bool, str]: | 17 | + tag: str = "latest" |
| 18 | + build_args: Optional[dict] = None | ||
| 19 | + docker_host: Optional[str] = None | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +class DockerfileGenerateParams: | ||
| 24 | + """生成 Dockerfile 内容的参数""" | ||
| 25 | + | ||
| 26 | + base_image: str = "python:3.10-slim" | ||
| 27 | + workdir: str = "/app" | ||
| 28 | + package_name: Optional[str] = None | ||
| 29 | + port: int = 8000 | ||
| 30 | + entrypoint: Optional[str] = None | ||
| 31 | + extra_commands: Optional[list] = None | ||
| 32 | + | ||
| 33 | + | ||
| 34 | +async def build_docker_image(params: DockerImageBuildParams) -> tuple[bool, str]: | ||
| 17 | """ | 35 | """ |
| 18 | 构建Docker镜像 | 36 | 构建Docker镜像 |
| 19 | - | 37 | + |
| 20 | - Args: | ||
| 21 | - context_path: 构建上下文路径 | ||
| 22 | - dockerfile: Dockerfile路径,相对于context_path | ||
| 23 | - image_name: 镜像名称 | ||
| 24 | - tag: 镜像标签 | ||
| 25 | - build_args: 构建参数 | ||
| 26 | - docker_host: Docker主机地址 | ||
| 27 | - | ||
| 28 | Returns: | 38 | Returns: |
| 29 | tuple[bool, str]: (是否成功, 输出信息) | 39 | tuple[bool, str]: (是否成功, 输出信息) |
| 30 | """ | 40 | """ |
| 31 | - context_path = Path(context_path).resolve() | 41 | + context_path = Path(params.context_path).resolve() |
| 32 | if not context_path.exists(): | 42 | if not context_path.exists(): |
| 33 | return False, f"Context path not found: {context_path}" | 43 | return False, f"Context path not found: {context_path}" |
| 34 | 44 | ||
| 35 | cmd = ["docker"] | 45 | cmd = ["docker"] |
| 36 | - if docker_host: | 46 | + if params.docker_host: |
| 37 | - cmd.extend(["-H", docker_host]) | 47 | + cmd.extend(["-H", params.docker_host]) |
| 38 | 48 | ||
| 39 | - cmd.extend(["build", "-t", f"{image_name}:{tag}"]) | 49 | + cmd.extend(["build", "-t", f"{params.image_name}:{params.tag}"]) |
| 40 | 50 | ||
| 41 | - if dockerfile != "Dockerfile": | 51 | + if params.dockerfile != "Dockerfile": |
| 42 | - cmd.extend(["-f", dockerfile]) | 52 | + cmd.extend(["-f", params.dockerfile]) |
| 43 | 53 | ||
| 44 | - if build_args: | 54 | + if params.build_args: |
| 45 | - for key, value in build_args.items(): | 55 | + for key, value in params.build_args.items(): |
| 46 | cmd.extend(["--build-arg", f"{key}={value}"]) | 56 | cmd.extend(["--build-arg", f"{key}={value}"]) |
| 47 | 57 | ||
| 48 | cmd.append(str(context_path)) | 58 | cmd.append(str(context_path)) |
| @@ -67,13 +77,13 @@ async def push_docker_image( | |||
| 67 | ) -> tuple[bool, str]: | 77 | ) -> tuple[bool, str]: |
| 68 | """ | 78 | """ |
| 69 | 推送Docker镜像 | 79 | 推送Docker镜像 |
| 70 | - | 80 | + |
| 71 | Args: | 81 | Args: |
| 72 | image_name: 镜像名称 | 82 | image_name: 镜像名称 |
| 73 | tag: 镜像标签 | 83 | tag: 镜像标签 |
| 74 | registry: 镜像仓库地址 | 84 | registry: 镜像仓库地址 |
| 75 | docker_host: Docker主机地址 | 85 | docker_host: Docker主机地址 |
| 76 | - | 86 | + |
| 77 | Returns: | 87 | Returns: |
| 78 | tuple[bool, str]: (是否成功, 输出信息) | 88 | tuple[bool, str]: (是否成功, 输出信息) |
| 79 | """ | 89 | """ |
| @@ -108,14 +118,14 @@ async def tag_docker_image( | |||
| 108 | ) -> tuple[bool, str]: | 118 | ) -> tuple[bool, str]: |
| 109 | """ | 119 | """ |
| 110 | 标记Docker镜像 | 120 | 标记Docker镜像 |
| 111 | - | 121 | + |
| 112 | Args: | 122 | Args: |
| 113 | source_image: 源镜像名称 | 123 | source_image: 源镜像名称 |
| 114 | target_image: 目标镜像名称 | 124 | target_image: 目标镜像名称 |
| 115 | source_tag: 源标签 | 125 | source_tag: 源标签 |
| 116 | target_tag: 目标标签 | 126 | target_tag: 目标标签 |
| 117 | docker_host: Docker主机地址 | 127 | docker_host: Docker主机地址 |
| 118 | - | 128 | + |
| 119 | Returns: | 129 | Returns: |
| 120 | tuple[bool, str]: (是否成功, 输出信息) | 130 | tuple[bool, str]: (是否成功, 输出信息) |
| 121 | """ | 131 | """ |
| @@ -149,13 +159,13 @@ async def remove_docker_image( | |||
| 149 | ) -> tuple[bool, str]: | 159 | ) -> tuple[bool, str]: |
| 150 | """ | 160 | """ |
| 151 | 删除Docker镜像 | 161 | 删除Docker镜像 |
| 152 | - | 162 | + |
| 153 | Args: | 163 | Args: |
| 154 | image_name: 镜像名称 | 164 | image_name: 镜像名称 |
| 155 | tag: 镜像标签 | 165 | tag: 镜像标签 |
| 156 | force: 是否强制删除 | 166 | force: 是否强制删除 |
| 157 | docker_host: Docker主机地址 | 167 | docker_host: Docker主机地址 |
| 158 | - | 168 | + |
| 159 | Returns: | 169 | Returns: |
| 160 | tuple[bool, str]: (是否成功, 输出信息) | 170 | tuple[bool, str]: (是否成功, 输出信息) |
| 161 | """ | 171 | """ |
| @@ -180,53 +190,38 @@ async def remove_docker_image( | |||
| 180 | return False, stderr.decode().strip() | 190 | return False, stderr.decode().strip() |
| 181 | 191 | ||
| 182 | 192 | ||
| 183 | -def generate_dockerfile( | 193 | +def generate_dockerfile(params: DockerfileGenerateParams) -> str: |
| 184 | - base_image: str = "python:3.10-slim", | ||
| 185 | - workdir: str = "/app", | ||
| 186 | - package_name: str = None, | ||
| 187 | - port: int = 8000, | ||
| 188 | - entrypoint: Optional[str] = None, | ||
| 189 | - extra_commands: Optional[list] = None, | ||
| 190 | -) -> str: | ||
| 191 | """ | 194 | """ |
| 192 | 生成Dockerfile内容 | 195 | 生成Dockerfile内容 |
| 193 | - | 196 | + |
| 194 | - Args: | ||
| 195 | - base_image: 基础镜像 | ||
| 196 | - workdir: 工作目录 | ||
| 197 | - package_name: 包名 | ||
| 198 | - port: 暴露端口 | ||
| 199 | - entrypoint: 入口命令 | ||
| 200 | - extra_commands: 额外命令 | ||
| 201 | - | ||
| 202 | Returns: | 197 | Returns: |
| 203 | str: Dockerfile内容 | 198 | str: Dockerfile内容 |
| 204 | """ | 199 | """ |
| 205 | lines = [ | 200 | lines = [ |
| 206 | - f"FROM {base_image}", | 201 | + f"FROM {params.base_image}", |
| 207 | "", | 202 | "", |
| 208 | - f"WORKDIR {workdir}", | 203 | + f"WORKDIR {params.workdir}", |
| 209 | "", | 204 | "", |
| 210 | ] | 205 | ] |
| 211 | 206 | ||
| 212 | - if package_name: | 207 | + if params.package_name: |
| 213 | lines.extend([ | 208 | lines.extend([ |
| 214 | - f"COPY dist/{package_name}*.whl /tmp/", | 209 | + f"COPY dist/{params.package_name}*.whl /tmp/", |
| 215 | "RUN pip install /tmp/*.whl", | 210 | "RUN pip install /tmp/*.whl", |
| 216 | "", | 211 | "", |
| 217 | ]) | 212 | ]) |
| 218 | 213 | ||
| 219 | - if extra_commands: | 214 | + if params.extra_commands: |
| 220 | - for cmd in extra_commands: | 215 | + for cmd in params.extra_commands: |
| 221 | lines.append(f"RUN {cmd}") | 216 | lines.append(f"RUN {cmd}") |
| 222 | lines.append("") | 217 | lines.append("") |
| 223 | 218 | ||
| 224 | - lines.append(f"EXPOSE {port}") | 219 | + lines.append(f"EXPOSE {params.port}") |
| 225 | lines.append("") | 220 | lines.append("") |
| 226 | 221 | ||
| 227 | - if entrypoint: | 222 | + if params.entrypoint: |
| 228 | - lines.append(f'ENTRYPOINT {entrypoint}') | 223 | + lines.append(f'ENTRYPOINT {params.entrypoint}') |
| 229 | - elif package_name: | 224 | + elif params.package_name: |
| 230 | - lines.append(f'ENTRYPOINT ["python", "-m", "{package_name}"]') | 225 | + lines.append(f'ENTRYPOINT ["python", "-m", "{params.package_name}"]') |
| 231 | 226 | ||
| 232 | return "\n".join(lines) | 227 | return "\n".join(lines) |
| @@ -15,7 +15,12 @@ def is_port_available(port: int, host: str = "0.0.0.0") -> bool: | |||
| 15 | return False | 15 | return False |
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | -def allocate_port(start_port: int = 8090, max_port: int = 9090, host: str = "127.0.0.1", exclude_ports: set[int] | None = None) -> int: | 18 | +def allocate_port( |
| 19 | + start_port: int = 8090, | ||
| 20 | + max_port: int = 9090, | ||
| 21 | + host: str = "127.0.0.1", | ||
| 22 | + exclude_ports: set[int] | None = None, | ||
| 23 | +) -> int: | ||
| 19 | """分配可用端口 | 24 | """分配可用端口 |
| 20 | 25 | ||
| 21 | Args: | 26 | Args: |
| @@ -31,7 +31,8 @@ class VirtualEnvironmentManager: | |||
| 31 | 31 | ||
| 32 | 负责为每个部署创建、管理和清理独立的虚拟环境。 | 32 | 负责为每个部署创建、管理和清理独立的虚拟环境。 |
| 33 | """ | 33 | """ |
| 34 | - def get_venv_path(self, deployment_id: str) -> Path: | 34 | + @staticmethod |
| 35 | + def get_venv_path(deployment_id: str) -> Path: | ||
| 35 | """ | 36 | """ |
| 36 | 根据部署ID获取虚拟环境路径 | 37 | 根据部署ID获取虚拟环境路径 |
| 37 | 38 | ||
| @@ -122,7 +123,7 @@ class VirtualEnvironmentManager: | |||
| 122 | return venv_path | 123 | return venv_path |
| 123 | except subprocess.CalledProcessError as e: | 124 | except subprocess.CalledProcessError as e: |
| 124 | logger.error("Failed to create virtual environment: %s", e.stderr) | 125 | logger.error("Failed to create virtual environment: %s", e.stderr) |
| 125 | - raise RuntimeError(f"Failed to create venv: {e}") | 126 | + raise RuntimeError(f"Failed to create venv: {e}") from e |
| 126 | 127 | ||
| 127 | def get_python_executable(self, deployment_id: str) -> Path: | 128 | def get_python_executable(self, deployment_id: str) -> Path: |
| 128 | """ | 129 | """ |
| @@ -220,14 +221,16 @@ class VirtualEnvironmentManager: | |||
| 220 | return True | 221 | return True |
| 221 | 222 | ||
| 222 | logger.error("WHL package not found in installed list: %s", whl_path) | 223 | logger.error("WHL package not found in installed list: %s", whl_path) |
| 223 | - raise RuntimeError(f"Failed to install WHL package: package not in pip list") | 224 | + raise RuntimeError( |
| 225 | + "Failed to install WHL package: package not in pip list" | ||
| 226 | + ) from None | ||
| 224 | else: | 227 | else: |
| 225 | logger.error("Failed to verify installation: %s", result.stderr) | 228 | logger.error("Failed to verify installation: %s", result.stderr) |
| 226 | - raise RuntimeError(f"Failed to verify WHL installation") | 229 | + raise RuntimeError("Failed to verify WHL installation") from None |
| 227 | 230 | ||
| 228 | except Exception as e: | 231 | except Exception as e: |
| 229 | logger.error("Failed to install WHL: %s", e) | 232 | logger.error("Failed to install WHL: %s", e) |
| 230 | - raise RuntimeError(f"Failed to install WHL package: {e}") | 233 | + raise RuntimeError(f"Failed to install WHL package: {e}") from e |
| 231 | 234 | ||
| 232 | def delete_venv(self, deployment_id: str) -> bool: | 235 | def delete_venv(self, deployment_id: str) -> bool: |
| 233 | """ | 236 | """ |
| @@ -7,11 +7,11 @@ import os | |||
| 7 | import tempfile | 7 | import tempfile |
| 8 | import unittest | 8 | import unittest |
| 9 | 9 | ||
| 10 | -from openjiuwen_runtime.management.sdk.db.sqlite_handler import SQLiteHandler | 10 | +from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler |
| 11 | -from openjiuwen_runtime.management.sdk.models.table_def import ( | 11 | +from openjiuwen_runtime.foundation.db.table_def import ( |
| 12 | - TableDefinition, | ||
| 13 | ColumnDefinition, | 12 | ColumnDefinition, |
| 14 | IndexDefinition, | 13 | IndexDefinition, |
| 14 | + TableDefinition, | ||
| 15 | ) | 15 | ) |
| 16 | 16 | ||
| 17 | 17 | ||
| @@ -51,7 +51,7 @@ class TestSQLiteHandler(unittest.IsolatedAsyncioTestCase): | |||
| 51 | async def test_init_table(self): | 51 | async def test_init_table(self): |
| 52 | """测试初始化表""" | 52 | """测试初始化表""" |
| 53 | await self.handler.init_table(self.test_table_def) | 53 | await self.handler.init_table(self.test_table_def) |
| 54 | - self.assertIn("test_table", self.handler._table_models) | 54 | + self.assertTrue(self.handler.is_table_registered("test_table")) |
| 55 | 55 | ||
| 56 | async def test_create(self): | 56 | async def test_create(self): |
| 57 | """测试创建记录""" | 57 | """测试创建记录""" |
| @@ -3,10 +3,18 @@ | |||
| 3 | 3 | ||
| 4 | """OpenJiuwen Runtime Management SDK""" | 4 | """OpenJiuwen Runtime Management SDK""" |
| 5 | 5 | ||
| 6 | -from .manager import DeploymentManager, DeployMode | ||
| 7 | -from .models.enums import DeploymentType, DeploymentStatus | ||
| 8 | -from .models.schemas import DeploymentInfo, DEPLOYMENT_TABLE_NAME, DeploymentFields | ||
| 9 | from openjiuwen_runtime.foundation.db.handler import DBHandler | 6 | from openjiuwen_runtime.foundation.db.handler import DBHandler |
| 7 | +from openjiuwen_runtime.foundation.db.mysql_handler import MySQLHandler | ||
| 8 | +from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler | ||
| 9 | + | ||
| 10 | +from .manager import DeploymentManager | ||
| 11 | +from .models.deployment_params import ( | ||
| 12 | + DeployAgentParams, | ||
| 13 | + DeployPluginParams, | ||
| 14 | + ListDeploymentsParams, | ||
| 15 | +) | ||
| 16 | +from .models.enums import DeployMode, DeploymentType, DeploymentStatus | ||
| 17 | +from .models.schemas import DeploymentInfo, DEPLOYMENT_TABLE_NAME, DeploymentFields | ||
| 10 | from .deployments import ( | 18 | from .deployments import ( |
| 11 | CommonParams, | 19 | CommonParams, |
| 12 | DeployContext, | 20 | DeployContext, |
| @@ -33,13 +41,14 @@ from .deployments import ( | |||
| 33 | K8sStrategy, | 41 | K8sStrategy, |
| 34 | ) | 42 | ) |
| 35 | 43 | ||
| 36 | -from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler | ||
| 37 | -from openjiuwen_runtime.foundation.db.mysql_handler import MySQLHandler | ||
| 38 | - | ||
| 39 | __all__ = [ | 44 | __all__ = [ |
| 40 | # Manager | 45 | # Manager |
| 41 | "DeploymentManager", | 46 | "DeploymentManager", |
| 42 | "DeployMode", | 47 | "DeployMode", |
| 48 | + # Deployment params | ||
| 49 | + "DeployAgentParams", | ||
| 50 | + "DeployPluginParams", | ||
| 51 | + "ListDeploymentsParams", | ||
| 43 | # Enums | 52 | # Enums |
| 44 | "DeploymentType", | 53 | "DeploymentType", |
| 45 | "DeploymentStatus", | 54 | "DeploymentStatus", |
| @@ -8,10 +8,11 @@ from typing import Any, Optional, Generic, TypeVar | |||
| 8 | from datetime import datetime | 8 | from datetime import datetime |
| 9 | from openjiuwen_runtime.foundation.log import get_logger | 9 | from openjiuwen_runtime.foundation.log import get_logger |
| 10 | from openjiuwen_runtime.foundation.db.handler import DBHandler | 10 | from openjiuwen_runtime.foundation.db.handler import DBHandler |
| 11 | -from .models import DeployContext, DeployResult | ||
| 12 | -from .deployer import Deployer | ||
| 13 | -from ...models.enums import DeploymentStatus | ||
| 14 | from openjiuwen_runtime.foundation.db.table_def import TableDefinition | 11 | from openjiuwen_runtime.foundation.db.table_def import TableDefinition |
| 12 | + | ||
| 13 | +from .deployer import Deployer | ||
| 14 | +from .models import DeployContext, DeployResult | ||
| 15 | +from ...models.enums import DeploymentStatus | ||
| 15 | from ...models.schemas import DEPLOYMENT_TABLE_NAME, DeploymentFields | 16 | from ...models.schemas import DEPLOYMENT_TABLE_NAME, DeploymentFields |
| 16 | 17 | ||
| 17 | T = TypeVar("T") | 18 | T = TypeVar("T") |
| @@ -12,19 +12,19 @@ | |||
| 12 | 12 | ||
| 13 | import asyncio | 13 | import asyncio |
| 14 | import os | 14 | import os |
| 15 | -import subprocess | ||
| 16 | import shutil | 15 | import shutil |
| 16 | +import subprocess | ||
| 17 | from pathlib import Path | 17 | from pathlib import Path |
| 18 | from typing import Optional | 18 | from typing import Optional |
| 19 | 19 | ||
| 20 | +from openjiuwen_runtime.foundation.config import settings | ||
| 21 | +from openjiuwen_runtime.foundation.log import get_logger | ||
| 22 | + | ||
| 20 | from ..base.deployer import Deployer | 23 | from ..base.deployer import Deployer |
| 21 | from ..base.models import DeployContext, DeployResult | 24 | from ..base.models import DeployContext, DeployResult |
| 22 | from .models import DockerParams | 25 | from .models import DockerParams |
| 23 | from ...models.enums import DeploymentStatus | 26 | from ...models.enums import DeploymentStatus |
| 24 | 27 | ||
| 25 | -from openjiuwen_runtime.foundation.config import settings | ||
| 26 | -from openjiuwen_runtime.foundation.log import get_logger | ||
| 27 | - | ||
| 28 | logger = get_logger(__name__) | 28 | logger = get_logger(__name__) |
| 29 | 29 | ||
| 30 | 30 | ||
| @@ -6,14 +6,13 @@ | |||
| 6 | from datetime import datetime | 6 | from datetime import datetime |
| 7 | from typing import Any | 7 | from typing import Any |
| 8 | 8 | ||
| 9 | -from ..base.strategy import BaseDeploymentStrategy | 9 | +from openjiuwen_runtime.foundation.log import get_logger |
| 10 | -from ..base.models import DeployContext, CommonParams | ||
| 11 | -from .models import DockerParams, DockerInfo, DOCKER_TABLE_DEF | ||
| 12 | -from .deployer import DockerDeployer | ||
| 13 | - | ||
| 14 | from openjiuwen_runtime.foundation.log.utils import mask_userdata | 10 | from openjiuwen_runtime.foundation.log.utils import mask_userdata |
| 15 | 11 | ||
| 16 | -from openjiuwen_runtime.foundation.log import get_logger | 12 | +from ..base.models import CommonParams, DeployContext |
| 13 | +from ..base.strategy import BaseDeploymentStrategy | ||
| 14 | +from .deployer import DockerDeployer | ||
| 15 | +from .models import DOCKER_TABLE_DEF, DockerInfo, DockerParams | ||
| 17 | 16 | ||
| 18 | logger = get_logger(__name__) | 17 | logger = get_logger(__name__) |
| 19 | 18 | ||
| @@ -7,6 +7,8 @@ import asyncio | |||
| 7 | import os | 7 | import os |
| 8 | from typing import Optional | 8 | from typing import Optional |
| 9 | 9 | ||
| 10 | +from openjiuwen_runtime.foundation.log import get_logger | ||
| 11 | + | ||
| 10 | from ..base.deployer import Deployer | 12 | from ..base.deployer import Deployer |
| 11 | from ..base.models import DeployContext, DeployResult | 13 | from ..base.models import DeployContext, DeployResult |
| 12 | from .models import K8sParams | 14 | from .models import K8sParams |
| @@ -15,9 +15,11 @@ import os | |||
| 15 | import shutil | 15 | import shutil |
| 16 | import subprocess | 16 | import subprocess |
| 17 | import sys | 17 | import sys |
| 18 | +from pathlib import Path | ||
| 18 | from typing import Dict | 19 | from typing import Dict |
| 19 | 20 | ||
| 20 | from openjiuwen_runtime.foundation.config import settings | 21 | from openjiuwen_runtime.foundation.config import settings |
| 22 | +from openjiuwen_runtime.foundation.log import get_logger | ||
| 21 | from openjiuwen_runtime.foundation.log.utils import mask_userdata | 23 | from openjiuwen_runtime.foundation.log.utils import mask_userdata |
| 22 | from openjiuwen_runtime.foundation.venv_manager import VirtualEnvironmentManager | 24 | from openjiuwen_runtime.foundation.venv_manager import VirtualEnvironmentManager |
| 23 | 25 | ||
| @@ -25,11 +27,15 @@ from ...models.enums import DeploymentStatus | |||
| 25 | from ..base.deployer import Deployer | 27 | from ..base.deployer import Deployer |
| 26 | from ..base.models import DeployContext, DeployResult | 28 | from ..base.models import DeployContext, DeployResult |
| 27 | from .models import SubprocessParams | 29 | from .models import SubprocessParams |
| 28 | -from openjiuwen_runtime.foundation.log import get_logger | ||
| 29 | 30 | ||
| 30 | logger = get_logger(__name__) | 31 | logger = get_logger(__name__) |
| 31 | 32 | ||
| 32 | 33 | ||
| 34 | +def _windows_system32_exe(filename: str) -> str: | ||
| 35 | + system_root = os.environ.get("SYSTEMROOT", r"C:\Windows") | ||
| 36 | + return str(Path(system_root) / "System32" / filename) | ||
| 37 | + | ||
| 38 | + | ||
| 33 | class LocalSubprocessDeployer(Deployer[SubprocessParams]): | 39 | class LocalSubprocessDeployer(Deployer[SubprocessParams]): |
| 34 | """本地进程部署器""" | 40 | """本地进程部署器""" |
| 35 | 41 | ||
| @@ -47,12 +53,12 @@ class LocalSubprocessDeployer(Deployer[SubprocessParams]): | |||
| 47 | """通过 PID 终止进程(跨进程有效)""" | 53 | """通过 PID 终止进程(跨进程有效)""" |
| 48 | try: | 54 | try: |
| 49 | if sys.platform == "win32": | 55 | if sys.platform == "win32": |
| 50 | - cmd = f"taskkill /F /PID {pid}" | 56 | + taskkill = _windows_system32_exe("taskkill.exe") |
| 51 | result = subprocess.run( | 57 | result = subprocess.run( |
| 52 | - cmd, | 58 | + [taskkill, "/F", "/PID", str(pid)], |
| 53 | - shell=True, | 59 | + shell=False, |
| 54 | capture_output=True, | 60 | capture_output=True, |
| 55 | - text=True | 61 | + text=True, |
| 56 | ) | 62 | ) |
| 57 | success = result.returncode == 0 | 63 | success = result.returncode == 0 |
| 58 | if success: | 64 | if success: |
| @@ -62,9 +68,9 @@ class LocalSubprocessDeployer(Deployer[SubprocessParams]): | |||
| 62 | return success | 68 | return success |
| 63 | else: | 69 | else: |
| 64 | result = subprocess.run( | 70 | result = subprocess.run( |
| 65 | - ["kill", "-9", str(pid)], | 71 | + ["/bin/kill", "-9", str(pid)], |
| 66 | capture_output=True, | 72 | capture_output=True, |
| 67 | - text=True | 73 | + text=True, |
| 68 | ) | 74 | ) |
| 69 | success = result.returncode == 0 | 75 | success = result.returncode == 0 |
| 70 | if success: | 76 | if success: |
| @@ -79,16 +85,17 @@ class LocalSubprocessDeployer(Deployer[SubprocessParams]): | |||
| 79 | def _check_process_by_pid(self, pid: int) -> bool: | 85 | def _check_process_by_pid(self, pid: int) -> bool: |
| 80 | """通过 PID 检查进程是否运行""" | 86 | """通过 PID 检查进程是否运行""" |
| 81 | if sys.platform == "win32": | 87 | if sys.platform == "win32": |
| 88 | + tasklist = _windows_system32_exe("tasklist.exe") | ||
| 82 | result = subprocess.run( | 89 | result = subprocess.run( |
| 83 | - ["tasklist", "/FI", f"PID eq {pid}"], | 90 | + [tasklist, "/FI", f"PID eq {pid}"], |
| 84 | capture_output=True, | 91 | capture_output=True, |
| 85 | - text=True | 92 | + text=True, |
| 86 | ) | 93 | ) |
| 87 | return str(pid) in result.stdout | 94 | return str(pid) in result.stdout |
| 88 | else: | 95 | else: |
| 89 | result = subprocess.run( | 96 | result = subprocess.run( |
| 90 | - ["kill", "-0", str(pid)], | 97 | + ["/bin/kill", "-0", str(pid)], |
| 91 | - capture_output=True | 98 | + capture_output=True, |
| 92 | ) | 99 | ) |
| 93 | return result.returncode == 0 | 100 | return result.returncode == 0 |
| 94 | 101 | ||
| @@ -191,7 +198,9 @@ class LocalSubprocessDeployer(Deployer[SubprocessParams]): | |||
| 191 | if process.poll() is not None: | 198 | if process.poll() is not None: |
| 192 | # 进程已经退出,读取错误信息 | 199 | # 进程已经退出,读取错误信息 |
| 193 | stdout, stderr = process.communicate() | 200 | stdout, stderr = process.communicate() |
| 194 | - error_msg = stderr.decode('utf-8', errors='ignore') or stdout.decode('utf-8', errors='ignore') or "Unknown error" | 201 | + stderr_txt = stderr.decode("utf-8", errors="ignore") |
| 202 | + stdout_txt = stdout.decode("utf-8", errors="ignore") | ||
| 203 | + error_msg = stderr_txt or stdout_txt or "Unknown error" | ||
| 195 | logger.error("Process exited for %s: %s", deployment_id, error_msg) | 204 | logger.error("Process exited for %s: %s", deployment_id, error_msg) |
| 196 | raise RuntimeError(f"Process exited: {error_msg}") | 205 | raise RuntimeError(f"Process exited: {error_msg}") |
| 197 | 206 | ||
| @@ -3,8 +3,8 @@ | |||
| 3 | 3 | ||
| 4 | import asyncio | 4 | import asyncio |
| 5 | import uuid | 5 | import uuid |
| 6 | +from dataclasses import dataclass, field | ||
| 6 | from datetime import datetime | 7 | from datetime import datetime |
| 7 | -from enum import Enum | ||
| 8 | from typing import Any, Optional | 8 | from typing import Any, Optional |
| 9 | from urllib import request as urllib_request | 9 | from urllib import request as urllib_request |
| 10 | from urllib.parse import urljoin, urlparse, urlunparse | 10 | from urllib.parse import urljoin, urlparse, urlunparse |
| @@ -21,7 +21,12 @@ from .deployments import ( | |||
| 21 | DockerStrategy, | 21 | DockerStrategy, |
| 22 | K8sStrategy, | 22 | K8sStrategy, |
| 23 | ) | 23 | ) |
| 24 | -from .models.enums import DeploymentType, DeploymentStatus | 24 | +from .models.deployment_params import ( |
| 25 | + DeployAgentParams, | ||
| 26 | + DeployPluginParams, | ||
| 27 | + ListDeploymentsParams, | ||
| 28 | +) | ||
| 29 | +from .models.enums import DeployMode, DeploymentType, DeploymentStatus | ||
| 25 | from .models.schemas import ( | 30 | from .models.schemas import ( |
| 26 | DeploymentInfo, | 31 | DeploymentInfo, |
| 27 | DeploymentCreate, | 32 | DeploymentCreate, |
| @@ -32,11 +37,17 @@ from .models.schemas import ( | |||
| 32 | logger = get_logger(__name__) | 37 | logger = get_logger(__name__) |
| 33 | 38 | ||
| 34 | 39 | ||
| 35 | -class DeployMode(str, Enum): | 40 | +@dataclass |
| 36 | - """部署模式""" | 41 | +class _DeployExecutionParams: |
| 37 | - SUBPROCESS = "subprocess" | 42 | + """内部统一部署流程参数(由 deploy_agent / deploy_plugin 组装)""" |
| 38 | - DOCKER = "docker" | 43 | + |
| 39 | - K8S = "k8s" | 44 | + deployment_type: DeploymentType |
| 45 | + name: str | ||
| 46 | + version: str | ||
| 47 | + mode: DeployMode | ||
| 48 | + user_id: Optional[str] = None | ||
| 49 | + space_id: Optional[str] = None | ||
| 50 | + extras: dict[str, Any] = field(default_factory=dict) | ||
| 40 | 51 | ||
| 41 | 52 | ||
| 42 | DEPLOYMENT_TABLE_DEF = TableDefinition( | 53 | DEPLOYMENT_TABLE_DEF = TableDefinition( |
| @@ -124,96 +135,80 @@ class DeploymentManager: | |||
| 124 | await self.db_handler.disconnect() | 135 | await self.db_handler.disconnect() |
| 125 | logger.info("DeploymentManager shutdown complete") | 136 | logger.info("DeploymentManager shutdown complete") |
| 126 | 137 | ||
| 127 | - async def deploy_agent( | 138 | + async def deploy_agent(self, params: DeployAgentParams) -> DeploymentInfo: |
| 128 | - self, | ||
| 129 | - name: str, | ||
| 130 | - version: str, | ||
| 131 | - mode: DeployMode = DeployMode.SUBPROCESS, | ||
| 132 | - user_id: Optional[str] = None, | ||
| 133 | - space_id: Optional[str] = None, | ||
| 134 | - **kwargs: Any, | ||
| 135 | - ) -> DeploymentInfo: | ||
| 136 | """部署Agent""" | 139 | """部署Agent""" |
| 137 | logger.info( | 140 | logger.info( |
| 138 | "Deploying agent: name=%s, version=%s, mode=%s, user_id=%s, space_id=%s", | 141 | "Deploying agent: name=%s, version=%s, mode=%s, user_id=%s, space_id=%s", |
| 139 | - name, | 142 | + params.name, |
| 140 | - version, | 143 | + params.version, |
| 141 | - mode, | 144 | + params.mode, |
| 142 | - user_id, | 145 | + params.user_id, |
| 143 | - space_id, | 146 | + params.space_id, |
| 144 | ) | 147 | ) |
| 145 | - kwargs["package_name"] = name | 148 | + extras = dict(params.extras) |
| 149 | + extras["package_name"] = params.name | ||
| 146 | return await self._deploy( | 150 | return await self._deploy( |
| 147 | - deployment_type=DeploymentType.AGENT, | 151 | + _DeployExecutionParams( |
| 148 | - name=name, | 152 | + deployment_type=DeploymentType.AGENT, |
| 149 | - version=version, | 153 | + name=params.name, |
| 150 | - mode=mode, | 154 | + version=params.version, |
| 151 | - user_id=user_id, | 155 | + mode=params.mode, |
| 152 | - space_id=space_id, | 156 | + user_id=params.user_id, |
| 153 | - **kwargs, | 157 | + space_id=params.space_id, |
| 158 | + extras=extras, | ||
| 159 | + ) | ||
| 154 | ) | 160 | ) |
| 155 | 161 | ||
| 156 | - async def deploy_plugin( | 162 | + async def deploy_plugin(self, params: DeployPluginParams) -> DeploymentInfo: |
| 157 | - self, | ||
| 158 | - name: str, | ||
| 159 | - version: str, | ||
| 160 | - mode: DeployMode = DeployMode.SUBPROCESS, | ||
| 161 | - url: Optional[str] = None, | ||
| 162 | - user_id: Optional[str] = None, | ||
| 163 | - space_id: Optional[str] = None, | ||
| 164 | - **kwargs: Any, | ||
| 165 | - ) -> DeploymentInfo: | ||
| 166 | """部署Plugin""" | 163 | """部署Plugin""" |
| 167 | logger.info( | 164 | logger.info( |
| 168 | "Deploying plugin: name=%s, version=%s, mode=%s, user_id=%s, space_id=%s", | 165 | "Deploying plugin: name=%s, version=%s, mode=%s, user_id=%s, space_id=%s", |
| 169 | - name, | 166 | + params.name, |
| 170 | - version, | 167 | + params.version, |
| 171 | - mode, | 168 | + params.mode, |
| 172 | - user_id, | 169 | + params.user_id, |
| 173 | - space_id, | 170 | + params.space_id, |
| 174 | ) | 171 | ) |
| 172 | + extras = dict(params.extras) | ||
| 173 | + extras["url"] = params.url | ||
| 175 | return await self._deploy( | 174 | return await self._deploy( |
| 176 | - deployment_type=DeploymentType.PLUGIN, | 175 | + _DeployExecutionParams( |
| 177 | - name=name, | 176 | + deployment_type=DeploymentType.PLUGIN, |
| 178 | - version=version, | 177 | + name=params.name, |
| 179 | - mode=mode, | 178 | + version=params.version, |
| 180 | - url=url, | 179 | + mode=params.mode, |
| 181 | - user_id=user_id, | 180 | + user_id=params.user_id, |
| 182 | - space_id=space_id, | 181 | + space_id=params.space_id, |
| 183 | - **kwargs, | 182 | + extras=extras, |
| 183 | + ) | ||
| 184 | ) | 184 | ) |
| 185 | 185 | ||
| 186 | - async def list_deployments( | 186 | + async def list_deployments(self, params: ListDeploymentsParams) -> list[DeploymentInfo]: |
| 187 | - self, | ||
| 188 | - deployment_type: Optional[DeploymentType] = None, | ||
| 189 | - deployment_status: Optional[DeploymentStatus] = None, | ||
| 190 | - user_id: Optional[str] = None, | ||
| 191 | - space_id: Optional[str] = None, | ||
| 192 | - limit: int = 100, | ||
| 193 | - offset: int = 0, | ||
| 194 | - ) -> list[DeploymentInfo]: | ||
| 195 | """列出部署""" | 187 | """列出部署""" |
| 196 | logger.debug( | 188 | logger.debug( |
| 197 | "Listing deployments: type=%s, status=%s, user_id=%s, space_id=%s, limit=%s, offset=%s", | 189 | "Listing deployments: type=%s, status=%s, user_id=%s, space_id=%s, limit=%s, offset=%s", |
| 198 | - deployment_type, | 190 | + params.deployment_type, |
| 199 | - deployment_status, | 191 | + params.deployment_status, |
| 200 | - user_id, | 192 | + params.user_id, |
| 201 | - space_id, | 193 | + params.space_id, |
| 202 | - limit, | 194 | + params.limit, |
| 203 | - offset, | 195 | + params.offset, |
| 204 | ) | 196 | ) |
| 205 | filters = {} | 197 | filters = {} |
| 206 | - if deployment_type: | 198 | + if params.deployment_type: |
| 207 | - filters[DeploymentFields.DEPLOYMENT_TYPE] = deployment_type.value | 199 | + filters[DeploymentFields.DEPLOYMENT_TYPE] = params.deployment_type.value |
| 208 | - if deployment_status: | 200 | + if params.deployment_status: |
| 209 | - filters[DeploymentFields.DEPLOYMENT_STATUS] = deployment_status.value | 201 | + filters[DeploymentFields.DEPLOYMENT_STATUS] = params.deployment_status.value |
| 210 | - if user_id: | 202 | + if params.user_id: |
| 211 | - filters[DeploymentFields.USER_ID] = user_id | 203 | + filters[DeploymentFields.USER_ID] = params.user_id |
| 212 | - if space_id: | 204 | + if params.space_id: |
| 213 | - filters[DeploymentFields.SPACE_ID] = space_id | 205 | + filters[DeploymentFields.SPACE_ID] = params.space_id |
| 214 | 206 | ||
| 215 | records = await self.db_handler.list_records( | 207 | records = await self.db_handler.list_records( |
| 216 | - DEPLOYMENT_TABLE_NAME, filters=filters if filters else None, limit=limit, offset=offset | 208 | + DEPLOYMENT_TABLE_NAME, |
| 209 | + filters=filters if filters else None, | ||
| 210 | + limit=params.limit, | ||
| 211 | + offset=params.offset, | ||
| 217 | ) | 212 | ) |
| 218 | result = [DeploymentInfo.model_validate(r if hasattr(r, "to_dict") else r) for r in records] | 213 | result = [DeploymentInfo.model_validate(r if hasattr(r, "to_dict") else r) for r in records] |
| 219 | logger.debug("Found %s deployments", len(result)) | 214 | logger.debug("Found %s deployments", len(result)) |
| @@ -384,49 +379,50 @@ class DeploymentManager: | |||
| 384 | f"Deployment {deployment_id} not ready within {timeout_seconds}s (last_status={last_status})" | 379 | f"Deployment {deployment_id} not ready within {timeout_seconds}s (last_status={last_status})" |
| 385 | ) | 380 | ) |
| 386 | 381 | ||
| 387 | - async def _deploy( | 382 | + async def _deploy(self, params: _DeployExecutionParams) -> DeploymentInfo: |
| 388 | - self, | ||
| 389 | - deployment_type: DeploymentType, | ||
| 390 | - name: str, | ||
| 391 | - version: str, | ||
| 392 | - mode: DeployMode, | ||
| 393 | - user_id: Optional[str] = None, | ||
| 394 | - space_id: Optional[str] = None, | ||
| 395 | - **kwargs: Any, | ||
| 396 | - ) -> DeploymentInfo: | ||
| 397 | """内部部署方法""" | 383 | """内部部署方法""" |
| 398 | - deployment_id = kwargs.pop("deployment_id", None) or self._generate_deployment_id() | 384 | + extras = dict(params.extras) |
| 385 | + deployment_id = extras.pop("deployment_id", None) or self._generate_deployment_id() | ||
| 399 | now = datetime.utcnow() | 386 | now = datetime.utcnow() |
| 400 | 387 | ||
| 401 | - logger.debug("deployment_id=%s, type=%s, name=%s", deployment_id, deployment_type, name) | 388 | + logger.debug( |
| 389 | + "deployment_id=%s, type=%s, name=%s", | ||
| 390 | + deployment_id, | ||
| 391 | + params.deployment_type, | ||
| 392 | + params.name, | ||
| 393 | + ) | ||
| 402 | 394 | ||
| 403 | create_model = DeploymentCreate( | 395 | create_model = DeploymentCreate( |
| 404 | deployment_id=deployment_id, | 396 | deployment_id=deployment_id, |
| 405 | - version=version, | 397 | + version=params.version, |
| 406 | - deployment_type=deployment_type, | 398 | + deployment_type=params.deployment_type, |
| 407 | - name=name, | 399 | + name=params.name, |
| 408 | - url=kwargs.get("url"), | 400 | + url=extras.get("url"), |
| 409 | - user_id=user_id, | 401 | + user_id=params.user_id, |
| 410 | - space_id=space_id, | 402 | + space_id=params.space_id, |
| 411 | - data=kwargs.get("data"), | 403 | + data=extras.get("data"), |
| 412 | ) | 404 | ) |
| 413 | deployment_data = create_model.model_dump() | 405 | deployment_data = create_model.model_dump() |
| 414 | deployment_data[DeploymentFields.DEPLOYMENT_STATUS] = DeploymentStatus.PENDING.value | 406 | deployment_data[DeploymentFields.DEPLOYMENT_STATUS] = DeploymentStatus.PENDING.value |
| 415 | deployment_data[DeploymentFields.CREATED_AT] = now | 407 | deployment_data[DeploymentFields.CREATED_AT] = now |
| 416 | deployment_data[DeploymentFields.UPDATED_AT] = now | 408 | deployment_data[DeploymentFields.UPDATED_AT] = now |
| 417 | - | 409 | + |
| 418 | await self.db_handler.create(DEPLOYMENT_TABLE_NAME, deployment_data) | 410 | await self.db_handler.create(DEPLOYMENT_TABLE_NAME, deployment_data) |
| 419 | 411 | ||
| 420 | - strategy = self._get_strategy(mode) | 412 | + strategy = self._get_strategy(params.mode) |
| 421 | 413 | ||
| 422 | try: | 414 | try: |
| 423 | await strategy.create_record( | 415 | await strategy.create_record( |
| 424 | - self.db_handler, deployment_id, version, **kwargs | 416 | + self.db_handler, deployment_id, params.version, **extras |
| 425 | ) | 417 | ) |
| 426 | await strategy.deploy(deployment_id, self.db_handler) | 418 | await strategy.deploy(deployment_id, self.db_handler) |
| 427 | 419 | ||
| 428 | await self._wait_until_deployment_ready(deployment_id) | 420 | await self._wait_until_deployment_ready(deployment_id) |
| 429 | - logger.info("Deployment completed: deployment_id=%s, name=%s", deployment_id, name) | 421 | + logger.info( |
| 422 | + "Deployment completed: deployment_id=%s, name=%s", | ||
| 423 | + deployment_id, | ||
| 424 | + params.name, | ||
| 425 | + ) | ||
| 430 | except Exception as e: | 426 | except Exception as e: |
| 431 | logger.error("Deployment failed: deployment_id=%s, error=%s", deployment_id, str(e)) | 427 | logger.error("Deployment failed: deployment_id=%s, error=%s", deployment_id, str(e)) |
| 432 | await self.db_handler.update( | 428 | await self.db_handler.update( |
| @@ -436,7 +432,7 @@ class DeploymentManager: | |||
| 436 | ) | 432 | ) |
| 437 | 433 | ||
| 438 | deployment_record = await self.db_handler.get( | 434 | deployment_record = await self.db_handler.get( |
| 439 | - DEPLOYMENT_TABLE_NAME, | 435 | + DEPLOYMENT_TABLE_NAME, |
| 440 | - {DeploymentFields.DEPLOYMENT_ID: deployment_id} | 436 | + {DeploymentFields.DEPLOYMENT_ID: deployment_id}, |
| 441 | ) | 437 | ) |
| 442 | return DeploymentInfo.model_validate(deployment_record) | 438 | return DeploymentInfo.model_validate(deployment_record) |
| @@ -3,14 +3,15 @@ | |||
| 3 | 3 | ||
| 4 | """数据模型""" | 4 | """数据模型""" |
| 5 | 5 | ||
| 6 | -from .enums import DeploymentType, DeploymentStatus | 6 | +from openjiuwen_runtime.foundation.db.table_def import ColumnDefinition, IndexDefinition, TableDefinition |
| 7 | + | ||
| 8 | +from .enums import DeploymentStatus, DeploymentType | ||
| 7 | from .schemas import ( | 9 | from .schemas import ( |
| 8 | - DeploymentInfo, | ||
| 9 | - DeploymentCreate, | ||
| 10 | DEPLOYMENT_TABLE_NAME, | 10 | DEPLOYMENT_TABLE_NAME, |
| 11 | + DeploymentCreate, | ||
| 11 | DeploymentFields, | 12 | DeploymentFields, |
| 13 | + DeploymentInfo, | ||
| 12 | ) | 14 | ) |
| 13 | -from openjiuwen_runtime.foundation.db.table_def import TableDefinition, ColumnDefinition, IndexDefinition | ||
| 14 | 15 | ||
| 15 | __all__ = [ | 16 | __all__ = [ |
| 16 | "DeploymentType", | 17 | "DeploymentType", |
| @@ -0,0 +1,46 @@ | |||
| 1 | +# coding: utf-8 | ||
| 2 | +# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved | ||
| 3 | + | ||
| 4 | +"""部署相关请求参数(收敛 Manager 方法形参数量)""" | ||
| 5 | + | ||
| 6 | +from dataclasses import dataclass, field | ||
| 7 | +from typing import Any, Optional | ||
| 8 | + | ||
| 9 | +from .enums import DeployMode, DeploymentStatus, DeploymentType | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +class ListDeploymentsParams: | ||
| 14 | + """列出部署的过滤与分页条件""" | ||
| 15 | + | ||
| 16 | + deployment_type: Optional[DeploymentType] = None | ||
| 17 | + deployment_status: Optional[DeploymentStatus] = None | ||
| 18 | + user_id: Optional[str] = None | ||
| 19 | + space_id: Optional[str] = None | ||
| 20 | + limit: int = 100 | ||
| 21 | + offset: int = 0 | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +class DeployAgentParams: | ||
| 26 | + """部署 Agent 的参数(策略扩展字段放入 extras)""" | ||
| 27 | + | ||
| 28 | + name: str | ||
| 29 | + version: str | ||
| 30 | + mode: DeployMode = DeployMode.SUBPROCESS | ||
| 31 | + user_id: Optional[str] = None | ||
| 32 | + space_id: Optional[str] = None | ||
| 33 | + extras: dict[str, Any] = field(default_factory=dict) | ||
| 34 | + | ||
| 35 | + | ||
| 36 | + | ||
| 37 | +class DeployPluginParams: | ||
| 38 | + """部署 Plugin 的参数(策略扩展字段放入 extras)""" | ||
| 39 | + | ||
| 40 | + name: str | ||
| 41 | + version: str | ||
| 42 | + mode: DeployMode = DeployMode.SUBPROCESS | ||
| 43 | + url: Optional[str] = None | ||
| 44 | + user_id: Optional[str] = None | ||
| 45 | + space_id: Optional[str] = None | ||
| 46 | + extras: dict[str, Any] = field(default_factory=dict) | ||
| @@ -18,3 +18,10 @@ class DeploymentStatus(str, Enum): | |||
| 18 | RUNNING = "running" | 18 | RUNNING = "running" |
| 19 | STOPPED = "stopped" | 19 | STOPPED = "stopped" |
| 20 | FAILED = "failed" | 20 | FAILED = "failed" |
| 21 | + | ||
| 22 | + | ||
| 23 | +class DeployMode(str, Enum): | ||
| 24 | + """部署模式""" | ||
| 25 | + SUBPROCESS = "subprocess" | ||
| 26 | + DOCKER = "docker" | ||
| 27 | + K8S = "k8s" | ||
| @@ -10,7 +10,11 @@ import unittest | |||
| 10 | from pathlib import Path | 10 | from pathlib import Path |
| 11 | 11 | ||
| 12 | from openjiuwen_runtime.foundation.log import get_logger | 12 | from openjiuwen_runtime.foundation.log import get_logger |
| 13 | -from openjiuwen_runtime.management import DeploymentManager, DeployMode | 13 | +from openjiuwen_runtime.management import ( |
| 14 | + DeployAgentParams, | ||
| 15 | + DeploymentManager, | ||
| 16 | + DeployMode, | ||
| 17 | +) | ||
| 14 | from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler | 18 | from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler |
| 15 | from openjiuwen_runtime.foundation.packaging import package_python_to_whl | 19 | from openjiuwen_runtime.foundation.packaging import package_python_to_whl |
| 16 | 20 | ||
| @@ -56,11 +60,12 @@ class ManagerTest(unittest.IsolatedAsyncioTestCase): | |||
| 56 | self.assertTrue(os.path.exists(whl_path), f"WHL file not found: {whl_path}") | 60 | self.assertTrue(os.path.exists(whl_path), f"WHL file not found: {whl_path}") |
| 57 | 61 | ||
| 58 | deployment_info = await self.manager.deploy_agent( | 62 | deployment_info = await self.manager.deploy_agent( |
| 59 | - name="test_simple_agent", | 63 | + DeployAgentParams( |
| 60 | - version="1.0.0", | 64 | + name="test_simple_agent", |
| 61 | - mode=DeployMode.SUBPROCESS, | 65 | + version="1.0.0", |
| 62 | - package_name="simple_agent", | 66 | + mode=DeployMode.SUBPROCESS, |
| 63 | - whl_path=whl_path, | 67 | + extras={"package_name": "simple_agent", "whl_path": whl_path}, |
| 68 | + ) | ||
| 64 | ) | 69 | ) |
| 65 | 70 | ||
| 66 | self.assertIsNotNone(deployment_info) | 71 | self.assertIsNotNone(deployment_info) |
| @@ -96,11 +101,12 @@ class ManagerTest(unittest.IsolatedAsyncioTestCase): | |||
| 96 | project_root / "resources" / "examples" / "simple_agent" / "openjiuwen_agent-1.0.0-py3-none-any.whl") | 101 | project_root / "resources" / "examples" / "simple_agent" / "openjiuwen_agent-1.0.0-py3-none-any.whl") |
| 97 | 102 | ||
| 98 | deployment_info = await self.manager.deploy_agent( | 103 | deployment_info = await self.manager.deploy_agent( |
| 99 | - name="test_simple_agent", | 104 | + DeployAgentParams( |
| 100 | - version="1.0.0", | 105 | + name="test_simple_agent", |
| 101 | - mode=DeployMode.SUBPROCESS, | 106 | + version="1.0.0", |
| 102 | - package_name="simple_agent", | 107 | + mode=DeployMode.SUBPROCESS, |
| 103 | - whl_path=whl_path, | 108 | + extras={"package_name": "simple_agent", "whl_path": whl_path}, |
| 109 | + ) | ||
| 104 | ) | 110 | ) |
| 105 | 111 | ||
| 106 | self.assertIsNotNone(deployment_info) | 112 | self.assertIsNotNone(deployment_info) |
| @@ -8,9 +8,11 @@ FastAPI 服务器,提供 Agent 部署管理 REST API(支持租户隔离) | |||
| 8 | 8 | ||
| 9 | import uuid | 9 | import uuid |
| 10 | from contextlib import asynccontextmanager | 10 | from contextlib import asynccontextmanager |
| 11 | +from dataclasses import dataclass | ||
| 11 | from pathlib import Path | 12 | from pathlib import Path |
| 13 | +from typing import Annotated | ||
| 12 | 14 | ||
| 13 | -from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, status | 15 | +from fastapi import Depends, FastAPI, HTTPException, Query, Request, UploadFile, status |
| 14 | from fastapi.responses import JSONResponse | 16 | from fastapi.responses import JSONResponse |
| 15 | 17 | ||
| 16 | from openjiuwen_runtime.foundation.log import get_logger | 18 | from openjiuwen_runtime.foundation.log import get_logger |
| @@ -18,8 +20,16 @@ from openjiuwen_runtime.foundation.config import settings | |||
| 18 | from openjiuwen_runtime.foundation.db.mysql_handler import MySQLHandler | 20 | from openjiuwen_runtime.foundation.db.mysql_handler import MySQLHandler |
| 19 | from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler | 21 | from openjiuwen_runtime.foundation.db.sqlite_handler import SQLiteHandler |
| 20 | from openjiuwen_runtime.foundation.port_utils import allocate_port, is_port_available | 22 | from openjiuwen_runtime.foundation.port_utils import allocate_port, is_port_available |
| 21 | -from openjiuwen_runtime.management.manager import DeploymentManager, DeployMode | 23 | +from openjiuwen_runtime.management.manager import DeploymentManager |
| 22 | -from openjiuwen_runtime.management.models.enums import DeploymentType, DeploymentStatus | 24 | +from openjiuwen_runtime.management.models.deployment_params import ( |
| 25 | + DeployAgentParams, | ||
| 26 | + ListDeploymentsParams, | ||
| 27 | +) | ||
| 28 | +from openjiuwen_runtime.management.models.enums import ( | ||
| 29 | + DeployMode, | ||
| 30 | + DeploymentStatus, | ||
| 31 | + DeploymentType, | ||
| 32 | +) | ||
| 23 | 33 | ||
| 24 | from .middleware.tenant import TenantContextMiddleware, get_tenant_context | 34 | from .middleware.tenant import TenantContextMiddleware, get_tenant_context |
| 25 | from .utils import mask_userdata | 35 | from .utils import mask_userdata |
| @@ -125,16 +135,32 @@ def get_deploy_type(mode: str) -> str: | |||
| 125 | return mode | 135 | return mode |
| 126 | 136 | ||
| 127 | 137 | ||
| 138 | + | ||
| 139 | +class AgentDeployQuery: | ||
| 140 | + """Agent 部署请求中的查询参数(用于收敛 FastAPI 路由形参个数)。""" | ||
| 141 | + | ||
| 142 | + name: str | ||
| 143 | + mode: str | ||
| 144 | + port: int | None | ||
| 145 | + userdata: str | None | ||
| 146 | + | ||
| 147 | + | ||
| 148 | +def _parse_agent_deploy_query( | ||
| 149 | + name: str = Query(..., description="部署名称(=包名)"), | ||
| 150 | + mode: str = Query(default="subprocess", description="部署器类型"), | ||
| 151 | + port: int | None = Query(default=None, description="服务端口,不填则自动分配"), | ||
| 152 | + userdata: str | None = Query(default=None, description="用户自定义数据"), | ||
| 153 | +) -> AgentDeployQuery: | ||
| 154 | + return AgentDeployQuery(name=name, mode=mode, port=port, userdata=userdata) | ||
| 155 | + | ||
| 156 | + | ||
| 128 | # ==================== Agent API ==================== | 157 | # ==================== Agent API ==================== |
| 129 | 158 | ||
| 130 | 159 | ||
| 131 | async def deploy_agent( | 160 | async def deploy_agent( |
| 132 | request: Request, | 161 | request: Request, |
| 133 | file: UploadFile, | 162 | file: UploadFile, |
| 134 | - name: str = Query(..., description="部署名称(=包名)"), | 163 | + deploy_query: Annotated[AgentDeployQuery, Depends(_parse_agent_deploy_query)], |
| 135 | - mode: str = Query(default="subprocess", description="部署器类型"), | ||
| 136 | - port: int | None = Query(default=None, description="服务端口,不填则自动分配"), | ||
| 137 | - userdata: str | None = Query(default=None, description="用户自定义数据"), | ||
| 138 | ): | 164 | ): |
| 139 | """ | 165 | """ |
| 140 | 部署 Agent(JSON 配置,低码方式) | 166 | 部署 Agent(JSON 配置,低码方式) |
| @@ -147,8 +173,10 @@ async def deploy_agent( | |||
| 147 | try: | 173 | try: |
| 148 | # 获取租户上下文 | 174 | # 获取租户上下文 |
| 149 | user_id, space_id = get_tenant_context(request) | 175 | user_id, space_id = get_tenant_context(request) |
| 150 | - logger.info(f"Received agent deploy request: user_id={user_id}, " | 176 | + logger.info( |
| 151 | - f"space_id={space_id}, name={name}, userdata={mask_userdata(userdata)}") | 177 | + f"Received agent deploy request: user_id={user_id}, " |
| 178 | + f"space_id={space_id}, name={deploy_query.name}, userdata={mask_userdata(deploy_query.userdata)}" | ||
| 179 | + ) | ||
| 152 | 180 | ||
| 153 | deployment_id = str(uuid.uuid4()) | 181 | deployment_id = str(uuid.uuid4()) |
| 154 | logger.info(f"Generated deployment_id: {deployment_id}") | 182 | logger.info(f"Generated deployment_id: {deployment_id}") |
| @@ -160,21 +188,27 @@ async def deploy_agent( | |||
| 160 | content = await file.read() | 188 | content = await file.read() |
| 161 | json_file_path.write_bytes(content) | 189 | json_file_path.write_bytes(content) |
| 162 | 190 | ||
| 163 | - deploy_type = get_deploy_type(mode) | 191 | + deploy_type = get_deploy_type(deploy_query.mode) |
| 164 | - port, whl_path = prepare_subprocess_deployment(deploy_type, port) | 192 | + port, whl_path = prepare_subprocess_deployment(deploy_type, deploy_query.port) |
| 165 | 193 | ||
| 166 | # 调用 Manager SDK 部署(传入 ir_path、whl_path 和 userdata) | 194 | # 调用 Manager SDK 部署(传入 ir_path、whl_path 和 userdata) |
| 167 | result = await manager.deploy_agent( | 195 | result = await manager.deploy_agent( |
| 168 | - name=name, | 196 | + DeployAgentParams( |
| 169 | - version="1.0.0", | 197 | + name=deploy_query.name, |
| 170 | - user_id=user_id, # 注入租户信息 | 198 | + version="1.0.0", |
| 171 | - space_id=space_id, # 注入租户信息 | 199 | + user_id=user_id, |
| 172 | - ir_path=str(json_file_path), # 用户上传的 JSON 配置文件路径 | 200 | + space_id=space_id, |
| 173 | - whl_path=str(whl_path), # 预编译的 whl 包路径 | 201 | + mode=DeployMode(deploy_type), |
| 174 | - mode=DeployMode(deploy_type), | 202 | + extras={ |
| 175 | - port=port, | 203 | + "ir_path": str(json_file_path), |
| 176 | - deployment_id=deployment_id, | 204 | + "whl_path": str(whl_path), |
| 177 | - data={"userdata": userdata} if userdata else None, # 用户自定义数据 | 205 | + "port": port, |
| 206 | + "deployment_id": deployment_id, | ||
| 207 | + "data": {"userdata": deploy_query.userdata} | ||
| 208 | + if deploy_query.userdata | ||
| 209 | + else None, | ||
| 210 | + }, | ||
| 211 | + ) | ||
| 178 | ) | 212 | ) |
| 179 | 213 | ||
| 180 | # 过滤内部实现细节,只返回用户需要的信息 | 214 | # 过滤内部实现细节,只返回用户需要的信息 |
| @@ -210,10 +244,14 @@ async def list_agents( | |||
| 210 | 244 | ||
| 211 | try: | 245 | try: |
| 212 | deployments = await manager.list_deployments( | 246 | deployments = await manager.list_deployments( |
| 213 | - deployment_type=DeploymentType.AGENT, | 247 | + ListDeploymentsParams( |
| 214 | - deployment_status=DeploymentStatus(status_filter) if status_filter else None, | 248 | + deployment_type=DeploymentType.AGENT, |
| 215 | - user_id=user_id, # 租户过滤 | 249 | + deployment_status=DeploymentStatus(status_filter) |
| 216 | - space_id=space_id, # 租户过滤 | 250 | + if status_filter |
| 251 | + else None, | ||
| 252 | + user_id=user_id, | ||
| 253 | + space_id=space_id, | ||
| 254 | + ) | ||
| 217 | ) | 255 | ) |
| 218 | 256 | ||
| 219 | # 过滤内部实现细节 | 257 | # 过滤内部实现细节 |
| @@ -6,8 +6,11 @@ | |||
| 6 | 用于模拟 OSS 文件上传功能 | 6 | 用于模拟 OSS 文件上传功能 |
| 7 | """ | 7 | """ |
| 8 | 8 | ||
| 9 | +import logging | ||
| 9 | from typing import Optional | 10 | from typing import Optional |
| 10 | 11 | ||
| 12 | +_LOG = logging.getLogger(__name__) | ||
| 13 | + | ||
| 11 | 14 | ||
| 12 | class MockOSSClient: | 15 | class MockOSSClient: |
| 13 | """Mock OSS 客户端""" | 16 | """Mock OSS 客户端""" |
| @@ -38,7 +41,7 @@ class MockOSSClient: | |||
| 38 | """ | 41 | """ |
| 39 | # Mock: 假设文件已上传成功,返回一个 mock URL | 42 | # Mock: 假设文件已上传成功,返回一个 mock URL |
| 40 | mock_oss_url = f"mock://oss.example.com/{object_key}" | 43 | mock_oss_url = f"mock://oss.example.com/{object_key}" |
| 41 | - print(f"[Mock OSS] Uploaded {local_file_path} -> {mock_oss_url}") | 44 | + _LOG.info("[Mock OSS] Uploaded %s -> %s", local_file_path, mock_oss_url) |
| 42 | return mock_oss_url | 45 | return mock_oss_url |
| 43 | 46 | ||
| 44 | async def delete_file(self, object_key: str) -> bool: | 47 | async def delete_file(self, object_key: str) -> bool: |
| @@ -51,5 +54,5 @@ class MockOSSClient: | |||
| 51 | Returns: | 54 | Returns: |
| 52 | 是否成功删除 | 55 | 是否成功删除 |
| 53 | """ | 56 | """ |
| 54 | - print(f"[Mock OSS] Deleted {object_key}") | 57 | + _LOG.info("[Mock OSS] Deleted %s", object_key) |
| 55 | return True | 58 | return True |
| @@ -14,7 +14,8 @@ from typing import AsyncIterator, Tuple | |||
| 14 | 14 | ||
| 15 | # 添加项目根目录到路径 | 15 | # 添加项目根目录到路径 |
| 16 | PROJECT_ROOT = Path(__file__).resolve().parent.parent | 16 | PROJECT_ROOT = Path(__file__).resolve().parent.parent |
| 17 | -sys.path.insert(0, str(PROJECT_ROOT)) | 17 | +if str(PROJECT_ROOT) not in sys.path: |
| 18 | + sys.path.append(str(PROJECT_ROOT)) | ||
| 18 | 19 | ||
| 19 | from openjiuwen_runtime.foundation.log import get_logger | 20 | from openjiuwen_runtime.foundation.log import get_logger |
| 20 | from openjiuwen_runtime.service import AgentApp, AppGroup | 21 | from openjiuwen_runtime.service import AgentApp, AppGroup |
| @@ -113,38 +113,36 @@ class AppGroup: | |||
| 113 | async def startup(): | 113 | async def startup(): |
| 114 | """按顺序执行所有挂载应用的 init 钩子""" | 114 | """按顺序执行所有挂载应用的 init 钩子""" |
| 115 | for prefix, app in self._mounted_apps.items(): | 115 | for prefix, app in self._mounted_apps.items(): |
| 116 | - if app._init_hook: | 116 | + try: |
| 117 | - try: | 117 | + if await app.run_init_hook(): |
| 118 | - await app._init_hook() | ||
| 119 | logger.info( | 118 | logger.info( |
| 120 | "[OK] %s 初始化完成 (挂载于 %s)", | 119 | "[OK] %s 初始化完成 (挂载于 %s)", |
| 121 | app.app_name, | 120 | app.app_name, |
| 122 | prefix, | 121 | prefix, |
| 123 | ) | 122 | ) |
| 124 | - except Exception as e: | 123 | + except Exception as e: |
| 125 | - logger.error( | 124 | + logger.error( |
| 126 | - "[ERROR] %s 初始化失败: %s", | 125 | + "[ERROR] %s 初始化失败: %s", |
| 127 | - app.app_name, | 126 | + app.app_name, |
| 128 | - e, | 127 | + e, |
| 129 | - exc_info=True, | 128 | + exc_info=True, |
| 130 | - ) | 129 | + ) |
| 131 | - raise | 130 | + raise |
| 132 | 131 | ||
| 133 | 132 | ||
| 134 | async def shutdown(): | 133 | async def shutdown(): |
| 135 | """按逆序执行所有挂载应用的 shutdown 钩子""" | 134 | """按逆序执行所有挂载应用的 shutdown 钩子""" |
| 136 | for prefix, app in reversed(list(self._mounted_apps.items())): | 135 | for prefix, app in reversed(list(self._mounted_apps.items())): |
| 137 | - if app._shutdown_hook: | 136 | + try: |
| 138 | - try: | 137 | + if await app.run_shutdown_hook(): |
| 139 | - await app._shutdown_hook() | ||
| 140 | logger.info("[OK] %s 关闭完成", app.app_name) | 138 | logger.info("[OK] %s 关闭完成", app.app_name) |
| 141 | - except Exception as e: | 139 | + except Exception as e: |
| 142 | - logger.warning( | 140 | + logger.warning( |
| 143 | - "[WARN] %s 关闭钩子执行失败: %s", | 141 | + "[WARN] %s 关闭钩子执行失败: %s", |
| 144 | - app.app_name, | 142 | + app.app_name, |
| 145 | - e, | 143 | + e, |
| 146 | - exc_info=True, | 144 | + exc_info=True, |
| 147 | - ) | 145 | + ) |
| 148 | 146 | ||
| 149 | def _register_routes(self): | 147 | def _register_routes(self): |
| 150 | """注册 AppGroup 特定路由""" | 148 | """注册 AppGroup 特定路由""" |
| @@ -173,7 +171,7 @@ class AppGroup: | |||
| 173 | 171 | ||
| 174 | elif isinstance(app, PluginApp): | 172 | elif isinstance(app, PluginApp): |
| 175 | app_info["type"] = "plugin" | 173 | app_info["type"] = "plugin" |
| 176 | - app_info["tools_count"] = len(app._tools) | 174 | + app_info["tools_count"] = app.tools_count |
| 177 | 175 | ||
| 178 | else: | 176 | else: |
| 179 | app_info["type"] = "base" | 177 | app_info["type"] = "base" |
| @@ -64,7 +64,7 @@ def _parse_cli_args() -> Dict[str, Any]: | |||
| 64 | file_path = str(Path(args.irpath).resolve()) | 64 | file_path = str(Path(args.irpath).resolve()) |
| 65 | if not Path(args.irpath).exists(): | 65 | if not Path(args.irpath).exists(): |
| 66 | logger.error("配置文件不存在: %s", file_path) | 66 | logger.error("配置文件不存在: %s", file_path) |
| 67 | - sys.exit(1) | 67 | + parser.error(f"配置文件不存在: {file_path}") |
| 68 | result["file"] = file_path | 68 | result["file"] = file_path |
| 69 | 69 | ||
| 70 | return result | 70 | return result |
| @@ -105,6 +105,20 @@ class BaseApp: | |||
| 105 | # 注册基础路由 | 105 | # 注册基础路由 |
| 106 | self._register_base_routes() | 106 | self._register_base_routes() |
| 107 | 107 | ||
| 108 | + async def run_init_hook(self) -> bool: | ||
| 109 | + """执行通过 @init 注册的钩子(供 AppGroup 等外部容器调用)。返回是否实际执行了钩子。""" | ||
| 110 | + if self._init_hook: | ||
| 111 | + await self._init_hook() | ||
| 112 | + return True | ||
| 113 | + return False | ||
| 114 | + | ||
| 115 | + async def run_shutdown_hook(self) -> bool: | ||
| 116 | + """执行通过 @shutdown 注册的钩子(供 AppGroup 等外部容器调用)。返回是否实际执行了钩子。""" | ||
| 117 | + if self._shutdown_hook: | ||
| 118 | + await self._shutdown_hook() | ||
| 119 | + return True | ||
| 120 | + return False | ||
| 121 | + | ||
| 108 | def init(self, func: Callable) -> Callable: | 122 | def init(self, func: Callable) -> Callable: |
| 109 | """ | 123 | """ |
| 110 | 初始化钩子装饰器 | 124 | 初始化钩子装饰器 |
| @@ -68,6 +68,11 @@ class PluginApp(BaseApp): | |||
| 68 | # 注册 Plugin 特定路由 | 68 | # 注册 Plugin 特定路由 |
| 69 | self._register_plugin_routes() | 69 | self._register_plugin_routes() |
| 70 | 70 | ||
| 71 | + | ||
| 72 | + def tools_count(self) -> int: | ||
| 73 | + """已注册工具数量(供健康检查等场景使用)。""" | ||
| 74 | + return len(self._tools) | ||
| 75 | + | ||
| 71 | def register_tool( | 76 | def register_tool( |
| 72 | self, | 77 | self, |
| 73 | name: str, | 78 | name: str, |
| @@ -153,7 +158,7 @@ class PluginApp(BaseApp): | |||
| 153 | raise HTTPException( | 158 | raise HTTPException( |
| 154 | status_code=500, | 159 | status_code=500, |
| 155 | detail=f"Tool execution failed: {str(e)}", | 160 | detail=f"Tool execution failed: {str(e)}", |
| 156 | - ) | 161 | + ) from e |
| 157 | 162 | ||
| 158 | def _get_python_type(self, param_type: str) -> type: | 163 | def _get_python_type(self, param_type: str) -> type: |
| 159 | """ | 164 | """ |