feat(mvp): 接入真实样板应用桥接并推进演示主线
- 新增 `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%`
This commit is contained in:
parent
ce299cbb18
commit
a0f7152e80
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
data/
|
||||
dist/
|
||||
tmp-linux-runtime/
|
||||
sample-apps/**/build/
|
||||
sample-apps/**/runtime/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.pyc
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
45
backend/app/services/local_sample_app_service.py
Normal file
45
backend/app/services/local_sample_app_service.py
Normal file
@ -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")
|
||||
@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@ -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"
|
||||
|
||||
32
backend/tests/test_sample_app_bridge.py
Normal file
32
backend/tests/test_sample_app_bridge.py
Normal file
@ -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"
|
||||
33
sample-apps/order-service/README.md
Normal file
33
sample-apps/order-service/README.md
Normal file
@ -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`
|
||||
25
sample-apps/order-service/scripts/build.ps1
Normal file
25
sample-apps/order-service/scripts/build.ps1
Normal file
@ -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
|
||||
16
sample-apps/order-service/scripts/build.sh
Normal file
16
sample-apps/order-service/scripts/build.sh
Normal file
@ -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"
|
||||
42
sample-apps/order-service/scripts/start.ps1
Normal file
42
sample-apps/order-service/scripts/start.ps1
Normal file
@ -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
|
||||
25
sample-apps/order-service/scripts/start.sh
Normal file
25
sample-apps/order-service/scripts/start.sh
Normal file
@ -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 $!
|
||||
23
sample-apps/order-service/scripts/status.ps1
Normal file
23
sample-apps/order-service/scripts/status.ps1
Normal file
@ -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
|
||||
19
sample-apps/order-service/scripts/status.sh
Normal file
19
sample-apps/order-service/scripts/status.sh
Normal file
@ -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
|
||||
15
sample-apps/order-service/scripts/stop.ps1
Normal file
15
sample-apps/order-service/scripts/stop.ps1
Normal file
@ -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"
|
||||
14
sample-apps/order-service/scripts/stop.sh
Normal file
14
sample-apps/order-service/scripts/stop.sh
Normal file
@ -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"
|
||||
@ -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<String, String> 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<String, String> parseArgs(String[] args) {
|
||||
Map<String, String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 与更多联调场景
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user