From a0f7152e80fbd90f97ff99b9458aee3e10d573e3 Mon Sep 17 00:00:00 2001 From: 2521690 Date: Thu, 9 Apr 2026 15:45:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(mvp):=20=E6=8E=A5=E5=85=A5=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=A0=B7=E6=9D=BF=E5=BA=94=E7=94=A8=E6=A1=A5=E6=8E=A5?= =?UTF-8?q?=E5=B9=B6=E6=8E=A8=E8=BF=9B=E6=BC=94=E7=A4=BA=E4=B8=BB=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `sample-apps/order-service` Java 样板应用及 Win/Linux 构建、启停、状态脚本 - 新增 `LocalSampleAppService`,在 `software-a` 中支持 `order-service test` 本地桥接部署 - 增加桥接开关配置:`ENABLE_SAMPLE_APP_BRIDGE`、`SAMPLE_APP_ROOT` - 修正后端配置读取方式,环境变量可在运行时生效(`Settings` 改为 `default_factory`) - 更新应用元数据默认验证目标:`127.0.0.1:18080`、本地日志路径 - 新增桥接测试 `test_sample_app_bridge.py`,后端基线更新至 `24 passed` - 更新 `.gitignore`,忽略样板应用 `build/runtime` 产物 - 更新 README 与《当前进度总结》:记录本轮“真实样板应用 + 桥接能力”进展,MVP 进度约 `97%` --- .gitignore | 2 + backend/README.md | 23 ++++- backend/app/core/config.py | 14 ++- .../app/services/local_sample_app_service.py | 45 +++++++++ backend/app/services/metadata_service.py | 6 +- backend/app/services/software_a_service.py | 19 ++++ backend/tests/test_sample_app_bridge.py | 32 +++++++ sample-apps/order-service/README.md | 33 +++++++ sample-apps/order-service/scripts/build.ps1 | 25 +++++ sample-apps/order-service/scripts/build.sh | 16 ++++ sample-apps/order-service/scripts/start.ps1 | 42 ++++++++ sample-apps/order-service/scripts/start.sh | 25 +++++ sample-apps/order-service/scripts/status.ps1 | 23 +++++ sample-apps/order-service/scripts/status.sh | 19 ++++ sample-apps/order-service/scripts/stop.ps1 | 15 +++ sample-apps/order-service/scripts/stop.sh | 14 +++ .../orderservice/OrderServiceApplication.java | 95 +++++++++++++++++++ 智能化部署agent-当前进度总结.md | 36 +++++++ 18 files changed, 473 insertions(+), 11 deletions(-) create mode 100644 backend/app/services/local_sample_app_service.py create mode 100644 backend/tests/test_sample_app_bridge.py create mode 100644 sample-apps/order-service/README.md create mode 100644 sample-apps/order-service/scripts/build.ps1 create mode 100644 sample-apps/order-service/scripts/build.sh create mode 100644 sample-apps/order-service/scripts/start.ps1 create mode 100644 sample-apps/order-service/scripts/start.sh create mode 100644 sample-apps/order-service/scripts/status.ps1 create mode 100644 sample-apps/order-service/scripts/status.sh create mode 100644 sample-apps/order-service/scripts/stop.ps1 create mode 100644 sample-apps/order-service/scripts/stop.sh create mode 100644 sample-apps/order-service/src/demo/orderservice/OrderServiceApplication.java diff --git a/.gitignore b/.gitignore index a04d282..abc412b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ data/ dist/ tmp-linux-runtime/ +sample-apps/**/build/ +sample-apps/**/runtime/ __pycache__/ .pytest_cache/ *.pyc diff --git a/backend/README.md b/backend/README.md index 4e011d4..67e6e2f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -13,6 +13,12 @@ python -m venv .venv .venv\\Scripts\\python -m uvicorn app.main:app --reload --app-dir backend ``` +## Demo UI + +After startup, open: + +`http://127.0.0.1:8000/demo/chat` + ## Test The lightweight API verification can run with in-memory SQLite: @@ -35,6 +41,9 @@ This repo currently defaults to: 4. demo operator defaults: `alice(u1001)` for task execution `bob(u2001)` for approval +5. optional sample app bridge: + `ENABLE_SAMPLE_APP_BRIDGE=true` + `SAMPLE_APP_ROOT=sample-apps/order-service` In the current sandbox, file-based SQLite may fail with `disk I/O error`. For tests and local verification here, use: @@ -100,6 +109,14 @@ Current execution flow: 10. task reaches `SUCCEEDED` / `FAILED` / `CANCELLED` 11. task detail/report returns software-a status, approval trace, tool trace, verification trace and audit trace +## Real Java Sample App + +The repo now includes a real Java sample app: + +`sample-apps/order-service` + +When `ENABLE_SAMPLE_APP_BRIDGE=true`, `software-a` minimal implementation can bridge the `order-service test` deploy action to the local sample app startup flow. + Current execution metrics: 1. `tool_call.duration_ms` is persisted from `started_at` / `finished_at` @@ -142,7 +159,7 @@ Automated tests currently cover: 6. task report trace aggregation 7. cancel running task -Current baseline: `20 passed` +Current baseline: `24 passed` Current baseline: `23 passed` ## Next Focus @@ -150,6 +167,6 @@ Current baseline: `23 passed` Recommended next implementation steps: 1. continue enriching app-metadata-driven verification templates -2. connect a real Java sample app to the current demo flow +2. continue polishing the demo UI and session flow 3. validate native Linux packaging in a real bash/Linux environment -4. then continue with second-batch OpenAPI and UI polish +4. then continue with second-batch OpenAPI diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 7c56b3f..4b81ee0 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,17 +1,21 @@ from __future__ import annotations import os -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path @dataclass(frozen=True) class Settings: app_name: str = "smart-deploy-agent-demo" - app_env: str = os.getenv("APP_ENV", "demo") - app_port: int = int(os.getenv("APP_PORT", "8000")) - default_timezone: str = os.getenv("DEFAULT_TIMEZONE", "Asia/Shanghai") - database_url: str = os.getenv("DATABASE_URL", "sqlite:///./data/agent_demo.db") + app_env: str = field(default_factory=lambda: os.getenv("APP_ENV", "demo")) + app_port: int = field(default_factory=lambda: int(os.getenv("APP_PORT", "8000"))) + default_timezone: str = field(default_factory=lambda: os.getenv("DEFAULT_TIMEZONE", "Asia/Shanghai")) + database_url: str = field(default_factory=lambda: os.getenv("DATABASE_URL", "sqlite:///./data/agent_demo.db")) + enable_sample_app_bridge: bool = field( + default_factory=lambda: os.getenv("ENABLE_SAMPLE_APP_BRIDGE", "false").lower() in {"1", "true", "yes", "on"} + ) + sample_app_root: str = field(default_factory=lambda: os.getenv("SAMPLE_APP_ROOT", "sample-apps/order-service")) def get_settings() -> Settings: diff --git a/backend/app/services/local_sample_app_service.py b/backend/app/services/local_sample_app_service.py new file mode 100644 index 0000000..5e73e65 --- /dev/null +++ b/backend/app/services/local_sample_app_service.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import os +import platform +import subprocess +from pathlib import Path +from typing import Any + + +class LocalSampleAppService: + def __init__(self, sample_app_root: str) -> None: + self.root = Path(sample_app_root).resolve() + + def deploy_order_service(self, version: str) -> dict[str, Any]: + self._ensure_root() + self._run_script("build") + self._run_script("stop", check=False) + self._run_script("start", extra_args=[f"-Version {version}"] if self._is_windows() else [version]) + return self.status() + + def status(self) -> dict[str, Any]: + self._ensure_root() + result = self._run_script("status", check=False) + return { + "status_text": (result.stdout + "\n" + result.stderr).strip(), + "return_code": result.returncode, + "running": result.returncode == 0, + } + + def _run_script(self, action: str, extra_args: list[str] | None = None, check: bool = True) -> subprocess.CompletedProcess[str]: + extra_args = extra_args or [] + if self._is_windows(): + script_path = self.root / "scripts" / f"{action}.ps1" + command = ["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script_path), *extra_args] + else: + script_path = self.root / "scripts" / f"{action}.sh" + command = ["bash", str(script_path), *extra_args] + return subprocess.run(command, cwd=str(self.root), capture_output=True, text=True, check=check) + + def _ensure_root(self) -> None: + if not self.root.exists(): + raise FileNotFoundError(f"sample app root not found: {self.root}") + + def _is_windows(self) -> bool: + return platform.system().upper().startswith("WIN") diff --git a/backend/app/services/metadata_service.py b/backend/app/services/metadata_service.py index 03324c2..ce68f9b 100644 --- a/backend/app/services/metadata_service.py +++ b/backend/app/services/metadata_service.py @@ -16,9 +16,9 @@ class MetadataService: "env": "test", "process_name": "java", "command_contains": "order-service", - "health_check_url": "http://order-service.test.demo/actuator/health", - "log_path": "logs/order-service.log", - "listen_port": 8080, + "health_check_url": "http://127.0.0.1:18080/actuator/health", + "log_path": "sample-apps/order-service/runtime/logs/order-service.log", + "listen_port": 18080, "startup_keyword": "Started order-service", }, { diff --git a/backend/app/services/software_a_service.py b/backend/app/services/software_a_service.py index 5585136..c051415 100644 --- a/backend/app/services/software_a_service.py +++ b/backend/app/services/software_a_service.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from uuid import uuid4 from app.core.constants import ( @@ -7,8 +8,10 @@ from app.core.constants import ( SOFTWARE_A_TASK_STATUS_RUNNING, SOFTWARE_A_TASK_STATUS_SUCCEEDED, ) +from app.core.config import get_settings from app.core.time import format_now from app.schemas.software_a import CreateDeployTaskRequest +from app.services.local_sample_app_service import LocalSampleAppService class SoftwareAService: @@ -22,6 +25,15 @@ class SoftwareAService: should_fail = self._should_fail_deploy(payload) task_status = SOFTWARE_A_TASK_STATUS_FAILED if should_fail else SOFTWARE_A_TASK_STATUS_RUNNING error_detail = self._build_error_detail(payload) if should_fail else None + sample_app_result: dict | None = None + + if not should_fail and self._should_use_local_sample_bridge(payload): + try: + sample_app_result = LocalSampleAppService(get_settings().sample_app_root).deploy_order_service(payload.version) + except Exception as exc: + task_status = SOFTWARE_A_TASK_STATUS_FAILED + error_detail = f"local sample app deploy failed: {exc}" + task = { "software_a_task_id": task_id, "task_status": task_status, @@ -33,6 +45,7 @@ class SoftwareAService: "started_at": format_now(self.timezone_name), "finished_at": format_now(self.timezone_name), "error_detail": error_detail, + "sample_app_result": sample_app_result, } self._deploy_tasks[task_id] = task return task @@ -59,3 +72,9 @@ class SoftwareAService: def _build_error_detail(self, payload: CreateDeployTaskRequest) -> str: return f"demo deploy failed for app={payload.app_code}, env={payload.env}, version={payload.version}" + + def _should_use_local_sample_bridge(self, payload: CreateDeployTaskRequest) -> bool: + settings = get_settings() + if not settings.enable_sample_app_bridge: + return False + return payload.app_code == "order-service" and payload.env == "test" diff --git a/backend/tests/test_sample_app_bridge.py b/backend/tests/test_sample_app_bridge.py new file mode 100644 index 0000000..b7066c9 --- /dev/null +++ b/backend/tests/test_sample_app_bridge.py @@ -0,0 +1,32 @@ +import os +from unittest.mock import patch + +from app.schemas.software_a import CreateDeployTaskRequest, DeployOptions, SoftwareAOperator +from app.services.software_a_service import SoftwareAService + + +def test_sample_app_bridge_can_be_enabled() -> None: + os.environ["ENABLE_SAMPLE_APP_BRIDGE"] = "true" + try: + with patch("app.services.software_a_service.LocalSampleAppService.deploy_order_service") as mocked_deploy: + mocked_deploy.return_value = { + "running": True, + "status_text": "RUNNING", + "return_code": 0, + } + payload = CreateDeployTaskRequest( + operator=SoftwareAOperator(user_id="u1001", user_name="alice"), + tenant_id="tenant-demo", + app_code="order-service", + env="test", + version="1.2.3", + target_nodes=["127.0.0.1"], + deploy_options=DeployOptions(graceful=True), + ) + result = SoftwareAService("Asia/Shanghai").create_deploy_task(payload) + + assert result["task_status"] == "RUNNING" + assert result["sample_app_result"]["running"] is True + mocked_deploy.assert_called_once() + finally: + os.environ["ENABLE_SAMPLE_APP_BRIDGE"] = "false" diff --git a/sample-apps/order-service/README.md b/sample-apps/order-service/README.md new file mode 100644 index 0000000..d45953e --- /dev/null +++ b/sample-apps/order-service/README.md @@ -0,0 +1,33 @@ +# Order Service Sample App + +## Purpose + +This sample app is used to demonstrate the smart deploy agent flow with a real Java process: + +1. build sample app +2. deploy/start sample app +3. verify process / port / tcp / http / log + +## Build + +```powershell +powershell -ExecutionPolicy Bypass -File .\sample-apps\order-service\scripts\build.ps1 +``` + +## Start + +```powershell +powershell -ExecutionPolicy Bypass -File .\sample-apps\order-service\scripts\start.ps1 -Version 1.2.3 +``` + +## Stop + +```powershell +powershell -ExecutionPolicy Bypass -File .\sample-apps\order-service\scripts\stop.ps1 +``` + +## Health + +Once started, health check is available at: + +`http://127.0.0.1:18080/actuator/health` diff --git a/sample-apps/order-service/scripts/build.ps1 b/sample-apps/order-service/scripts/build.ps1 new file mode 100644 index 0000000..137fac6 --- /dev/null +++ b/sample-apps/order-service/scripts/build.ps1 @@ -0,0 +1,25 @@ +param( + [string]$JavaHome = $env:JAVA_HOME +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$srcRoot = Join-Path $root "src" +$buildRoot = Join-Path $root "build" +$classesRoot = Join-Path $buildRoot "classes" +$jarPath = Join-Path $buildRoot "order-service-demo.jar" +$manifestPath = Join-Path $buildRoot "manifest.mf" + +New-Item -ItemType Directory -Path $classesRoot -Force | Out-Null + +$javac = if ($JavaHome) { Join-Path $JavaHome "bin\\javac.exe" } else { "javac" } +$jar = if ($JavaHome) { Join-Path $JavaHome "bin\\jar.exe" } else { "jar" } + +$sources = Get-ChildItem -Path $srcRoot -Recurse -Filter *.java | ForEach-Object { $_.FullName } +$javacArgs = @("-encoding", "UTF-8", "-d", $classesRoot) + $sources +& $javac @javacArgs + +Set-Content -LiteralPath $manifestPath -Value "Main-Class: demo.orderservice.OrderServiceApplication`r`n" -Encoding ASCII +& $jar cfm $jarPath $manifestPath -C $classesRoot . + +Write-Output $jarPath diff --git a/sample-apps/order-service/scripts/build.sh b/sample-apps/order-service/scripts/build.sh new file mode 100644 index 0000000..965f392 --- /dev/null +++ b/sample-apps/order-service/scripts/build.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC_ROOT="$ROOT_DIR/src" +BUILD_ROOT="$ROOT_DIR/build" +CLASSES_ROOT="$BUILD_ROOT/classes" +JAR_PATH="$BUILD_ROOT/order-service-demo.jar" +MANIFEST_PATH="$BUILD_ROOT/manifest.mf" + +mkdir -p "$CLASSES_ROOT" +find "$SRC_ROOT" -name '*.java' > "$BUILD_ROOT/sources.txt" +javac -encoding UTF-8 -d "$CLASSES_ROOT" @"$BUILD_ROOT/sources.txt" +printf 'Main-Class: demo.orderservice.OrderServiceApplication\n' > "$MANIFEST_PATH" +jar cfm "$JAR_PATH" "$MANIFEST_PATH" -C "$CLASSES_ROOT" . +echo "$JAR_PATH" diff --git a/sample-apps/order-service/scripts/start.ps1 b/sample-apps/order-service/scripts/start.ps1 new file mode 100644 index 0000000..a3ebeab --- /dev/null +++ b/sample-apps/order-service/scripts/start.ps1 @@ -0,0 +1,42 @@ +param( + [string]$Version = "1.2.3", + [int]$Port = 18080, + [string]$JavaHome = $env:JAVA_HOME +) + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$buildRoot = Join-Path $root "build" +$jarPath = Join-Path $buildRoot "order-service-demo.jar" +$runtimeRoot = Join-Path $root "runtime" +$logsRoot = Join-Path $runtimeRoot "logs" +$pidFile = Join-Path $runtimeRoot "order-service.pid" +$logPath = Join-Path $logsRoot "order-service.log" + +New-Item -ItemType Directory -Path $logsRoot -Force | Out-Null + +if (-not (Test-Path $jarPath)) { + & (Join-Path $PSScriptRoot "build.ps1") -JavaHome $JavaHome | Out-Null +} + +if (Test-Path $pidFile) { + try { + $existingPid = Get-Content -LiteralPath $pidFile | Select-Object -First 1 + if ($existingPid) { + Stop-Process -Id ([int]$existingPid) -Force -ErrorAction SilentlyContinue + } + } catch {} +} + +$java = if ($JavaHome) { Join-Path $JavaHome "bin\\java.exe" } else { "java" } +$arguments = @( + "-jar", $jarPath, + "--app-name=order-service", + "--version=$Version", + "--port=$Port", + "--log-path=$logPath" +) + +$process = Start-Process -FilePath $java -ArgumentList $arguments -PassThru -WindowStyle Hidden +Set-Content -LiteralPath $pidFile -Value $process.Id -Encoding ASCII +Write-Output $process.Id diff --git a/sample-apps/order-service/scripts/start.sh b/sample-apps/order-service/scripts/start.sh new file mode 100644 index 0000000..bb7a0fd --- /dev/null +++ b/sample-apps/order-service/scripts/start.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION="${1:-1.2.3}" +PORT="${2:-18080}" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +JAR_PATH="$ROOT_DIR/build/order-service-demo.jar" +RUNTIME_DIR="$ROOT_DIR/runtime" +LOG_DIR="$RUNTIME_DIR/logs" +PID_FILE="$RUNTIME_DIR/order-service.pid" +LOG_PATH="$LOG_DIR/order-service.log" + +mkdir -p "$LOG_DIR" + +if [[ ! -f "$JAR_PATH" ]]; then + "$ROOT_DIR/scripts/build.sh" +fi + +if [[ -f "$PID_FILE" ]]; then + kill "$(cat "$PID_FILE")" 2>/dev/null || true +fi + +nohup java -jar "$JAR_PATH" --app-name=order-service --version="$VERSION" --port="$PORT" --log-path="$LOG_PATH" >/dev/null 2>&1 & +echo $! > "$PID_FILE" +echo $! diff --git a/sample-apps/order-service/scripts/status.ps1 b/sample-apps/order-service/scripts/status.ps1 new file mode 100644 index 0000000..8870a1d --- /dev/null +++ b/sample-apps/order-service/scripts/status.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$pidFile = Join-Path $root "runtime\\order-service.pid" + +if (-not (Test-Path $pidFile)) { + Write-Output "STOPPED" + exit 1 +} + +$appPid = Get-Content -LiteralPath $pidFile | Select-Object -First 1 +if (-not $appPid) { + Write-Output "STOPPED" + exit 1 +} + +$process = Get-Process -Id ([int]$appPid) -ErrorAction SilentlyContinue +if ($process) { + Write-Output "RUNNING" + exit 0 +} + +Write-Output "STOPPED" +exit 1 diff --git a/sample-apps/order-service/scripts/status.sh b/sample-apps/order-service/scripts/status.sh new file mode 100644 index 0000000..a5c50d9 --- /dev/null +++ b/sample-apps/order-service/scripts/status.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PID_FILE="$ROOT_DIR/runtime/order-service.pid" + +if [[ ! -f "$PID_FILE" ]]; then + echo "STOPPED" + exit 1 +fi + +PID="$(cat "$PID_FILE")" +if ps -p "$PID" >/dev/null 2>&1; then + echo "RUNNING" + exit 0 +fi + +echo "STOPPED" +exit 1 diff --git a/sample-apps/order-service/scripts/stop.ps1 b/sample-apps/order-service/scripts/stop.ps1 new file mode 100644 index 0000000..4f93e17 --- /dev/null +++ b/sample-apps/order-service/scripts/stop.ps1 @@ -0,0 +1,15 @@ +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$pidFile = Join-Path $root "runtime\\order-service.pid" + +if (-not (Test-Path $pidFile)) { + Write-Output "not running" + exit 0 +} + +$appPid = Get-Content -LiteralPath $pidFile | Select-Object -First 1 +if ($appPid) { + Stop-Process -Id ([int]$appPid) -Force -ErrorAction SilentlyContinue +} +Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue +Write-Output "stopped" diff --git a/sample-apps/order-service/scripts/stop.sh b/sample-apps/order-service/scripts/stop.sh new file mode 100644 index 0000000..e065928 --- /dev/null +++ b/sample-apps/order-service/scripts/stop.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PID_FILE="$ROOT_DIR/runtime/order-service.pid" + +if [[ ! -f "$PID_FILE" ]]; then + echo "not running" + exit 0 +fi + +kill "$(cat "$PID_FILE")" 2>/dev/null || true +rm -f "$PID_FILE" +echo "stopped" diff --git a/sample-apps/order-service/src/demo/orderservice/OrderServiceApplication.java b/sample-apps/order-service/src/demo/orderservice/OrderServiceApplication.java new file mode 100644 index 0000000..5e5724c --- /dev/null +++ b/sample-apps/order-service/src/demo/orderservice/OrderServiceApplication.java @@ -0,0 +1,95 @@ +package demo.orderservice; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +public final class OrderServiceApplication { + private static final Logger LOGGER = Logger.getLogger("order-service"); + + private OrderServiceApplication() { + } + + public static void main(String[] args) throws Exception { + Map options = parseArgs(args); + String appName = options.getOrDefault("app-name", "order-service"); + String version = options.getOrDefault("version", "0.0.1"); + int port = Integer.parseInt(options.getOrDefault("port", "18080")); + String logPath = options.getOrDefault("log-path", "sample-apps/order-service/runtime/logs/order-service.log"); + + configureLogging(logPath); + + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0); + server.createContext("/actuator/health", new JsonHandler( + "{\"status\":\"UP\",\"app\":\"" + appName + "\",\"version\":\"" + version + "\"}" + )); + server.createContext("/orders/ping", new JsonHandler( + "{\"message\":\"pong\",\"app\":\"" + appName + "\",\"version\":\"" + version + "\"}" + )); + server.setExecutor(Executors.newCachedThreadPool()); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> LOGGER.info("Stopping " + appName))); + server.start(); + LOGGER.info("Started " + appName + " version " + version + " on port " + port); + System.out.println("Started " + appName + " version " + version + " on port " + port); + } + + private static void configureLogging(String logPath) throws IOException { + java.io.File file = new java.io.File(logPath); + java.io.File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + FileHandler handler = new FileHandler(logPath, true); + handler.setEncoding("UTF-8"); + handler.setFormatter(new SimpleFormatter()); + handler.setLevel(Level.INFO); + LOGGER.setLevel(Level.INFO); + LOGGER.setUseParentHandlers(false); + LOGGER.addHandler(handler); + } + + private static Map parseArgs(String[] args) { + Map options = new HashMap<>(); + for (String arg : args) { + if (!arg.startsWith("--")) { + continue; + } + int index = arg.indexOf('='); + if (index <= 2) { + continue; + } + options.put(arg.substring(2, index), arg.substring(index + 1)); + } + return options; + } + + private static final class JsonHandler implements HttpHandler { + private final byte[] payload; + + private JsonHandler(String payload) { + this.payload = payload.getBytes(StandardCharsets.UTF_8); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().add("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(200, payload.length); + try (OutputStream outputStream = exchange.getResponseBody()) { + outputStream.write(payload); + } + } + } +} diff --git a/智能化部署agent-当前进度总结.md b/智能化部署agent-当前进度总结.md index 8ac893d..260ca89 100644 --- a/智能化部署agent-当前进度总结.md +++ b/智能化部署agent-当前进度总结.md @@ -485,3 +485,39 @@ set DATABASE_URL=sqlite:///:memory: 2. 继续增强 app_metadata 驱动的验证模板与真实插件能力 3. 原生 Linux/bash 环境下验证私有运行时打包 4. 对演示 UI 做产品化打磨 + +## 11. 本轮更新(2026-04-09, 真实样板应用与演示主线) + +本轮新增完成内容: + +1. 已新增真实 Java 样板应用: + `sample-apps/order-service` +2. 已补充样板应用构建、启动、停止、状态脚本,支持 Windows 与 Linux 两套脚本。 +3. 已手工验证样板应用可编译、可启动、健康检查可访问、日志可落盘。 +4. 已新增可选样板桥接能力: + 当 `ENABLE_SAMPLE_APP_BRIDGE=true` 时,`software-a` 最小能力可将 `order-service test` 部署桥接到本地样板应用启动流程。 +5. 已修复 backend 配置读取方式,环境变量变更可在运行时正确生效。 +6. 已通过后端测试覆盖样板桥接开关分支。 +7. 已补最小会话层和 demo chat API。 +8. 已新增最小 Web Demo 页面: + `GET /` + `GET /demo/chat` +9. 已形成可视化演示流: + 一句话输入 -> 结构化解析 -> 确认 -> 执行 -> 验证 -> 报告 +10. 已补应用元数据驱动验证链路,默认验证参数不再全部写死。 + +本轮测试结果: + +1. backend 测试 `24 passed` +2. edge-agent 测试 `20 passed` + +本轮 MVP 进度更新: + +**约 97%** + +当前 MVP 主线剩余重点: + +1. 接一个真实的 end-to-end 演示脚本/录屏流程 +2. 原生 Linux/bash 环境下验证私有运行时打包 +3. 继续打磨 app_metadata 验证模板和演示 UI 体验 +4. 第二批 OpenAPI 与更多联调场景