Procházet zdrojové kódy

fix(server): serve embedded UI from bunfs (#25632)

Kit Langton před 3 měsíci
rodič
revize
6312c55d55

+ 26 - 13
packages/opencode/src/server/shared/ui.ts

@@ -45,6 +45,31 @@ export function embeddedUI() {
   return embeddedUIPromise
 }
 
+function notFound() {
+  return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
+}
+
+function embeddedUIResponse(file: string, body: Uint8Array) {
+  const mime = AppFileSystem.mimeType(file)
+  const headers = new Headers({ "content-type": mime })
+  if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
+  return HttpServerResponse.raw(body, { headers })
+}
+
+export function serveEmbeddedUIEffect(
+  requestPath: string,
+  fs: AppFileSystem.Interface,
+  embeddedWebUI: Record<string, string>,
+) {
+  const file = embeddedWebUI[requestPath.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
+  if (!file) return Effect.succeed(notFound())
+
+  return fs.readFile(file).pipe(
+    Effect.map((body) => embeddedUIResponse(file, body)),
+    Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(notFound())),
+  )
+}
+
 export function serveUIEffect(
   request: HttpServerRequest.HttpServerRequest,
   services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
@@ -53,19 +78,7 @@ export function serveUIEffect(
     const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
     const path = new URL(request.url, "http://localhost").pathname
 
-    if (embeddedWebUI) {
-      const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
-      if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
-
-      if (yield* services.fs.existsSafe(match)) {
-        const mime = AppFileSystem.mimeType(match)
-        const headers = new Headers({ "content-type": mime })
-        if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
-        return HttpServerResponse.raw(yield* services.fs.readFile(match), { headers })
-      }
-
-      return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
-    }
+    if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI)
 
     const response = yield* services.client.execute(
       HttpClientRequest.make(request.method)(upstreamURL(path), {

+ 34 - 1
packages/opencode/test/server/httpapi-ui.test.ts

@@ -15,7 +15,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { ServerAuth } from "../../src/server/auth"
 import { authorizationRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/authorization"
 import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
-import { serveUIEffect } from "../../src/server/shared/ui"
+import { serveEmbeddedUIEffect, serveUIEffect } from "../../src/server/shared/ui"
 import { Server } from "../../src/server/server"
 
 void Log.init({ print: false })
@@ -184,6 +184,39 @@ describe("HttpApi UI fallback", () => {
     expect(await response.text()).toBe("console.log('ok')")
   })
 
+  test("serves embedded UI assets when Bun can read them but access reports missing", async () => {
+    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+    let readPath: string | undefined
+
+    const response = await Effect.runPromise(
+      Effect.gen(function* () {
+        const fs = yield* AppFileSystem.Service
+        return yield* serveEmbeddedUIEffect(
+          "/assets/app.js",
+          {
+            ...fs,
+            existsSafe: () => Effect.die("embedded UI should not rely on filesystem access checks"),
+            readFile: (path) => {
+              readPath = path
+              return path === "/$bunfs/root/assets/app.js"
+                ? Effect.succeed(new TextEncoder().encode("console.log('embedded')"))
+                : Effect.die(`unexpected embedded UI path: ${path}`)
+            },
+          },
+          { "assets/app.js": "/$bunfs/root/assets/app.js" },
+        )
+      }).pipe(
+        Effect.provide(AppFileSystem.defaultLayer),
+        Effect.map(HttpServerResponse.toWeb),
+      ),
+    )
+
+    expect(response.status).toBe(200)
+    expect(readPath).toBe("/$bunfs/root/assets/app.js")
+    expect(response.headers.get("content-type")).toContain("text/javascript")
+    expect(await response.text()).toBe("console.log('embedded')")
+  })
+
   test("keeps matched API routes ahead of the UI fallback", async () => {
     Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true