소스 검색

Fix HttpApi web UI fallback (#25163)

Kit Langton 3 달 전
부모
커밋
5518ecaefe
2개의 변경된 파일62개의 추가작업 그리고 2개의 파일을 삭제
  1. 17 2
      packages/opencode/src/server/routes/instance/httpapi/server.ts
  2. 45 0
      packages/opencode/test/server/httpapi-ui.test.ts

+ 17 - 2
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -1,6 +1,6 @@
 import { Context, Effect, Layer } from "effect"
 import { HttpApiBuilder } from "effect/unstable/httpapi"
-import { HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
+import { HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
 import * as Socket from "effect/unstable/socket/Socket"
 import { Account } from "@/account/account"
 import { Agent } from "@/agent/agent"
@@ -38,6 +38,7 @@ import { Vcs } from "@/project/vcs"
 import { Worktree } from "@/worktree"
 import { Workspace } from "@/control-plane/workspace"
 import { isAllowedCorsOrigin } from "@/server/cors"
+import { UIRoutes } from "@/server/routes/ui"
 import { InstanceHttpApi, RootHttpApi } from "./api"
 import { ServerAuthConfig, authorizationLayer, authorizationRouterMiddleware } from "./middleware/authorization"
 import { eventRoute } from "./event"
@@ -119,7 +120,21 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe
   ]),
 )
 
-export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes).pipe(
+const uiRoutes = lazy(() => UIRoutes())
+const uiRoute = HttpRouter.add("*", "/*", (request) =>
+  Effect.promise(async () =>
+    uiRoutes().fetch(
+      request.source instanceof Request
+        ? request.source
+        : new Request(new URL(request.originalUrl, "http://localhost"), {
+            method: request.method,
+            headers: request.headers,
+          }),
+    ),
+  ).pipe(Effect.map(HttpServerResponse.fromWeb)),
+)
+
+export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pipe(
   Layer.provide([
     cors,
     runtime,

+ 45 - 0
packages/opencode/test/server/httpapi-ui.test.ts

@@ -0,0 +1,45 @@
+import { afterEach, describe, expect, test } from "bun:test"
+import { Flag } from "@opencode-ai/core/flag/flag"
+import * as Log from "@opencode-ai/core/util/log"
+import { Server } from "../../src/server/server"
+
+void Log.init({ print: false })
+
+const original = {
+  OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
+  fetch: globalThis.fetch,
+}
+
+afterEach(() => {
+  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
+  globalThis.fetch = original.fetch
+})
+
+describe("HttpApi UI fallback", () => {
+  test("serves the web UI through the experimental backend", async () => {
+    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+    let proxiedUrl: string | undefined
+    globalThis.fetch = ((input: RequestInfo | URL) => {
+      proxiedUrl = String(input instanceof Request ? input.url : input)
+      return Promise.resolve(new Response("<html>opencode</html>", { headers: { "content-type": "text/html" } }))
+    }) as typeof fetch
+
+    const response = await Server.Default().app.request("/")
+
+    expect(response.status).toBe(200)
+    expect(response.headers.get("content-type")).toContain("text/html")
+    expect(await response.text()).toBe("<html>opencode</html>")
+    expect(proxiedUrl).toBe("https://app.opencode.ai/")
+  })
+
+  test("keeps matched API routes ahead of the UI fallback", async () => {
+    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+    globalThis.fetch = (() => {
+      throw new Error("UI fallback should not handle matched API routes")
+    }) as unknown as typeof fetch
+
+    const response = await Server.Default().app.request("/session/nope")
+
+    expect(response.status).toBe(404)
+  })
+})