Просмотр исходного кода

Build HttpApi UI route from services (#25177)

Kit Langton 3 месяцев назад
Родитель
Сommit
fc155e9fc5

+ 13 - 4
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 { FetchHttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
+import { FetchHttpClient, HttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
 import * as Socket from "effect/unstable/socket/Socket"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { Account } from "@/account/account"
@@ -121,9 +121,16 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe
   ]),
 )
 
-const uiRoute = HttpRouter.add("*", "/*", (request) =>
-  serveUIEffect(request).pipe(Effect.provide(AppFileSystem.defaultLayer), Effect.provide(FetchHttpClient.layer)),
-).pipe(Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))))
+const uiRoute = Layer.effectDiscard(
+  Effect.gen(function* () {
+    const fs = yield* AppFileSystem.Service
+    const client = yield* HttpClient.HttpClient
+    const router = yield* HttpRouter.HttpRouter
+    yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
+  }),
+).pipe(
+  Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))),
+)
 
 export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pipe(
   Layer.provide([
@@ -162,6 +169,8 @@ export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes, uiRoute).pip
     Workspace.defaultLayer,
     Worktree.defaultLayer,
     Bus.layer,
+    AppFileSystem.defaultLayer,
+    FetchHttpClient.layer,
     HttpServer.layerServices,
   ]),
   Layer.provideMerge(Observability.layer),

+ 7 - 5
packages/opencode/src/server/routes/ui.ts

@@ -79,7 +79,10 @@ export async function serveUI(request: Request) {
   return response
 }
 
-export function serveUIEffect(request: HttpServerRequest.HttpServerRequest) {
+export function serveUIEffect(
+  request: HttpServerRequest.HttpServerRequest,
+  services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
+) {
   return Effect.gen(function* () {
     const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
     const path = new URL(request.url, "http://localhost").pathname
@@ -88,18 +91,17 @@ export function serveUIEffect(request: HttpServerRequest.HttpServerRequest) {
       const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
       if (!match) return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
 
-      const fs = yield* AppFileSystem.Service
-      if (yield* fs.existsSafe(match)) {
+      if (yield* services.fs.existsSafe(match)) {
         const mime = getMimeType(match) ?? "text/plain"
         const headers = new Headers({ "content-type": mime })
         if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP)
-        return HttpServerResponse.raw(yield* fs.readFile(match), { headers })
+        return HttpServerResponse.raw(yield* services.fs.readFile(match), { headers })
       }
 
       return HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })
     }
 
-    const response = yield* HttpClient.execute(
+    const response = yield* services.client.execute(
       HttpClientRequest.make(request.method)(upstreamURL(path), {
         headers: ProxyUtil.headers(request.headers, { host: UI_UPSTREAM.host }),
         body: requestBody(request),

+ 37 - 27
packages/opencode/test/server/httpapi-ui.test.ts

@@ -74,22 +74,26 @@ function app(input?: { password?: string; username?: string }) {
 
 function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer<HttpClient.HttpClient> }) {
   const handler = HttpRouter.toWebHandler(
-    HttpRouter.add("*", "/*", (request) =>
-      serveUIEffect(request).pipe(
-        Effect.provide(AppFileSystem.defaultLayer),
-        Effect.provide(input?.client ?? httpClient(new Response("ui"))),
-      ),
+    Layer.effectDiscard(
+      Effect.gen(function* () {
+        const fs = yield* AppFileSystem.Service
+        const client = yield* HttpClient.HttpClient
+        const router = yield* HttpRouter.HttpRouter
+        yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
+      }),
     ).pipe(
       Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuthConfig.defaultLayer))),
-      Layer.provide(HttpServer.layerServices),
-      Layer.provide(
+      Layer.provide([
+        AppFileSystem.defaultLayer,
+        input?.client ?? httpClient(new Response("ui")),
+        HttpServer.layerServices,
         ConfigProvider.layer(
           ConfigProvider.fromUnknown({
             OPENCODE_SERVER_PASSWORD: input?.password,
             OPENCODE_SERVER_USERNAME: input?.username,
           }),
         ),
-      ),
+      ]),
     ),
     { disableLogger: true },
   ).handler
@@ -140,26 +144,32 @@ describe("HttpApi UI fallback", () => {
     let proxiedUrl: string | undefined
 
     const response = await Effect.runPromise(
-      serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js"))).pipe(
-        Effect.provide(AppFileSystem.defaultLayer),
+      Effect.gen(function* () {
+        const fs = yield* AppFileSystem.Service
+        const client = yield* HttpClient.HttpClient
+        return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js")), { fs, client })
+      }).pipe(
         Effect.provide(
-          Layer.succeed(
-            HttpClient.HttpClient,
-            HttpClient.make((request) => {
-              proxiedUrl = request.url
-              return Effect.succeed(
-                HttpClientResponse.fromWeb(
-                  request,
-                  new Response("console.log('ok')", {
-                    headers: {
-                      "content-encoding": "br",
-                      "content-length": "999",
-                      "content-type": "text/javascript",
-                    },
-                  }),
-                ),
-              )
-            }),
+          Layer.mergeAll(
+            AppFileSystem.defaultLayer,
+            Layer.succeed(
+              HttpClient.HttpClient,
+              HttpClient.make((request) => {
+                proxiedUrl = request.url
+                return Effect.succeed(
+                  HttpClientResponse.fromWeb(
+                    request,
+                    new Response("console.log('ok')", {
+                      headers: {
+                        "content-encoding": "br",
+                        "content-length": "999",
+                        "content-type": "text/javascript",
+                      },
+                    }),
+                  ),
+                )
+              }),
+            ),
           ),
         ),
         Effect.map(HttpServerResponse.toWeb),