Plan API proxy startup implementation

This commit is contained in:
张家训
2026-08-03 10:46:56 +08:00
parent a9bc06fc2f
commit 88b0d3888c
@@ -0,0 +1,308 @@
# WorkUtils API Debug Proxy Startup Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task in the current session. Do not dispatch subagents.
**Goal:** Start the API debug proxy with WorkUtils, show its read-only state on the API export page, and remove the per-export proxy opt-in.
**Architecture:** A Spring singleton `ApiDebugProxyLifecycle` owns startup state, endpoint publication, failure degradation, and shutdown. `WorkUtilsApplication` starts and stops it around the existing GUI/CLI dispatch, while `ApiDocPlugin` passes the same service to `ApiDocWindow`; export requests no longer carry a proxy boolean.
**Tech Stack:** Java 17, Spring Boot, JavaFX 17, JDK `HttpServer`, JUnit 5, Maven Wrapper.
## Global Constraints
- WorkUtils must continue when proxy startup fails.
- The proxy starts before GUI or CLI dispatch and stops in `finally`.
- Keep the random `127.0.0.1` port and `/dubbo/invoke` path.
- Remove the API page proxy checkbox; provide no retry/start/stop UI.
- Failed proxy startup must not block HTML or Markdown export.
- Do not modify or stage the three pre-existing untracked paths on `master`.
- Execute single-threaded; do not use subagents.
---
### Task 1: Proxy Lifecycle and Resource Shutdown
**Files:**
- Create: `src/main/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycle.java`
- Modify: `src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java`
- Create: `src/test/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycleTest.java`
**Interfaces:**
- Produces: `ApiDebugProxyLifecycle.start()`, `stop()`, `status()`, and `endpoint()`.
- Produces: `Status(State state, String endpoint, String errorMessage)` and `State.NOT_STARTED/RUNNING/FAILED`.
- Produces: idempotent `DubboDebugProxy.stop()`.
- [ ] **Step 1: Write the failing lifecycle tests**
```java
@Test
void startsOnceAndStopsOnce() {
AtomicInteger starts = new AtomicInteger();
AtomicInteger stops = new AtomicInteger();
ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle(() -> {
starts.incrementAndGet();
return new ApiDebugProxyLifecycle.ProxyHandle() {
public String endpoint() { return "http://127.0.0.1:1234/dubbo/invoke"; }
public void stop() { stops.incrementAndGet(); }
};
});
lifecycle.start();
lifecycle.start();
lifecycle.stop();
lifecycle.stop();
assertEquals(1, starts.get());
assertEquals(1, stops.get());
assertEquals(ApiDebugProxyLifecycle.State.RUNNING, lifecycle.status().state());
}
@Test
void recordsFailureWithoutThrowing() {
ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle(
() -> { throw new IllegalStateException("端口不可用"); });
assertDoesNotThrow(lifecycle::start);
assertEquals(ApiDebugProxyLifecycle.State.FAILED, lifecycle.status().state());
assertEquals("", lifecycle.endpoint());
assertTrue(lifecycle.status().errorMessage().contains("端口不可用"));
}
```
- [ ] **Step 2: Verify RED**
Run: `.\mvnw.cmd -q '-Dtest=ApiDebugProxyLifecycleTest' test`
Expected: test compilation fails because `ApiDebugProxyLifecycle` does not exist.
- [ ] **Step 3: Implement the lifecycle service**
Use a public default constructor backed by `DubboDebugProxy.getInstance()` and a package-private constructor accepting `Supplier<ProxyHandle>`. `start()` must be synchronized and catch `RuntimeException`:
```java
public synchronized Status start() {
if (status.state() != State.NOT_STARTED) return status;
try {
proxy = proxyFactory.get();
status = new Status(State.RUNNING,
Objects.requireNonNull(proxy.endpoint(), "endpoint"), "");
} catch (RuntimeException error) {
status = new Status(State.FAILED, "", readableMessage(error));
log.error("API 本地调试代理启动失败", error);
}
return status;
}
```
`stop()` atomically detaches the handle, calls it at most once, and logs rather than rethrows shutdown failures.
- [ ] **Step 4: Release proxy resources**
Store the fixed pool in an `ExecutorService` field and implement:
```java
public synchronized void stop() {
if (stopped) return;
stopped = true;
server.stop(0);
executor.shutdownNow();
log.info("DubboDebugProxy 已停止");
}
```
- [ ] **Step 5: Verify GREEN**
Run: `.\mvnw.cmd -q '-Dtest=ApiDebugProxyLifecycleTest,ApiDocGeneratorProxyTest' test`
Expected: PASS.
- [ ] **Step 6: Commit**
```powershell
git add src/main/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycle.java src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java src/test/java/com/example/workutils/biz/apidoc/application/ApiDebugProxyLifecycleTest.java
git commit -m "Start and stop the API debug proxy safely"
```
---
### Task 2: Remove Per-Export Proxy Selection
**Files:**
- Modify: `src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java`
- Modify: `src/main/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCase.java`
- Modify: `src/test/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequestTest.java`
- Modify: `src/test/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCaseTest.java`
**Interfaces:**
- Produces: `path(String)`, `fqn(String)`, and `batch(List<String>, boolean, boolean, boolean)`.
- Consumes: lifecycle endpoint supplier returning a URL or `""`.
- [ ] **Step 1: Write flag-free request tests**
```java
ApiDocExportRequest request = ApiDocExportRequest.fqn(
" com.example.UserFacade ");
assertEquals("com.example.UserFacade", request.input());
assertFalse(Arrays.stream(ApiDocExportRequest.class.getRecordComponents())
.anyMatch(component -> component.getName().equals("debugProxyEnabled")));
```
Update path and batch tests to omit the final boolean.
- [ ] **Step 2: Write unconditional endpoint tests**
```java
@Test
void alwaysPassesStartupEndpointToExporter() throws Exception {
AtomicReference<String> endpoint = new AtomicReference<>();
RunApiDocExportUseCase useCase = new RunApiDocExportUseCase(
() -> "http://127.0.0.1:1234/dubbo/invoke",
(request, value, context) -> endpoint.set(value));
useCase.execute(ApiDocExportRequest.path("A.java"), OperationContext.none());
assertEquals("http://127.0.0.1:1234/dubbo/invoke", endpoint.get());
}
@Test
void emptyEndpointStillRunsExporter() throws Exception {
AtomicInteger exports = new AtomicInteger();
RunApiDocExportUseCase useCase = new RunApiDocExportUseCase(
() -> "", (request, value, context) -> exports.incrementAndGet());
useCase.execute(ApiDocExportRequest.path("A.java"), OperationContext.none());
assertEquals(1, exports.get());
}
```
- [ ] **Step 3: Verify RED**
Run: `.\mvnw.cmd -q '-Dtest=ApiDocExportRequestTest,RunApiDocExportUseCaseTest' test`
Expected: compilation fails because flag-free factories do not exist.
- [ ] **Step 4: Remove the boolean and simplify the use case**
Delete the record component and factory parameters. Resolve the endpoint unconditionally:
```java
String endpoint = Objects.requireNonNullElse(debugEndpointSupplier.get(), "");
exporter.export(request, endpoint, context);
```
- [ ] **Step 5: Verify GREEN and commit**
Run the Step 3 command; expect PASS. Then:
```powershell
git add src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java src/main/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCase.java src/test/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequestTest.java src/test/java/com/example/workutils/biz/apidoc/application/RunApiDocExportUseCaseTest.java
git commit -m "Remove per-export proxy selection"
```
---
### Task 3: Application Startup and API Page Status
**Files:**
- Modify: `src/main/java/com/example/workutils/WorkUtilsApplication.java`
- Modify: `src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java`
- Modify: `src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java`
- Modify: `src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java`
- Modify: `src/test/java/com/example/workutils/biz/apidoc/ApiDocWindowArchitectureTest.java`
- Create: `src/test/java/com/example/workutils/biz/apidoc/ApiDebugProxyStatusLabelTest.java`
**Interfaces:**
- Consumes: lifecycle API from Task 1 and flag-free request factories from Task 2.
- Produces: package-private `ApiDocWindow.buildProxyStatusLabel(Status, int)`.
- [ ] **Step 1: Write application lifecycle architecture test**
Extract `run(String... args)` and assert startup precedes dispatch, `finally` exists, and `apiDebugProxyLifecycle.stop()` is present.
- [ ] **Step 2: Write page architecture and JavaFX label tests**
Assert `ApiDocWindow` contains `buildProxyStatusLabel` and excludes `debugProxyCb`, `new CheckBox("启用接口调试`, and `DubboDebugProxy.getInstance()`.
```java
Label running = ApiDocWindow.buildProxyStatusLabel(
new Status(State.RUNNING, "http://127.0.0.1:1234/dubbo/invoke", ""), 13);
assertTrue(running.getText().contains("本地代理已启动"));
assertEquals(Theme.SUCCESS, running.getTextFill());
Label failed = ApiDocWindow.buildProxyStatusLabel(
new Status(State.FAILED, "", "端口不可用"), 13);
assertTrue(failed.getText().contains("本地代理启动失败"));
assertEquals(Theme.DANGER, failed.getTextFill());
assertTrue(failed.getTooltip().getText().contains("端口不可用"));
assertTrue(failed.getAccessibleText().contains("端口不可用"));
```
- [ ] **Step 3: Verify RED**
Run: `.\mvnw.cmd -q '-Dtest=MainWindowArchitectureTest,ApiDocWindowArchitectureTest,ApiDebugProxyStatusLabelTest' test`
Expected: failures because lifecycle wiring and the label factory are absent.
- [ ] **Step 4: Wire application and plugin lifecycle**
Inject `ApiDebugProxyLifecycle` into both Spring constructors. Wrap runner dispatch:
```java
apiDebugProxyLifecycle.start();
try {
if (args.length == 0) showMainWindow();
else processCommand(args);
} finally {
apiDebugProxyLifecycle.stop();
}
```
Pass the same lifecycle from `ApiDocPlugin` to every `ApiDocWindow` constructor call.
- [ ] **Step 5: Replace checkbox with status label**
Inject the lifecycle into `ApiDocWindow`, build the use case with `apiDebugProxyLifecycle::endpoint`, add the read-only label to the action row, and remove all checkbox state. Use `Theme.SUCCESS` for `RUNNING`, `Theme.DANGER` otherwise, plus failure Tooltip and accessible text. Before an export with a blank endpoint, log `[WARN] 本地代理不可用,导出文档的接口调试功能已禁用`. Call the flag-free request factories.
- [ ] **Step 6: Verify GREEN and commit**
Run:
```powershell
.\mvnw.cmd -q '-Dtest=ApiDebugProxyLifecycleTest,ApiDocExportRequestTest,RunApiDocExportUseCaseTest,ApiDocWindowArchitectureTest,ApiDebugProxyStatusLabelTest,MainWindowArchitectureTest,ApiDocGeneratorProxyTest' test
```
Expected: PASS. Then commit all Task 3 files with message `Start API proxy with WorkUtils`.
---
### Task 4: Full Verification and Master Integration
**Files:** Verify all planned files; modify none unless a verification failure exposes a scoped defect.
**Interfaces:** Produces a verified local `master` with the owned worktree and feature branch removed.
- [ ] **Step 1: Run branch verification**
```powershell
.\mvnw.cmd -q test
.\mvnw.cmd -q -DskipTests compile
git diff --check master...HEAD
git status --short --branch
```
Expected: tests, compilation, and diff checks exit 0; the feature worktree is clean.
- [ ] **Step 2: Fast-forward local master and verify again**
From `D:\workspace\workutils`:
```powershell
git merge --ff-only codex/api-proxy-startup
.\mvnw.cmd -q test
.\mvnw.cmd -q -DskipTests compile
git diff --check
```
Expected: every command exits 0; the three pre-existing untracked paths remain untouched.
- [ ] **Step 3: Clean the owned worktree and branch**
```powershell
git worktree remove D:\workspace\workutils\.worktrees\codex-api-proxy-startup
git worktree prune
git branch -d codex/api-proxy-startup
```