diff --git a/apps/server/README.md b/apps/server/README.md index dc3b4bbe1c..f96fd9f2fa 100644 --- a/apps/server/README.md +++ b/apps/server/README.md @@ -26,16 +26,27 @@ apps/server/src/ └── workflows-hono/ # /api/workflows/* Hono sub-app (agent-signal, memory, task) ``` -The package's exports resolve via the `@/server/*` alias (dual-path tsconfig: `apps/server/src/*` first, `src/server/*` fallback for the SSR-page helpers that still live there). +The package's exports resolve via the `~server/*` alias (`apps/server/src/*`); `@/server/*` now refers only to the Next SSR-page helpers remaining in `src/server/`. + +## Next as a Shell + +Every `src/app/(backend)/**/route.ts` is a thin forwarder into this package: + +- In dev, `fetchHonoRuntime` proxies each request to the standalone Hono server (`LOBE_DEV_HONO_TARGET`, default `http://localhost:3011`). +- In production, it loads the vite-built `apps/server/dist/index.js` in-process via an opaque runtime `require` (override with `LOBE_HONO_DIST_ENTRY`), so the backend never passes through `next build`. +- Route file paths, HTTP method export names, and segment config (`maxDuration`, `dynamic`) are contractual — the cloud repo re-exports them by path. `src/app/(backend)/route-shell.guard.test.ts` enforces that route files stay logic-free. +- Exceptions: `webapi/revalidate` (Next ISR machinery) and `hono-runtime/[...path]` (the binding route itself). + +The root `build` / `build:raw` scripts run `build:hono` first so the dist always pairs with the Next build. ## Dev Modes -| Mode | Command | Topology | -| ------------------- | ----------------------- | ---------------------------------------- | -| **Classic** | `bun run dev` | Next (`:3010`) + Vite (`:9876`) | -| **Hono-Lite** (POC) | `bun run dev:hono-lite` | Hono (`:3011`) + Vite (`:9876`), no Next | +| Mode | Command | Topology | +| ------------------- | ----------------------- | ------------------------------------------------------ | +| **Classic** | `bun run dev` | Hono (`:3011`) + Next shell (`:3010`) + Vite (`:9876`) | +| **Hono-Lite** (POC) | `bun run dev:hono-lite` | Hono (`:3011`) + Vite (`:9876`), no Next | -Both modes coexist on this branch — pick whichever fits the task. +Both modes coexist on this branch — pick whichever fits the task. Classic dev now spawns the Hono server automatically and points the Next API shells at it. ### Standalone Server (this package only) diff --git a/src/app/(backend)/api/agent/stream/__tests__/route.test.ts b/apps/server/src/api-runtime/__tests__/agentStream.test.ts similarity index 95% rename from src/app/(backend)/api/agent/stream/__tests__/route.test.ts rename to apps/server/src/api-runtime/__tests__/agentStream.test.ts index 1327e56602..2f8ac85009 100644 --- a/src/app/(backend)/api/agent/stream/__tests__/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/agentStream.test.ts @@ -2,7 +2,7 @@ import { NextRequest } from 'next/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { GET } from '../route'; +import { agentStreamAPIHandler } from '../agentStream'; // Mock dependencies first const mockStreamEventManager = { @@ -14,7 +14,7 @@ vi.mock('~server/modules/AgentRuntime', () => ({ createStreamEventManager: vi.fn(() => mockStreamEventManager), })); -describe('/api/agent/stream route', () => { +describe('agentStreamAPIHandler', () => { const MOCK_TIMESTAMP = 1758203237000; beforeEach(() => { @@ -30,7 +30,7 @@ describe('/api/agent/stream route', () => { describe('GET handler', () => { it('should return 400 when operationId parameter is missing', async () => { const request = new NextRequest('https://test.com/api/agent/stream'); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(400); const data = await response.json(); @@ -41,7 +41,7 @@ describe('/api/agent/stream route', () => { const request = new NextRequest( 'https://test.com/api/agent/stream?operationId=test-operation', ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toBe('text/event-stream'); @@ -62,7 +62,7 @@ describe('/api/agent/stream route', () => { 'https://test.com/api/agent/stream?operationId=test-operation&lastEventId=123', ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); const decoder = new TextDecoder(); const reader = response.body!.getReader(); @@ -133,7 +133,7 @@ describe('/api/agent/stream route', () => { ]; mockStreamEventManager.getStreamHistory.mockResolvedValue(mockEvents); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); const decoder = new TextDecoder(); const reader = response.body!.getReader(); @@ -215,7 +215,7 @@ describe('/api/agent/stream route', () => { ]; mockStreamEventManager.getStreamHistory.mockResolvedValue(mockEvents); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); const decoder = new TextDecoder(); const reader = response.body!.getReader(); @@ -285,7 +285,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" new Error('Redis connection failed'), ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); const decoder = new TextDecoder(); const reader = response.body!.getReader(); @@ -347,7 +347,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" mockStreamEventManager.subscribeStreamEvents.mockResolvedValue(undefined); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); @@ -373,7 +373,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" mockStreamEventManager.subscribeStreamEvents.mockResolvedValue(undefined); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); @@ -394,7 +394,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" 'https://test.com/api/agent/stream?operationId=test-operation', ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); const decoder = new TextDecoder(); const reader = response.body!.getReader(); @@ -456,7 +456,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); // Verify the subscription was set up correctly expect(mockStreamEventManager.subscribeStreamEvents).toHaveBeenCalledWith( @@ -491,7 +491,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); // Verify we captured the callback expect(capturedCallback).toBeDefined(); @@ -527,7 +527,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); // Verify we captured the callback expect(capturedCallback).toBeDefined(); @@ -563,7 +563,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(capturedCallback).toBeDefined(); expect(response.status).toBe(200); @@ -624,7 +624,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(capturedCallback).toBeDefined(); expect(capturedSignal).toBeDefined(); @@ -661,7 +661,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); expect(capturedCallback).toBeDefined(); @@ -695,7 +695,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); expect(capturedCallback).toBeDefined(); @@ -730,7 +730,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); expect(capturedCallback).toBeDefined(); @@ -777,7 +777,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" }, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); expect(capturedCallback).toBeDefined(); @@ -818,7 +818,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" `https://test.com/api/agent/stream?operationId=${operationId}`, ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); }); @@ -828,7 +828,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" 'https://test.com/api/agent/stream?operationId=test&lastEventId=12345', ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); }); @@ -838,7 +838,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" 'https://test.com/api/agent/stream?operationId=test&includeHistory=false', ); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(200); expect(mockStreamEventManager.getStreamHistory).not.toHaveBeenCalled(); @@ -847,7 +847,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data" it('should handle invalid URL gracefully', async () => { const request = new NextRequest('https://test.com/api/agent/stream?operationId='); - const response = await GET(request); + const response = await agentStreamAPIHandler(request); expect(response.status).toBe(400); const data = await response.json(); diff --git a/src/app/(backend)/api/auth/[...all]/route.test.ts b/apps/server/src/api-runtime/__tests__/betterAuth.test.ts similarity index 89% rename from src/app/(backend)/api/auth/[...all]/route.test.ts rename to apps/server/src/api-runtime/__tests__/betterAuth.test.ts index 107f56095c..6967fd5811 100644 --- a/src/app/(backend)/api/auth/[...all]/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/betterAuth.test.ts @@ -2,7 +2,7 @@ import type { NextRequest } from 'next/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { GET, POST } from './route'; +import { betterAuthAPIHandler } from '../betterAuth'; type RouteHandler = (request: Request) => Promise; @@ -29,7 +29,7 @@ const createPostRequest = (body: string, contentType = 'application/json') => method: 'POST', }) as NextRequest; -describe('/api/auth/[...all] route', () => { +describe('betterAuthAPIHandler', () => { beforeEach(() => { vi.clearAllMocks(); mocks.get.mockResolvedValue(Response.json({ ok: true })); @@ -37,7 +37,7 @@ describe('/api/auth/[...all] route', () => { }); it('returns 400 for malformed JSON auth requests before Better Auth handles them', async () => { - const response = await POST( + const response = await betterAuthAPIHandler( createPostRequest('{"email":"user@example.com","password":"secret",}'), ); @@ -54,7 +54,7 @@ describe('/api/auth/[...all] route', () => { Response.json(await request.json()), ); - const response = await POST( + const response = await betterAuthAPIHandler( createPostRequest(JSON.stringify({ email: 'user@example.com', password: 'secret' })), ); @@ -66,7 +66,7 @@ describe('/api/auth/[...all] route', () => { }); it('delegates non-JSON auth requests to Better Auth', async () => { - const response = await POST( + const response = await betterAuthAPIHandler( createPostRequest( 'email=user%40example.com&password=secret', 'application/x-www-form-urlencoded', @@ -80,7 +80,7 @@ describe('/api/auth/[...all] route', () => { it('delegates GET requests to Better Auth', async () => { const request = new Request('https://localhost/api/auth/get-session') as NextRequest; - const response = await GET(request); + const response = await betterAuthAPIHandler(request); expect(response.status).toBe(200); expect(mocks.get).toHaveBeenCalledWith(request); diff --git a/src/app/(backend)/webapi/chat/[provider]/route.test.ts b/apps/server/src/api-runtime/__tests__/chat.test.ts similarity index 85% rename from src/app/(backend)/webapi/chat/[provider]/route.test.ts rename to apps/server/src/api-runtime/__tests__/chat.test.ts index 5849c3da2e..75b28ad4be 100644 --- a/src/app/(backend)/webapi/chat/[provider]/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/chat.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { auth } from '@/auth'; import { initModelRuntimeFromDB } from '~server/modules/ModelRuntime'; -import { POST } from './route'; +import { chatAPIHandler } from '../chat'; vi.mock('@/app/(backend)/middleware/auth/utils', () => ({ checkAuthMethod: vi.fn(), @@ -45,11 +45,9 @@ afterEach(() => { vi.clearAllMocks(); }); -describe('POST handler', () => { +describe('chatAPIHandler', () => { describe('init chat model', () => { it('should initialize ModelRuntime correctly with valid session', async () => { - const mockParams = Promise.resolve({ provider: 'test-provider' }); - const mockChatResponse = new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' }, }); @@ -60,7 +58,7 @@ describe('POST handler', () => { vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - await POST(request as unknown as Request, { params: mockParams }); + await chatAPIHandler(request as unknown as Request, { provider: 'test-provider' }); expect(initModelRuntimeFromDB).toHaveBeenCalledWith( expect.anything(), @@ -73,9 +71,7 @@ describe('POST handler', () => { it('should return Unauthorized error when no session exists', async () => { vi.mocked(auth.api.getSession).mockResolvedValue(null); - const mockParams = Promise.resolve({ provider: 'test-provider' }); - - const response = await POST(request, { params: mockParams }); + const response = await chatAPIHandler(request, { provider: 'test-provider' }); expect(response.status).toBe(401); }); @@ -83,7 +79,6 @@ describe('POST handler', () => { describe('chat', () => { it('should correctly handle chat completion with valid payload', async () => { - const mockParams = Promise.resolve({ provider: 'test-provider' }); const mockChatPayload = { message: 'Hello, world!' }; request = new Request(new URL('https://test.com'), { method: 'POST', @@ -98,7 +93,9 @@ describe('POST handler', () => { vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await POST(request as unknown as Request, { params: mockParams }); + const response = await chatAPIHandler(request as unknown as Request, { + provider: 'test-provider', + }); expect(response).toEqual(mockChatResponse); expect(mockRuntime.chat).toHaveBeenCalledWith(mockChatPayload, { @@ -108,7 +105,6 @@ describe('POST handler', () => { }); it('should return an error response when chat completion fails', async () => { - const mockParams = Promise.resolve({ provider: 'test-provider' }); const mockChatPayload = { message: 'Hello, world!' }; request = new Request(new URL('https://test.com'), { method: 'POST', @@ -128,7 +124,7 @@ describe('POST handler', () => { vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await POST(request, { params: mockParams }); + const response = await chatAPIHandler(request, { provider: 'test-provider' }); expect(response.status).toBe(500); expect(await response.json()).toEqual({ diff --git a/src/app/(backend)/oauth/connector/callback/route.test.ts b/apps/server/src/api-runtime/__tests__/connectorOAuthCallback.test.ts similarity index 86% rename from src/app/(backend)/oauth/connector/callback/route.test.ts rename to apps/server/src/api-runtime/__tests__/connectorOAuthCallback.test.ts index 23d2caa035..e03c48a3ec 100644 --- a/src/app/(backend)/oauth/connector/callback/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/connectorOAuthCallback.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { GET } from './route'; +import { connectorOAuthCallbackAPIHandler } from '../connectorOAuthCallback'; const { mockConsume, mockFindById, mockSync, mockUpdate } = vi.hoisted(() => ({ mockConsume: vi.fn(), @@ -41,7 +41,7 @@ vi.mock('@/database/models/connectorTool', () => ({ vi.mock('~server/services/connector/sync', () => ({ syncConnectorToolsById: mockSync })); const makeReq = () => - ({ nextUrl: { searchParams: new URLSearchParams('code=abc&state=xyz') } }) as any; + new Request('https://app.example.com/oauth/connector/callback?code=abc&state=xyz'); beforeEach(() => { vi.clearAllMocks(); @@ -62,11 +62,11 @@ beforeEach(() => { mockUpdate.mockResolvedValue(undefined); }); -describe('connector OAuth callback', () => { +describe('connectorOAuthCallbackAPIHandler', () => { it('reports synced:false when auth succeeds but tool sync fails', async () => { mockSync.mockRejectedValue(new Error('mcp down')); - const body = await (await GET(makeReq())).text(); + const body = await (await connectorOAuthCallbackAPIHandler(makeReq())).text(); expect(body).toContain('"success":true'); expect(body).toContain('"synced":false'); @@ -75,7 +75,7 @@ describe('connector OAuth callback', () => { it('reports synced:true when auth and tool sync both succeed', async () => { mockSync.mockResolvedValue({ toolCount: 5 }); - const body = await (await GET(makeReq())).text(); + const body = await (await connectorOAuthCallbackAPIHandler(makeReq())).text(); expect(body).toContain('"success":true'); expect(body).toContain('"synced":true'); diff --git a/src/app/(backend)/f/[id]/route.test.ts b/apps/server/src/api-runtime/__tests__/fileProxy.test.ts similarity index 90% rename from src/app/(backend)/f/[id]/route.test.ts rename to apps/server/src/api-runtime/__tests__/fileProxy.test.ts index e1d19c9bbd..6c93a53cf6 100644 --- a/src/app/(backend)/f/[id]/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/fileProxy.test.ts @@ -7,7 +7,7 @@ import type { FileItem } from '@/database/schemas'; import { getServerDB } from '@/database/server'; import { FileService } from '~server/services/file'; -import { GET } from './route'; +import { fileProxyAPIHandler } from '../fileProxy'; const fileServiceMocks = vi.hoisted(() => { const instance = { @@ -35,7 +35,7 @@ vi.mock('~server/services/file', () => ({ FileService: fileServiceMocks.FileService, })); -describe('file proxy route', () => { +describe('fileProxyAPIHandler', () => { const db = {} as LobeChatDatabase; beforeEach(() => { @@ -53,8 +53,8 @@ describe('file proxy route', () => { }); it('should redirect to a cached presigned preview URL instead of a public full file URL', async () => { - const response = await GET(new Request('https://lobehub.com/f/file-id'), { - params: Promise.resolve({ id: 'file-id' }), + const response = await fileProxyAPIHandler(new Request('https://lobehub.com/f/file-id'), { + id: 'file-id', }); expect(response.status).toBe(302); diff --git a/src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/__tests__/route.test.ts b/apps/server/src/api-runtime/__tests__/memoryExtractionBenchmark.test.ts similarity index 92% rename from src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/__tests__/route.test.ts rename to apps/server/src/api-runtime/__tests__/memoryExtractionBenchmark.test.ts index 94cabaa00a..bd2769648e 100644 --- a/src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/__tests__/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/memoryExtractionBenchmark.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { memoryExtractionBenchmarkLoCoMoWebhookAPIHandler } from '../memoryExtractionBenchmark'; + const replaceParts = vi.fn(); const upsertSource = vi.fn(); const extractBenchmarkSource = vi.fn(); @@ -27,7 +29,7 @@ vi.mock('@lobechat/memory-user-memory', () => ({ BenchmarkLocomoContextProvider: vi.fn().mockImplementation((params) => params), })); -describe('benchmark LoCoMo memory extraction webhook', () => { +describe('memoryExtractionBenchmarkLoCoMoWebhookAPIHandler', () => { beforeEach(() => { replaceParts.mockReset(); upsertSource.mockReset(); @@ -42,8 +44,7 @@ describe('benchmark LoCoMo memory extraction webhook', () => { }); it('returns per-session ingestion results and inserted part count', async () => { - const { POST } = await import('../route'); - const response = await POST( + const response = await memoryExtractionBenchmarkLoCoMoWebhookAPIHandler( new Request('http://localhost/api/webhooks/memory-extraction/benchmark-locomo', { body: JSON.stringify({ sampleId: 'conv-26', diff --git a/src/app/(backend)/webapi/models/[provider]/route.test.ts b/apps/server/src/api-runtime/__tests__/models.test.ts similarity index 85% rename from src/app/(backend)/webapi/models/[provider]/route.test.ts rename to apps/server/src/api-runtime/__tests__/models.test.ts index 7cd2ba3951..053ad83300 100644 --- a/src/app/(backend)/webapi/models/[provider]/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/models.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { auth } from '@/auth'; import { initModelRuntimeFromDB } from '~server/modules/ModelRuntime'; -import { GET } from './route'; +import { modelsAPIHandler } from '../models'; vi.mock('@/app/(backend)/middleware/auth/utils', () => ({ checkAuthMethod: vi.fn(), @@ -43,11 +43,9 @@ afterEach(() => { vi.clearAllMocks(); }); -describe('GET handler', () => { +describe('modelsAPIHandler', () => { describe('error handling', () => { it('should not expose stack trace when an Error is thrown', async () => { - const mockParams = Promise.resolve({ provider: 'google' }); - const errorWithStack = new Error('Something went wrong'); errorWithStack.stack = 'Error: Something went wrong\n at Object. (/path/to/file.ts:10:15)'; @@ -59,7 +57,7 @@ describe('GET handler', () => { }; vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await GET(request, { params: mockParams }); + const response = await modelsAPIHandler(request, { provider: 'google' }); const responseBody = await response.json(); expect(responseBody.body.error.name).toBe('Error'); @@ -72,8 +70,6 @@ describe('GET handler', () => { }); it('should preserve error name for custom error types', async () => { - const mockParams = Promise.resolve({ provider: 'google' }); - class CustomError extends Error { constructor(message: string) { super(message); @@ -91,7 +87,7 @@ describe('GET handler', () => { }; vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await GET(request, { params: mockParams }); + const response = await modelsAPIHandler(request, { provider: 'google' }); const responseBody = await response.json(); expect(responseBody.body.error.name).toBe('CustomError'); @@ -100,8 +96,6 @@ describe('GET handler', () => { }); it('should pass through structured error objects as-is', async () => { - const mockParams = Promise.resolve({ provider: 'google' }); - const structuredError = { errorType: ChatErrorType.InternalServerError, error: { code: 'PROVIDER_ERROR', details: 'API limit exceeded' }, @@ -114,7 +108,7 @@ describe('GET handler', () => { }; vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await GET(request, { params: mockParams }); + const response = await modelsAPIHandler(request, { provider: 'google' }); const responseBody = await response.json(); expect(responseBody.body.error.code).toBe('PROVIDER_ERROR'); @@ -122,8 +116,6 @@ describe('GET handler', () => { }); it('should return correct status code for errors', async () => { - const mockParams = Promise.resolve({ provider: 'google' }); - const mockRuntime: LobeRuntimeAI = { baseURL: 'abc', chat: vi.fn(), @@ -131,14 +123,12 @@ describe('GET handler', () => { }; vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await GET(request, { params: mockParams }); + const response = await modelsAPIHandler(request, { provider: 'google' }); expect(response.status).toBe(500); }); it('should include provider in error response', async () => { - const mockParams = Promise.resolve({ provider: 'openai' }); - const mockRuntime: LobeRuntimeAI = { baseURL: 'abc', chat: vi.fn(), @@ -146,7 +136,7 @@ describe('GET handler', () => { }; vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await GET(request, { params: mockParams }); + const response = await modelsAPIHandler(request, { provider: 'openai' }); const responseBody = await response.json(); expect(responseBody.body.provider).toBe('openai'); @@ -155,8 +145,6 @@ describe('GET handler', () => { describe('success cases', () => { it('should return model list on success', async () => { - const mockParams = Promise.resolve({ provider: 'openai' }); - const mockModelList = [ { id: 'gpt-4', name: 'GPT-4' }, { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' }, @@ -169,7 +157,7 @@ describe('GET handler', () => { }; vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - const response = await GET(request, { params: mockParams }); + const response = await modelsAPIHandler(request, { provider: 'openai' }); const responseBody = await response.json(); expect(response.status).toBe(200); diff --git a/src/app/(backend)/oidc/[...oidc]/route.test.ts b/apps/server/src/api-runtime/__tests__/oidcProvider.test.ts similarity index 53% rename from src/app/(backend)/oidc/[...oidc]/route.test.ts rename to apps/server/src/api-runtime/__tests__/oidcProvider.test.ts index 31ebce31bd..11c665214e 100644 --- a/src/app/(backend)/oidc/[...oidc]/route.test.ts +++ b/apps/server/src/api-runtime/__tests__/oidcProvider.test.ts @@ -32,7 +32,22 @@ vi.mock('~server/services/oidc/oidcProvider', () => ({ })), })); -describe('OIDC route', () => { +const makeRequest = () => + new Request('https://example.com/oidc/token', { + body: 'grant_type=refresh_token', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + method: 'POST', + }) as unknown as NextRequest; + +const callWithTimeout = (handler: (req: NextRequest) => Promise, request: NextRequest) => + Promise.race([ + handler(request), + new Promise((_, reject) => + setTimeout(() => reject(new Error('OIDC route timed out')), 50), + ), + ]); + +describe('oidcProviderAPIHandler', () => { beforeEach(() => { vi.clearAllMocks(); @@ -48,22 +63,24 @@ describe('OIDC route', () => { it('returns a 500 response when creating the Node request fails', async () => { mocks.createNodeRequest.mockRejectedValueOnce(new Error('body stream aborted')); - const { POST } = await import('./route'); - const request = new Request('https://example.com/oidc/token', { - body: 'grant_type=refresh_token', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - method: 'POST', - }) as unknown as NextRequest; - - const response = await Promise.race([ - POST(request), - new Promise((_, reject) => - setTimeout(() => reject(new Error('OIDC route timed out')), 50), - ), - ]); + const { oidcProviderAPIHandler } = await import('../oidc'); + const response = await callWithTimeout(oidcProviderAPIHandler, makeRequest()); expect(response.status).toBe(500); await expect(response.text()).resolves.toContain('body stream aborted'); expect(mocks.middleware).not.toHaveBeenCalled(); }); + + it('returns a 500 response when the OIDC provider flow fails', async () => { + mocks.providerCallback.mockImplementationOnce(() => { + throw new Error('callback exploded'); + }); + + const { oidcProviderAPIHandler } = await import('../oidc'); + const response = await callWithTimeout(oidcProviderAPIHandler, makeRequest()); + + expect(response.status).toBe(500); + await expect(response.text()).resolves.toContain('callback exploded'); + expect(mocks.middleware).not.toHaveBeenCalled(); + }); }); diff --git a/apps/server/src/api-runtime/oidc.ts b/apps/server/src/api-runtime/oidc.ts index a8fd0f103c..07fb0dc5c8 100644 --- a/apps/server/src/api-runtime/oidc.ts +++ b/apps/server/src/api-runtime/oidc.ts @@ -339,7 +339,7 @@ export const oidcProviderAPIHandler = async (request: Request) => { } }); providerLog('Middleware call initiated, waiting for its callback OR nodeResponse.end()...'); - }); + }, reject); }); providerLog('Promise surrounding middleware call resolved.'); diff --git a/package.json b/package.json index 0b50551683..41b9db8738 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,13 @@ "apps/desktop/src/main" ], "scripts": { - "build": "bun run build:spa && bun run build:spa:auth && bun run build:spa:copy && bun run build:next", + "build": "bun run build:hono && bun run build:spa && bun run build:spa:auth && bun run build:spa:copy && bun run build:next", "build:analyze": "cross-env NODE_OPTIONS=--max-old-space-size=81920 next experimental-analyze", "build:docker": "pnpm run build:spa && pnpm run build:spa:mobile && pnpm run build:spa:auth && pnpm run build:spa:copy && cross-env NODE_OPTIONS=--max-old-space-size=8192 DOCKER=true next build", "build:hono": "pnpm --filter @lobechat/server build", "build:next": "cross-env NODE_OPTIONS=--max-old-space-size=7168 bun run build:next:raw", "build:next:raw": "next build", - "build:raw": "bun run build:spa:raw && bun run build:spa:auth && bun run build:spa:copy && bun run build:next:raw", + "build:raw": "bun run build:hono && bun run build:spa:raw && bun run build:spa:auth && bun run build:spa:copy && bun run build:next:raw", "build:spa": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm run build:spa:raw", "build:spa:auth": "rm -rf public/_spa-auth && cross-env NODE_OPTIONS=--max-old-space-size=8192 AUTH=true vite build", "build:spa:copy": "tsx scripts/copySpaBuild.mts && tsx scripts/generateSpaTemplates.mts", diff --git a/scripts/devStartupSequence.mts b/scripts/devStartupSequence.mts index 43a0a2ab8b..a731e9ab98 100644 --- a/scripts/devStartupSequence.mts +++ b/scripts/devStartupSequence.mts @@ -42,8 +42,10 @@ let nextPort = 3010; let nextRootUrl = `http://${NEXT_HOST}:${nextPort}/`; let nextProcess: ChildProcess | undefined; let viteProcess: ChildProcess | undefined; +let honoProcess: ChildProcess | undefined; let nextHandle: DevProcessHandle | undefined; let viteHandle: DevProcessHandle | undefined; +let honoHandle: DevProcessHandle | undefined; let forceKillTimer: ReturnType | undefined; let shuttingDown = false; @@ -171,11 +173,13 @@ const runNextBackgroundTasks = () => { const terminateChildren = () => { sendSignalToDevProcess(viteHandle, 'SIGTERM'); sendSignalToDevProcess(nextHandle, 'SIGTERM'); + sendSignalToDevProcess(honoHandle, 'SIGTERM'); }; const forceKillChildren = () => { sendSignalToDevProcess(viteHandle, 'SIGKILL'); sendSignalToDevProcess(nextHandle, 'SIGKILL'); + sendSignalToDevProcess(honoHandle, 'SIGKILL'); }; const clearForceKillTimer = () => { @@ -189,7 +193,8 @@ const hasChildSettled = (child?: ChildProcess) => const clearForceKillTimerWhenChildrenSettle = () => { if (!shuttingDown) return; - if (hasChildSettled(nextProcess) && hasChildSettled(viteProcess)) clearForceKillTimer(); + if (hasChildSettled(nextProcess) && hasChildSettled(viteProcess) && hasChildSettled(honoProcess)) + clearForceKillTimer(); }; const shutdownAll = (signal: NodeJS.Signals) => { @@ -209,7 +214,7 @@ const shutdownAll = (signal: NodeJS.Signals) => { }, FORCE_KILL_TIMEOUT_MS); }; -const watchChildExit = (child: ChildProcess, name: 'next' | 'vite') => { +const watchChildExit = (child: ChildProcess, name: 'hono' | 'next' | 'vite') => { child.once('exit', (code, signal) => { if (shuttingDown) { clearForceKillTimerWhenChildrenSettle(); @@ -247,6 +252,15 @@ const main = async () => { forceKillChildren(); }); + // The (backend) routes are thin shells forwarding into the standalone Hono + // runtime; point them at the dev Hono server and spawn it alongside Next. + const honoPort = Number.parseInt(process.env.HONO_PORT || '', 10) || 3011; + process.env.LOBE_DEV_HONO_TARGET ||= `http://localhost:${honoPort}`; + + honoProcess = runNpmScript('dev:hono:server'); + honoHandle = createDevProcessHandle({ isWindows, pid: honoProcess.pid }); + watchChildExit(honoProcess, 'hono'); + nextProcess = spawn('npx', ['next', 'dev', '-p', String(nextPort)], { detached: !isWindows, env: process.env, @@ -264,6 +278,7 @@ const main = async () => { await Promise.race([ new Promise((resolve) => nextProcess?.once('exit', resolve)), new Promise((resolve) => viteProcess?.once('exit', resolve)), + new Promise((resolve) => honoProcess?.once('exit', resolve)), ]); }; diff --git a/src/app/(backend)/api/agent/[[...route]]/route.ts b/src/app/(backend)/api/agent/[[...route]]/route.ts index b777175161..84768f130a 100644 --- a/src/app/(backend)/api/agent/[[...route]]/route.ts +++ b/src/app/(backend)/api/agent/[[...route]]/route.ts @@ -1,6 +1,6 @@ -import app from '~server/agent-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -const handler = (request: Request) => app.fetch(request); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/api/agent/stream/route.ts b/src/app/(backend)/api/agent/stream/route.ts index efcfefeccd..820bb1b73a 100644 --- a/src/app/(backend)/api/agent/stream/route.ts +++ b/src/app/(backend)/api/agent/stream/route.ts @@ -1,3 +1,5 @@ -import { agentStreamAPIHandler } from '~server/api-runtime/agentStream'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const GET = (req: Request) => agentStreamAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/api/auth/[...all]/route.ts b/src/app/(backend)/api/auth/[...all]/route.ts index fd684b4b92..84768f130a 100644 --- a/src/app/(backend)/api/auth/[...all]/route.ts +++ b/src/app/(backend)/api/auth/[...all]/route.ts @@ -1,6 +1,6 @@ -import { betterAuthAPIHandler } from '~server/api-runtime/betterAuth'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -const handler = (req: Request) => betterAuthAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/api/auth/check-user/route.ts b/src/app/(backend)/api/auth/check-user/route.ts index 8a10899fe1..5018771c75 100644 --- a/src/app/(backend)/api/auth/check-user/route.ts +++ b/src/app/(backend)/api/auth/check-user/route.ts @@ -1,2 +1,7 @@ +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; + export type { CheckUserResponseData } from '~server/api-runtime/auth'; -export { checkUserAPIHandler as POST } from '~server/api-runtime/auth'; diff --git a/src/app/(backend)/api/auth/resolve-username/route.ts b/src/app/(backend)/api/auth/resolve-username/route.ts index d26c3dff0a..09faf86dfe 100644 --- a/src/app/(backend)/api/auth/resolve-username/route.ts +++ b/src/app/(backend)/api/auth/resolve-username/route.ts @@ -1,2 +1,7 @@ +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; + export type { ResolveUsernameResponseData } from '~server/api-runtime/auth'; -export { resolveUsernameAPIHandler as POST } from '~server/api-runtime/auth'; diff --git a/src/app/(backend)/api/dev/agent-tracing/route.ts b/src/app/(backend)/api/dev/agent-tracing/route.ts index 263928925e..820bb1b73a 100644 --- a/src/app/(backend)/api/dev/agent-tracing/route.ts +++ b/src/app/(backend)/api/dev/agent-tracing/route.ts @@ -1 +1,5 @@ -export { agentTracingAPIHandler as GET } from '~server/api-runtime/dev'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts b/src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts index efd6c5de17..e1e8861da8 100644 --- a/src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts +++ b/src/app/(backend)/api/dev/memory-user-memory/benchmark-locomo/route.ts @@ -1 +1,5 @@ -export { memoryUserMemoryBenchmarkLoCoMoDevAPIHandler as POST } from '~server/api-runtime/memoryBenchmarkDev'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/dev/test-push/route.ts b/src/app/(backend)/api/dev/test-push/route.ts index e8818d4193..e1e8861da8 100644 --- a/src/app/(backend)/api/dev/test-push/route.ts +++ b/src/app/(backend)/api/dev/test-push/route.ts @@ -1 +1,5 @@ -export { testPushAPIHandler as POST } from '~server/api-runtime/dev'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/v1/[[...route]]/route.ts b/src/app/(backend)/api/v1/[[...route]]/route.ts index b22f9d03ca..02e8d4183c 100644 --- a/src/app/(backend)/api/v1/[[...route]]/route.ts +++ b/src/app/(backend)/api/v1/[[...route]]/route.ts @@ -1,6 +1,6 @@ -import { openAPIHandler } from '~server/api-runtime/openapi'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -const handler = (req: Request) => openAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/api/version/route.ts b/src/app/(backend)/api/version/route.ts index 660aef7020..b0bb21d530 100644 --- a/src/app/(backend)/api/version/route.ts +++ b/src/app/(backend)/api/version/route.ts @@ -1,2 +1,7 @@ +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; + export type { VersionResponseData } from '~server/api-runtime/version'; -export { versionAPIHandler as GET } from '~server/api-runtime/version'; diff --git a/src/app/(backend)/api/webhooks/casdoor/route.ts b/src/app/(backend)/api/webhooks/casdoor/route.ts index 48ed9104ac..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/casdoor/route.ts +++ b/src/app/(backend)/api/webhooks/casdoor/route.ts @@ -1 +1,5 @@ -export { casdoorWebhookAPIHandler as POST } from '~server/api-runtime/webhooks'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/webhooks/logto/route.ts b/src/app/(backend)/api/webhooks/logto/route.ts index c9d3866ea6..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/logto/route.ts +++ b/src/app/(backend)/api/webhooks/logto/route.ts @@ -1 +1,5 @@ -export { logtoWebhookAPIHandler as POST } from '~server/api-runtime/webhooks'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/route.ts b/src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/route.ts index ed6846a9da..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/route.ts +++ b/src/app/(backend)/api/webhooks/memory-extraction/benchmark-locomo/route.ts @@ -1 +1,5 @@ -export { memoryExtractionBenchmarkLoCoMoWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtractionBenchmark'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/webhooks/memory-extraction/route.ts b/src/app/(backend)/api/webhooks/memory-extraction/route.ts index 1eca8889c3..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/memory-extraction/route.ts +++ b/src/app/(backend)/api/webhooks/memory-extraction/route.ts @@ -1 +1,5 @@ -export { memoryExtractionWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtraction'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/webhooks/memory-user-memory/persona/update-writing/route.ts b/src/app/(backend)/api/webhooks/memory-user-memory/persona/update-writing/route.ts index 0d17fd0403..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/memory-user-memory/persona/update-writing/route.ts +++ b/src/app/(backend)/api/webhooks/memory-user-memory/persona/update-writing/route.ts @@ -1 +1,5 @@ -export { memoryUserPersonaUpdateWritingWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtraction'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/webhooks/memory-user-memory/pipelines/extract/chat-topic/cancel/route.ts b/src/app/(backend)/api/webhooks/memory-user-memory/pipelines/extract/chat-topic/cancel/route.ts index b5365532a6..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/memory-user-memory/pipelines/extract/chat-topic/cancel/route.ts +++ b/src/app/(backend)/api/webhooks/memory-user-memory/pipelines/extract/chat-topic/cancel/route.ts @@ -1 +1,5 @@ -export { memoryExtractChatTopicCancelWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtraction'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/webhooks/video/[provider]/route.ts b/src/app/(backend)/api/webhooks/video/[provider]/route.ts index 2bb13bbf85..e1e8861da8 100644 --- a/src/app/(backend)/api/webhooks/video/[provider]/route.ts +++ b/src/app/(backend)/api/webhooks/video/[provider]/route.ts @@ -1,4 +1,5 @@ -import { videoWebhookAPIHandler } from '~server/api-runtime/videoWebhook'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = async (req: Request, ctx: { params: Promise<{ provider: string }> }) => - videoWebhookAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/[[...route]]/route.ts b/src/app/(backend)/api/workflows/[[...route]]/route.ts index 261302c2fa..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/[[...route]]/route.ts +++ b/src/app/(backend)/api/workflows/[[...route]]/route.ts @@ -1,3 +1,5 @@ -import app from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (request: Request) => app.fetch(request); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/execute-test-case/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/execute-test-case/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/execute-test-case/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/execute-test-case/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/finalize-run/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/finalize-run/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/finalize-run/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/finalize-run/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts index fda12f33a7..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/on-thread-complete/route.ts @@ -1 +1,5 @@ -export { agentEvalRunOnThreadCompleteAPIHandler as POST } from '~server/api-runtime/agentEvalRunWorkflow'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts index 8d86fd303f..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/on-trajectory-complete/route.ts @@ -1 +1,5 @@ -export { agentEvalRunOnTrajectoryCompleteAPIHandler as POST } from '~server/api-runtime/agentEvalRunWorkflow'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/paginate-test-cases/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/paginate-test-cases/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/paginate-test-cases/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/paginate-test-cases/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/resume-agent-trajectory/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/resume-agent-trajectory/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/resume-agent-trajectory/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/resume-agent-trajectory/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/resume-thread-trajectory/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/resume-thread-trajectory/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/resume-thread-trajectory/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/resume-thread-trajectory/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/run-agent-trajectory/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/run-agent-trajectory/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/run-agent-trajectory/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/run-agent-trajectory/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/run-benchmark/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/run-benchmark/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/run-benchmark/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/run-benchmark/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/api/workflows/agent-eval-run/run-thread-trajectory/route.ts b/src/app/(backend)/api/workflows/agent-eval-run/run-thread-trajectory/route.ts index e3b329066b..e1e8861da8 100644 --- a/src/app/(backend)/api/workflows/agent-eval-run/run-thread-trajectory/route.ts +++ b/src/app/(backend)/api/workflows/agent-eval-run/run-thread-trajectory/route.ts @@ -1,3 +1,5 @@ -import workflowsApp from '~server/workflows-hono'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => workflowsApp.fetch(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/f/[id]/route.ts b/src/app/(backend)/f/[id]/route.ts index 5edef19754..820bb1b73a 100644 --- a/src/app/(backend)/f/[id]/route.ts +++ b/src/app/(backend)/f/[id]/route.ts @@ -1,4 +1,5 @@ -import { fileProxyAPIHandler } from '~server/api-runtime/fileProxy'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const GET = async (req: Request, ctx: { params: Promise<{ id: string }> }) => - fileProxyAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/market/agent/[[...segments]]/route.ts b/src/app/(backend)/market/agent/[[...segments]]/route.ts index 48c7e1fe5e..199901179d 100644 --- a/src/app/(backend)/market/agent/[[...segments]]/route.ts +++ b/src/app/(backend)/market/agent/[[...segments]]/route.ts @@ -1,9 +1,6 @@ -import { marketAgentAPIHandler } from '~server/api-runtime/market'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -type RouteContext = { params: Promise<{ segments?: string[] }> }; - -const handler = async (req: Request, ctx: RouteContext) => - marketAgentAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/market/oidc/[[...segments]]/route.ts b/src/app/(backend)/market/oidc/[[...segments]]/route.ts index 7203dba65e..199901179d 100644 --- a/src/app/(backend)/market/oidc/[[...segments]]/route.ts +++ b/src/app/(backend)/market/oidc/[[...segments]]/route.ts @@ -1,9 +1,6 @@ -import { marketOIDCAPIHandler } from '~server/api-runtime/market'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -type RouteContext = { params: Promise<{ segments?: string[] }> }; - -const handler = async (req: Request, ctx: RouteContext) => - marketOIDCAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/market/social/[[...segments]]/route.ts b/src/app/(backend)/market/social/[[...segments]]/route.ts index d44720ab9a..199901179d 100644 --- a/src/app/(backend)/market/social/[[...segments]]/route.ts +++ b/src/app/(backend)/market/social/[[...segments]]/route.ts @@ -1,9 +1,6 @@ -import { marketSocialAPIHandler } from '~server/api-runtime/market'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -type RouteContext = { params: Promise<{ segments?: string[] }> }; - -const handler = async (req: Request, ctx: RouteContext) => - marketSocialAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/market/user/[username]/route.ts b/src/app/(backend)/market/user/[username]/route.ts index 60411afd4c..92803e98bd 100644 --- a/src/app/(backend)/market/user/[username]/route.ts +++ b/src/app/(backend)/market/user/[username]/route.ts @@ -1,6 +1,7 @@ -import { marketUserProfileAPIHandler } from '~server/api-runtime/market'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const GET = async (req: Request, ctx: { params: Promise<{ username: string }> }) => - marketUserProfileAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; export const dynamic = 'force-dynamic'; diff --git a/src/app/(backend)/market/user/me/route.ts b/src/app/(backend)/market/user/me/route.ts index be89f87a55..0b3d905f12 100644 --- a/src/app/(backend)/market/user/me/route.ts +++ b/src/app/(backend)/market/user/me/route.ts @@ -1,5 +1,7 @@ -import { marketUserMeAPIHandler } from '~server/api-runtime/market'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const PUT = (req: Request) => marketUserMeAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const PUT = handler; export const dynamic = 'force-dynamic'; diff --git a/src/app/(backend)/oauth/connector/callback/route.ts b/src/app/(backend)/oauth/connector/callback/route.ts index 194b8360b8..820bb1b73a 100644 --- a/src/app/(backend)/oauth/connector/callback/route.ts +++ b/src/app/(backend)/oauth/connector/callback/route.ts @@ -1 +1,5 @@ -export { connectorOAuthCallbackAPIHandler as GET } from '~server/api-runtime/connectorOAuthCallback'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/oidc/[...oidc]/route.ts b/src/app/(backend)/oidc/[...oidc]/route.ts index 61e4ce285c..3a5227d6c2 100644 --- a/src/app/(backend)/oidc/[...oidc]/route.ts +++ b/src/app/(backend)/oidc/[...oidc]/route.ts @@ -1,6 +1,6 @@ -import { oidcProviderAPIHandler } from '~server/api-runtime/oidc'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -const handler = (req: Request) => oidcProviderAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); export const GET = handler; export const POST = handler; diff --git a/src/app/(backend)/oidc/callback/desktop/route.ts b/src/app/(backend)/oidc/callback/desktop/route.ts index c4e7773aa3..820bb1b73a 100644 --- a/src/app/(backend)/oidc/callback/desktop/route.ts +++ b/src/app/(backend)/oidc/callback/desktop/route.ts @@ -1,3 +1,5 @@ -import { oidcCallbackDesktopAPIHandler } from '~server/api-runtime/oidc'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const GET = (req: Request) => oidcCallbackDesktopAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/oidc/clear-session/route.ts b/src/app/(backend)/oidc/clear-session/route.ts index 66a0f05385..e1e8861da8 100644 --- a/src/app/(backend)/oidc/clear-session/route.ts +++ b/src/app/(backend)/oidc/clear-session/route.ts @@ -1 +1,5 @@ -export { oidcClearSessionAPIHandler as POST } from '~server/api-runtime/oidc'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/oidc/consent/route.ts b/src/app/(backend)/oidc/consent/route.ts index 83e3082654..e1e8861da8 100644 --- a/src/app/(backend)/oidc/consent/route.ts +++ b/src/app/(backend)/oidc/consent/route.ts @@ -1 +1,5 @@ -export { oidcConsentAPIHandler as POST } from '~server/api-runtime/oidc'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/oidc/handoff/route.ts b/src/app/(backend)/oidc/handoff/route.ts index e816f8825b..820bb1b73a 100644 --- a/src/app/(backend)/oidc/handoff/route.ts +++ b/src/app/(backend)/oidc/handoff/route.ts @@ -1 +1,5 @@ -export { oidcHandoffAPIHandler as GET } from '~server/api-runtime/oidc'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/trpc/async/[trpc]/route.ts b/src/app/(backend)/trpc/async/[trpc]/route.ts index 08e77071bc..84768f130a 100644 --- a/src/app/(backend)/trpc/async/[trpc]/route.ts +++ b/src/app/(backend)/trpc/async/[trpc]/route.ts @@ -1 +1,6 @@ -export { asyncTRPCHandler as GET, asyncTRPCHandler as POST } from '~server/trpc-runtime/async'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; +export const POST = handler; diff --git a/src/app/(backend)/trpc/lambda/[trpc]/route.ts b/src/app/(backend)/trpc/lambda/[trpc]/route.ts index 78cdb82018..84768f130a 100644 --- a/src/app/(backend)/trpc/lambda/[trpc]/route.ts +++ b/src/app/(backend)/trpc/lambda/[trpc]/route.ts @@ -1 +1,6 @@ -export { lambdaTRPCHandler as GET, lambdaTRPCHandler as POST } from '~server/trpc-runtime/lambda'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; +export const POST = handler; diff --git a/src/app/(backend)/trpc/mobile/[trpc]/route.ts b/src/app/(backend)/trpc/mobile/[trpc]/route.ts index 27c306c36f..84768f130a 100644 --- a/src/app/(backend)/trpc/mobile/[trpc]/route.ts +++ b/src/app/(backend)/trpc/mobile/[trpc]/route.ts @@ -1 +1,6 @@ -export { mobileTRPCHandler as GET, mobileTRPCHandler as POST } from '~server/trpc-runtime/mobile'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; +export const POST = handler; diff --git a/src/app/(backend)/trpc/tools/[trpc]/route.ts b/src/app/(backend)/trpc/tools/[trpc]/route.ts index b7aa60b723..84768f130a 100644 --- a/src/app/(backend)/trpc/tools/[trpc]/route.ts +++ b/src/app/(backend)/trpc/tools/[trpc]/route.ts @@ -1 +1,6 @@ -export { toolsTRPCHandler as GET, toolsTRPCHandler as POST } from '~server/trpc-runtime/tools'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; +export const POST = handler; diff --git a/src/app/(backend)/webapi/chat/[provider]/route.ts b/src/app/(backend)/webapi/chat/[provider]/route.ts index db3ff76d64..c683fff3e5 100644 --- a/src/app/(backend)/webapi/chat/[provider]/route.ts +++ b/src/app/(backend)/webapi/chat/[provider]/route.ts @@ -1,8 +1,7 @@ -import { chatAPIHandler } from '~server/api-runtime/chat'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; -// If user don't use fluid compute, will build failed -// this enforce user to enable fluid compute export const maxDuration = 300; - -export const POST = async (req: Request, ctx: { params: Promise<{ provider: string }> }) => - chatAPIHandler(req, await ctx.params); diff --git a/src/app/(backend)/webapi/create-image/comfyui/route.ts b/src/app/(backend)/webapi/create-image/comfyui/route.ts index 3869588532..c683fff3e5 100644 --- a/src/app/(backend)/webapi/create-image/comfyui/route.ts +++ b/src/app/(backend)/webapi/create-image/comfyui/route.ts @@ -1,5 +1,7 @@ -import { comfyUICreateImageAPIHandler } from '~server/api-runtime/createImage'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; export const maxDuration = 300; - -export const POST = (req: Request) => comfyUICreateImageAPIHandler(req); diff --git a/src/app/(backend)/webapi/models/[provider]/pull/route.ts b/src/app/(backend)/webapi/models/[provider]/pull/route.ts index 71b155675f..e1e8861da8 100644 --- a/src/app/(backend)/webapi/models/[provider]/pull/route.ts +++ b/src/app/(backend)/webapi/models/[provider]/pull/route.ts @@ -1,4 +1,5 @@ -import { pullModelsAPIHandler } from '~server/api-runtime/models'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = async (req: Request, ctx: { params: Promise<{ provider: string }> }) => - pullModelsAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/webapi/models/[provider]/route.ts b/src/app/(backend)/webapi/models/[provider]/route.ts index 442157ffe0..820bb1b73a 100644 --- a/src/app/(backend)/webapi/models/[provider]/route.ts +++ b/src/app/(backend)/webapi/models/[provider]/route.ts @@ -1,4 +1,5 @@ -import { modelsAPIHandler } from '~server/api-runtime/models'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const GET = async (req: Request, ctx: { params: Promise<{ provider: string }> }) => - modelsAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/app/(backend)/webapi/stt/openai/route.ts b/src/app/(backend)/webapi/stt/openai/route.ts index 10582fb7d2..e1e8861da8 100644 --- a/src/app/(backend)/webapi/stt/openai/route.ts +++ b/src/app/(backend)/webapi/stt/openai/route.ts @@ -1 +1,5 @@ -export { openAISTTAPIHandler as POST } from '~server/api-runtime/speech'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/webapi/trace/route.ts b/src/app/(backend)/webapi/trace/route.ts index 47b9b5ed2f..e1e8861da8 100644 --- a/src/app/(backend)/webapi/trace/route.ts +++ b/src/app/(backend)/webapi/trace/route.ts @@ -1,3 +1,5 @@ -import { traceAPIHandler } from '~server/api-runtime/trace'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const POST = (req: Request) => traceAPIHandler(req); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/webapi/tts/edge/route.ts b/src/app/(backend)/webapi/tts/edge/route.ts index a6bd5329df..e1e8861da8 100644 --- a/src/app/(backend)/webapi/tts/edge/route.ts +++ b/src/app/(backend)/webapi/tts/edge/route.ts @@ -1 +1,5 @@ -export { edgeTTSAPIHandler as POST } from '~server/api-runtime/speech'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/webapi/tts/microsoft/route.ts b/src/app/(backend)/webapi/tts/microsoft/route.ts index 4af94bd16d..e1e8861da8 100644 --- a/src/app/(backend)/webapi/tts/microsoft/route.ts +++ b/src/app/(backend)/webapi/tts/microsoft/route.ts @@ -1 +1,5 @@ -export { microsoftTTSAPIHandler as POST } from '~server/api-runtime/speech'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/webapi/tts/openai/route.ts b/src/app/(backend)/webapi/tts/openai/route.ts index ee2ace3818..e1e8861da8 100644 --- a/src/app/(backend)/webapi/tts/openai/route.ts +++ b/src/app/(backend)/webapi/tts/openai/route.ts @@ -1 +1,5 @@ -export { openAITTSAPIHandler as POST } from '~server/api-runtime/speech'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; + +const handler = (req: Request) => fetchHonoRuntime(req); + +export const POST = handler; diff --git a/src/app/(backend)/webapi/user/avatar/[id]/[image]/route.ts b/src/app/(backend)/webapi/user/avatar/[id]/[image]/route.ts index cced1cfe98..820bb1b73a 100644 --- a/src/app/(backend)/webapi/user/avatar/[id]/[image]/route.ts +++ b/src/app/(backend)/webapi/user/avatar/[id]/[image]/route.ts @@ -1,4 +1,5 @@ -import { userAvatarAPIHandler } from '~server/api-runtime/userAvatar'; +import { fetchHonoRuntime } from '@/server/hono-runtime/client'; -export const GET = async (req: Request, ctx: { params: Promise<{ id: string; image: string }> }) => - userAvatarAPIHandler(req, await ctx.params); +const handler = (req: Request) => fetchHonoRuntime(req); + +export const GET = handler; diff --git a/src/server/hono-runtime/client.ts b/src/server/hono-runtime/client.ts index 67c883cfd7..ed46868954 100644 --- a/src/server/hono-runtime/client.ts +++ b/src/server/hono-runtime/client.ts @@ -63,7 +63,20 @@ const loadProductionHonoApp = () => { const entry = process.env.LOBE_HONO_DIST_ENTRY || path.join(process.cwd(), 'apps/server/dist/index.js'); - const module = loadExternalModule(entry) as HonoDistModule | HonoFetchApp; + + let module: HonoDistModule | HonoFetchApp; + try { + module = loadExternalModule(entry) as HonoDistModule | HonoFetchApp; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { + throw new Error( + `Hono dist entry not found at ${entry}. Build it with \`pnpm --filter @lobechat/server build\`, ` + + 'or in dev run `bun run dev` / set LOBE_DEV_HONO_TARGET to a running Hono server.', + { cause: error }, + ); + } + throw error; + } const app = isHonoFetchApp(module) ? module : isHonoFetchApp(module.default)