mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
refactor: extract shared HTTP routes into @openchatlab/http-routes package
Migrate 30+ endpoints from apps/cli/src/http/ to packages/http-routes/ with a platform-agnostic HttpRouteContext DI interface, enabling future reuse by Electron Internal Server. Batch 1: auth, errors, context, sessions, members, preferences Batch 2: analytics, sql, session-index, export, nlp Batch 3: REST system, REST sessions Includes 7 smoke tests via Fastify inject covering /api/v1/* and /_web/* route groups. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
cd3ae8c5ba
commit
a8f0d5cb88
26 files changed
+347
-128
No files matched your search
@@ -66,6 +66,7 @@
|
||||
"devDependencies": {
|
||||
"@openchatlab/config": "workspace:*",
|
||||
"@openchatlab/core": "workspace:*",
|
||||
"@openchatlab/http-routes": "workspace:*",
|
||||
"@openchatlab/node-runtime": "workspace:*",
|
||||
"@openchatlab/parser": "workspace:*",
|
||||
"@openchatlab/shared-types": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { setAuthToken, setRequireAuth, authHook } from './auth'
|
||||
import { setAuthToken, setRequireAuth, authHook } from '@openchatlab/http-routes'
|
||||
|
||||
function fakeRequest(url: string, authorization?: string) {
|
||||
return { url, headers: { authorization } } as never
|
||||
|
||||
@@ -18,13 +18,9 @@ import {
|
||||
verifyCliDataPath,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import { createServer } from './server'
|
||||
import { setAuthToken, setRequireAuth } from './auth'
|
||||
import { registerSystemRoutes } from './routes/system'
|
||||
import { registerSessionRoutes } from './routes/sessions'
|
||||
import { setAuthToken, setRequireAuth } from '@openchatlab/http-routes'
|
||||
import { registerWebRoutes } from './routes/web'
|
||||
import { registerNlpRoutes } from './routes/nlp'
|
||||
import { registerAiRoutes } from './routes/ai'
|
||||
import { registerPreferencesRoutes } from './routes/preferences'
|
||||
import { registerProxyRoutes } from './routes/proxy'
|
||||
import { initServerAiLogger, closeServerAiLogger } from '../ai/logger'
|
||||
import { initSync, cleanupSync } from '../sync'
|
||||
@@ -133,12 +129,8 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
limits: { fileSize: 1024 * 1024 * 1024 }, // 1GB
|
||||
})
|
||||
|
||||
registerSystemRoutes(server, dbManager)
|
||||
registerSessionRoutes(server, dbManager)
|
||||
registerNlpRoutes(server, dbManager)
|
||||
registerAiRoutes(server, dbManager, convManager)
|
||||
registerWebRoutes(server, dbManager, { pathProvider, nativeBinding })
|
||||
registerPreferencesRoutes(server, pathProvider)
|
||||
|
||||
initSync(server, dbManager, pathProvider, { port, host, token })
|
||||
|
||||
@@ -189,8 +181,5 @@ export async function stopHttpServer(): Promise<void> {
|
||||
}
|
||||
|
||||
export { createServer } from './server'
|
||||
export { registerSystemRoutes } from './routes/system'
|
||||
export { registerSessionRoutes } from './routes/sessions'
|
||||
export { registerWebRoutes } from './routes/web'
|
||||
export { registerNlpRoutes } from './routes/nlp'
|
||||
export { registerAiRoutes } from './routes/ai'
|
||||
@@ -23,16 +23,11 @@ import type { FastifyInstance } from 'fastify'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import { createDatabaseManagerAdapter } from '@openchatlab/node-runtime'
|
||||
import { registerSharedRoutes } from '@openchatlab/http-routes'
|
||||
import { MergeSessionCache } from '../../../merger/merge-cache'
|
||||
import { registerSessionRoutes } from './sessions'
|
||||
import { registerMemberRoutes } from './members'
|
||||
import { registerAnalyticsRoutes } from './analytics'
|
||||
import { registerSqlRoutes } from './sql'
|
||||
import { registerSessionIndexRoutes } from './session-index'
|
||||
import { registerSummaryRoutes } from './summaries'
|
||||
import { registerImportRoutes } from './import'
|
||||
import { registerMergeRoutes } from './merge'
|
||||
import { registerExportRoutes } from './export'
|
||||
import { registerCacheRoutes } from './cache'
|
||||
import { getVersion } from '../../../version'
|
||||
|
||||
@@ -82,17 +77,20 @@ export function registerWebRoutes(
|
||||
}
|
||||
const resolvedPathProvider = options?.pathProvider ?? fallbackPathProvider
|
||||
|
||||
registerSessionRoutes(server, adapter)
|
||||
registerMemberRoutes(server, adapter)
|
||||
registerAnalyticsRoutes(server, dbManager, adapter)
|
||||
registerSqlRoutes(server, adapter)
|
||||
registerSessionIndexRoutes(server, adapter)
|
||||
registerSharedRoutes(server, {
|
||||
dbManager,
|
||||
sessionAdapter: adapter,
|
||||
pathProvider: resolvedPathProvider,
|
||||
getVersion,
|
||||
nativeBinding: options?.nativeBinding,
|
||||
})
|
||||
|
||||
// CLI-specific routes not yet migrated to @openchatlab/http-routes
|
||||
registerSummaryRoutes(server, dbManager, adapter)
|
||||
registerImportRoutes(server, dbManager)
|
||||
if (mergeCache) {
|
||||
registerMergeRoutes(server, dbManager, mergeCache)
|
||||
}
|
||||
registerExportRoutes(server, adapter)
|
||||
registerCacheRoutes(server, resolvedPathProvider)
|
||||
|
||||
server.get('/_web/system/check-update', async () => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import Fastify from 'fastify'
|
||||
import type { SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import { registerSessionRoutes } from './sessions'
|
||||
import type { HttpRouteContext } from '@openchatlab/http-routes'
|
||||
import { registerSessionRoutes } from '@openchatlab/http-routes'
|
||||
|
||||
function createMissingSessionAdapter(): SessionRuntimeAdapter {
|
||||
return {
|
||||
@@ -21,10 +22,20 @@ function createMissingSessionAdapter(): SessionRuntimeAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
describe('CLI Web session routes', () => {
|
||||
function createMockContext(adapter: SessionRuntimeAdapter): HttpRouteContext {
|
||||
return {
|
||||
dbManager: {} as any,
|
||||
sessionAdapter: adapter,
|
||||
pathProvider: {} as any,
|
||||
getVersion: () => '0.0.0-test',
|
||||
}
|
||||
}
|
||||
|
||||
describe('shared session routes', () => {
|
||||
it('returns 404 when requesting a missing session by id', async () => {
|
||||
const app = Fastify()
|
||||
registerSessionRoutes(app, createMissingSessionAdapter())
|
||||
const ctx = createMockContext(createMissingSessionAdapter())
|
||||
registerSessionRoutes(app, ctx)
|
||||
|
||||
const response = await app.inject({ method: 'GET', url: '/_web/sessions/missing' })
|
||||
await app.close()
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import Fastify, { type FastifyInstance, type FastifyError } from 'fastify'
|
||||
import { authHook } from './auth'
|
||||
import { ApiError, ApiErrorCode, errorResponse, serverError } from './errors'
|
||||
import { authHook, ApiError, ApiErrorCode, errorResponse, serverError } from '@openchatlab/http-routes'
|
||||
|
||||
const JSON_BODY_LIMIT = 50 * 1024 * 1024 // 50MB
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"../../packages/parser/**/*",
|
||||
"../../packages/tools/**/*",
|
||||
"../../packages/sync/**/*",
|
||||
"../../packages/mcp-server/**/*"
|
||||
"../../packages/mcp-server/**/*",
|
||||
"../../packages/http-routes/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@openchatlab/http-routes",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "ChatLab shared HTTP API routes — consumed by CLI Server and Electron Internal Server",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@openchatlab/config": "workspace:*",
|
||||
"@openchatlab/core": "workspace:*",
|
||||
"@openchatlab/node-runtime": "workspace:*",
|
||||
"@openchatlab/shared-types": "workspace:*",
|
||||
"fastify": "^5.8.4"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* ChatLab HTTP API — Bearer Token authentication hook
|
||||
*
|
||||
* 从 electron/main/api/auth.ts 迁移,使用 @openchatlab/config 读取 token。
|
||||
* Shared auth middleware for CLI Server and Electron Internal Server.
|
||||
* URL classification: /api/* always requires token, /_web/* conditionally,
|
||||
* static files and SPA fallback are public.
|
||||
*/
|
||||
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify'
|
||||
@@ -11,9 +13,6 @@ import { unauthorized, errorResponse } from './errors'
|
||||
let cachedToken: string | null = null
|
||||
let requireAuthEnabled = false
|
||||
|
||||
/**
|
||||
* 设置 auth hook 使用的 token(由 server 启动时注入)
|
||||
*/
|
||||
export function setAuthToken(token: string): void {
|
||||
cachedToken = token
|
||||
}
|
||||
@@ -40,12 +39,10 @@ export async function authHook(request: FastifyRequest, reply: FastifyReply): Pr
|
||||
|
||||
const url = request.url
|
||||
|
||||
// /api/* always requires Bearer token
|
||||
if (url.startsWith('/api/')) {
|
||||
return requireBearerToken(request, reply)
|
||||
}
|
||||
|
||||
// /_web/* requires Bearer token when requireAuthEnabled
|
||||
if (url.startsWith('/_web/')) {
|
||||
if (requireAuthEnabled) return requireBearerToken(request, reply)
|
||||
return
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* HttpRouteContext — shared dependency injection interface for route handlers.
|
||||
*
|
||||
* CLI Server and Electron Internal Server each construct their own context
|
||||
* and pass it to registerSharedRoutes(). Route handlers only depend on this
|
||||
* interface, never on CLI or Electron specific modules.
|
||||
*/
|
||||
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import type { DatabaseManager, SessionRuntimeAdapter, PreferencesManager } from '@openchatlab/node-runtime'
|
||||
|
||||
export interface HttpRouteContext {
|
||||
dbManager: DatabaseManager
|
||||
sessionAdapter: SessionRuntimeAdapter
|
||||
pathProvider: PathProvider
|
||||
|
||||
getVersion: () => string
|
||||
|
||||
/** Reserved for future migrated routes (import/merge) that need the native binding path */
|
||||
nativeBinding?: string
|
||||
|
||||
preferencesManager?: PreferencesManager
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* ChatLab HTTP API — Error codes and factory functions
|
||||
*
|
||||
* 从 electron/main/api/errors.ts 迁移,完全平台无关。
|
||||
* Platform-agnostic error handling shared by CLI Server and Electron Internal Server.
|
||||
*/
|
||||
|
||||
export enum ApiErrorCode {
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Shared helpers for route modules.
|
||||
*/
|
||||
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
|
||||
export function parseTimeFilter(query: Record<string, string | undefined>): TimeFilter | undefined {
|
||||
const { startTs, endTs, memberId } = query
|
||||
if (!startTs && !endTs && !memberId) return undefined
|
||||
const filter: TimeFilter = {}
|
||||
if (startTs) filter.startTs = parseInt(startTs, 10)
|
||||
if (endTs) filter.endTs = parseInt(endTs, 10)
|
||||
if (memberId) filter.memberId = parseInt(memberId, 10)
|
||||
return filter
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export type { HttpRouteContext } from './context'
|
||||
export { registerSharedRoutes } from './register'
|
||||
export { setAuthToken, setRequireAuth, authHook } from './auth'
|
||||
export {
|
||||
ApiError,
|
||||
ApiErrorCode,
|
||||
unauthorized,
|
||||
sessionNotFound,
|
||||
invalidPayload,
|
||||
sqlReadonlyViolation,
|
||||
sqlExecutionError,
|
||||
exportTooLarge,
|
||||
serverError,
|
||||
successResponse,
|
||||
errorResponse,
|
||||
} from './errors'
|
||||
export { parseTimeFilter } from './helpers'
|
||||
|
||||
// Individual route registration for granular testing or selective registration
|
||||
export { registerSystemRoutes } from './routes/system'
|
||||
export { registerRestSessionRoutes } from './routes/sessions'
|
||||
export { registerSessionRoutes } from './routes/web/sessions'
|
||||
export { registerMemberRoutes } from './routes/web/members'
|
||||
export { registerPreferencesRoutes } from './routes/web/preferences'
|
||||
export { registerAnalyticsRoutes } from './routes/web/analytics'
|
||||
export { registerSqlRoutes } from './routes/web/sql'
|
||||
export { registerSessionIndexRoutes } from './routes/web/session-index'
|
||||
export { registerExportRoutes } from './routes/web/export'
|
||||
export { registerNlpRoutes } from './routes/web/nlp'
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Aggregate route registration — one call to register all shared routes.
|
||||
*
|
||||
* CLI Server and Electron Internal Server call this instead of
|
||||
* importing individual route modules.
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { HttpRouteContext } from './context'
|
||||
import { registerSystemRoutes } from './routes/system'
|
||||
import { registerRestSessionRoutes } from './routes/sessions'
|
||||
import { registerSessionRoutes } from './routes/web/sessions'
|
||||
import { registerMemberRoutes } from './routes/web/members'
|
||||
import { registerPreferencesRoutes } from './routes/web/preferences'
|
||||
import { registerAnalyticsRoutes } from './routes/web/analytics'
|
||||
import { registerSqlRoutes } from './routes/web/sql'
|
||||
import { registerSessionIndexRoutes } from './routes/web/session-index'
|
||||
import { registerExportRoutes } from './routes/web/export'
|
||||
import { registerNlpRoutes } from './routes/web/nlp'
|
||||
|
||||
export function registerSharedRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
// REST API (/api/v1/*)
|
||||
registerSystemRoutes(server, ctx)
|
||||
registerRestSessionRoutes(server, ctx)
|
||||
|
||||
// Web UI API (/_web/*)
|
||||
registerSessionRoutes(server, ctx)
|
||||
registerMemberRoutes(server, ctx)
|
||||
registerPreferencesRoutes(server, ctx)
|
||||
registerAnalyticsRoutes(server, ctx)
|
||||
registerSqlRoutes(server, ctx)
|
||||
registerSessionIndexRoutes(server, ctx)
|
||||
registerExportRoutes(server, ctx)
|
||||
registerNlpRoutes(server, ctx)
|
||||
}
|
||||
+15
-22
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* ChatLab HTTP API — Session routes
|
||||
* ChatLab HTTP API — REST Session routes (/api/v1/sessions/*)
|
||||
*
|
||||
* 从 electron/main/api/routes/sessions.ts 迁移。
|
||||
* 使用 DatabaseManager + @openchatlab/core 替代 workerManager。
|
||||
* Public REST API for external tools, scripts, and integrations.
|
||||
* Uses DatabaseManager + @openchatlab/core for data access.
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../context'
|
||||
import {
|
||||
getSessionInfo,
|
||||
getSessionMeta,
|
||||
@@ -22,19 +22,18 @@ import { successResponse, errorResponse, sessionNotFound, exportTooLarge, sqlExe
|
||||
|
||||
const EXPORT_MESSAGE_LIMIT = 100_000
|
||||
|
||||
function ensureDb(dbManager: DatabaseManager, sessionId: string) {
|
||||
const db = dbManager.open(sessionId)
|
||||
function ensureDb(ctx: HttpRouteContext, sessionId: string) {
|
||||
const db = ctx.dbManager.open(sessionId)
|
||||
if (!db) throw sessionNotFound(sessionId)
|
||||
return db
|
||||
}
|
||||
|
||||
export function registerSessionRoutes(server: FastifyInstance, dbManager: DatabaseManager): void {
|
||||
// GET /api/v1/sessions — List all sessions
|
||||
export function registerRestSessionRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
server.get('/api/v1/sessions', async () => {
|
||||
const sessionIds = dbManager.listSessionIds()
|
||||
const sessionIds = ctx.dbManager.listSessionIds()
|
||||
const sessions = sessionIds
|
||||
.map((id) => {
|
||||
const db = dbManager.open(id)
|
||||
const db = ctx.dbManager.open(id)
|
||||
if (!db) return null
|
||||
const info = getSessionInfo(db)
|
||||
if (!info) return null
|
||||
@@ -54,9 +53,8 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
return successResponse(sessions)
|
||||
})
|
||||
|
||||
// GET /api/v1/sessions/:id — Single session detail
|
||||
server.get<{ Params: { id: string } }>('/api/v1/sessions/:id', async (request) => {
|
||||
const db = ensureDb(dbManager, request.params.id)
|
||||
const db = ensureDb(ctx, request.params.id)
|
||||
const info = getSessionInfo(db)
|
||||
if (!info) throw sessionNotFound(request.params.id)
|
||||
return successResponse({
|
||||
@@ -72,7 +70,6 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/v1/sessions/:id/messages — Query messages (paginated)
|
||||
server.get<{
|
||||
Params: { id: string }
|
||||
Querystring: {
|
||||
@@ -85,7 +82,7 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
}
|
||||
}>('/api/v1/sessions/:id/messages', async (request) => {
|
||||
const { id } = request.params
|
||||
const db = ensureDb(dbManager, id)
|
||||
const db = ensureDb(ctx, id)
|
||||
|
||||
const page = Math.max(1, parseInt(request.query.page || '1', 10) || 1)
|
||||
const limit = Math.min(1000, Math.max(1, parseInt(request.query.limit || '100', 10) || 100))
|
||||
@@ -111,17 +108,15 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/v1/sessions/:id/members — Member list
|
||||
server.get<{ Params: { id: string } }>('/api/v1/sessions/:id/members', async (request) => {
|
||||
const db = ensureDb(dbManager, request.params.id)
|
||||
const db = ensureDb(ctx, request.params.id)
|
||||
const members = getMembers(db)
|
||||
return successResponse(members)
|
||||
})
|
||||
|
||||
// GET /api/v1/sessions/:id/stats/overview — Overview statistics
|
||||
server.get<{ Params: { id: string } }>('/api/v1/sessions/:id/stats/overview', async (request) => {
|
||||
const { id } = request.params
|
||||
const db = ensureDb(dbManager, id)
|
||||
const db = ensureDb(ctx, id)
|
||||
|
||||
const overview = getSessionOverview(db)
|
||||
const memberActivity = getMemberActivity(db)
|
||||
@@ -151,10 +146,9 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
})
|
||||
})
|
||||
|
||||
// POST /api/v1/sessions/:id/sql — Execute SQL (read-only)
|
||||
server.post<{ Params: { id: string }; Body: { sql: string } }>('/api/v1/sessions/:id/sql', async (request, reply) => {
|
||||
const { id } = request.params
|
||||
const db = ensureDb(dbManager, id)
|
||||
const db = ensureDb(ctx, id)
|
||||
|
||||
const { sql } = request.body || {}
|
||||
if (!sql || typeof sql !== 'string') {
|
||||
@@ -177,10 +171,9 @@ export function registerSessionRoutes(server: FastifyInstance, dbManager: Databa
|
||||
}
|
||||
})
|
||||
|
||||
// GET /api/v1/sessions/:id/export — Export ChatLab Format JSON
|
||||
server.get<{ Params: { id: string } }>('/api/v1/sessions/:id/export', async (request, reply) => {
|
||||
const { id } = request.params
|
||||
const db = ensureDb(dbManager, id)
|
||||
const db = ensureDb(ctx, id)
|
||||
const meta = getSessionMeta(db)
|
||||
if (!meta) throw sessionNotFound(id)
|
||||
const overview = getSessionOverview(db)
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Smoke tests for registerSharedRoutes — verifies all route groups
|
||||
* are registered and respond correctly with mock context.
|
||||
*
|
||||
* Uses a single Fastify instance to avoid repeated NLP dict init overhead.
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import Fastify, { type FastifyInstance } from 'fastify'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import type { DatabaseManager, SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../context'
|
||||
import { registerSharedRoutes } from '../register'
|
||||
|
||||
function createTestContext(): HttpRouteContext {
|
||||
const pathProvider: PathProvider = {
|
||||
getSystemDir: () => '/tmp/chatlab-test',
|
||||
getUserDataDir: () => '/tmp/chatlab-test/data',
|
||||
getDatabaseDir: () => '/tmp/chatlab-test/databases',
|
||||
getAiDataDir: () => '/tmp/chatlab-test/ai',
|
||||
getSettingsDir: () => '/tmp/chatlab-test/settings',
|
||||
getCacheDir: () => '/tmp/chatlab-test/cache',
|
||||
getTempDir: () => '/tmp/chatlab-test/temp',
|
||||
getLogsDir: () => '/tmp/chatlab-test/logs',
|
||||
getDownloadsDir: () => '/tmp/chatlab-test/downloads',
|
||||
}
|
||||
|
||||
const dbManager = {
|
||||
listSessionIds: () => [],
|
||||
open: () => null,
|
||||
openWritable: () => null,
|
||||
close: () => {},
|
||||
closeAll: () => {},
|
||||
getDbPath: (id: string) => `/tmp/${id}.db`,
|
||||
} as unknown as DatabaseManager
|
||||
|
||||
const sessionAdapter: SessionRuntimeAdapter = {
|
||||
listSessionIds: () => [],
|
||||
openReadonly: () => null,
|
||||
openWritable: () => null,
|
||||
closeSession: () => {},
|
||||
getDbPath: (id: string) => `/tmp/${id}.db`,
|
||||
deleteSessionFile: () => false,
|
||||
ensureReadonly: () => {
|
||||
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
||||
},
|
||||
ensureWritable: () => {
|
||||
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
||||
},
|
||||
}
|
||||
|
||||
return { dbManager, sessionAdapter, pathProvider, getVersion: () => '0.0.0-test' }
|
||||
}
|
||||
|
||||
describe('registerSharedRoutes smoke tests', () => {
|
||||
let app: FastifyInstance
|
||||
|
||||
before(async () => {
|
||||
app = Fastify()
|
||||
registerSharedRoutes(app, createTestContext())
|
||||
await app.ready()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('GET /api/v1/status returns 200 with version', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/api/v1/status' })
|
||||
assert.equal(resp.statusCode, 200)
|
||||
const body = resp.json()
|
||||
assert.equal(body.data.version, '0.0.0-test')
|
||||
assert.equal(body.data.name, 'ChatLab API')
|
||||
})
|
||||
|
||||
it('GET /api/v1/schema returns schema definition', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/api/v1/schema' })
|
||||
assert.equal(resp.statusCode, 200)
|
||||
const body = resp.json()
|
||||
assert.equal(body.data.format, 'ChatLab Format')
|
||||
})
|
||||
|
||||
it('GET /api/v1/sessions returns empty list', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/api/v1/sessions' })
|
||||
assert.equal(resp.statusCode, 200)
|
||||
assert.deepEqual(resp.json().data, [])
|
||||
})
|
||||
|
||||
it('GET /_web/sessions returns empty list', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/_web/sessions' })
|
||||
assert.equal(resp.statusCode, 200)
|
||||
assert.ok(Array.isArray(resp.json()))
|
||||
})
|
||||
|
||||
it('GET /_web/sessions/:id returns 404 for missing session', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/_web/sessions/nonexistent' })
|
||||
assert.equal(resp.statusCode, 404)
|
||||
})
|
||||
|
||||
it('GET /_web/nlp/pos-tags returns 200', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/_web/nlp/pos-tags' })
|
||||
assert.equal(resp.statusCode, 200)
|
||||
})
|
||||
|
||||
it('GET /_web/preferences returns 200 or 500', async () => {
|
||||
const resp = await app.inject({ method: 'GET', url: '/_web/preferences' })
|
||||
assert.ok([200, 500].includes(resp.statusCode), `Expected 200 or 500, got ${resp.statusCode}`)
|
||||
})
|
||||
})
|
||||
@@ -6,22 +6,21 @@
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../context'
|
||||
import { successResponse } from '../errors'
|
||||
import { getVersion } from '../../version'
|
||||
|
||||
export function registerSystemRoutes(server: FastifyInstance, dbManager: DatabaseManager): void {
|
||||
export function registerSystemRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
server.get('/api/v1/status', async () => {
|
||||
let sessionCount = 0
|
||||
try {
|
||||
sessionCount = dbManager.listSessionIds().length
|
||||
sessionCount = ctx.dbManager.listSessionIds().length
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return successResponse({
|
||||
name: 'ChatLab API',
|
||||
version: getVersion(),
|
||||
version: ctx.getVersion(),
|
||||
uptime: Math.floor(process.uptime()),
|
||||
sessionCount,
|
||||
})
|
||||
+5
-13
@@ -1,6 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import { createJiebaNlpProvider, type SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { createJiebaNlpProvider } from '@openchatlab/node-runtime'
|
||||
import {
|
||||
getTimeRange,
|
||||
getAvailableYears,
|
||||
@@ -20,16 +20,12 @@ import {
|
||||
getClusterGraph,
|
||||
getLanguagePreferenceAnalysis,
|
||||
} from '@openchatlab/core'
|
||||
import { parseTimeFilter } from './helpers'
|
||||
import { parseTimeFilter } from '../../helpers'
|
||||
|
||||
type FilteredQuery = { startTs?: string; endTs?: string; memberId?: string }
|
||||
|
||||
export function registerAnalyticsRoutes(
|
||||
server: FastifyInstance,
|
||||
_dbManager: DatabaseManager,
|
||||
adapter: SessionRuntimeAdapter
|
||||
): void {
|
||||
// ==================== 时间范围 ====================
|
||||
export function registerAnalyticsRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
|
||||
server.get<{ Params: { id: string } }>('/_web/sessions/:id/years', async (request) => {
|
||||
const db = adapter.ensureReadonly(request.params.id)
|
||||
@@ -41,8 +37,6 @@ export function registerAnalyticsRoutes(
|
||||
return getTimeRange(db)
|
||||
})
|
||||
|
||||
// ==================== 统计分析 ====================
|
||||
|
||||
server.get<{ Params: { id: string }; Querystring: FilteredQuery }>(
|
||||
'/_web/sessions/:id/stats/member-activity',
|
||||
async (request) => {
|
||||
@@ -83,8 +77,6 @@ export function registerAnalyticsRoutes(
|
||||
}
|
||||
)
|
||||
|
||||
// ==================== 高级分析 ====================
|
||||
|
||||
server.get<{ Params: { id: string }; Querystring: FilteredQuery }>(
|
||||
'/_web/sessions/:id/analytics/relationship',
|
||||
async (request) => {
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { exportService, type SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { exportService } from '@openchatlab/node-runtime'
|
||||
|
||||
export function registerExportRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
|
||||
export function registerExportRoutes(server: FastifyInstance, adapter: SessionRuntimeAdapter): void {
|
||||
server.post<{
|
||||
Params: { id: string }
|
||||
Body: {
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { memberService, type SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { memberService } from '@openchatlab/node-runtime'
|
||||
|
||||
export function registerMemberRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
|
||||
export function registerMemberRoutes(server: FastifyInstance, adapter: SessionRuntimeAdapter): void {
|
||||
server.get<{ Params: { id: string } }>('/_web/sessions/:id/members', async (request) => {
|
||||
return memberService.getMembers(adapter, request.params.id)
|
||||
})
|
||||
@@ -1,16 +1,13 @@
|
||||
/**
|
||||
* NLP Web API — /_web/nlp/ routes
|
||||
*
|
||||
* 提供词频统计、词性标签、词库管理等 NLP 功能的 HTTP 接口,
|
||||
* 供 CLI Web 前端使用(对齐 Electron preload 的 window.nlpApi)。
|
||||
*
|
||||
* 业务逻辑全部来自 @openchatlab/core(类型/数据)和
|
||||
* @openchatlab/node-runtime(分词引擎/词频计算/词库管理)。
|
||||
* Word frequency, POS tags, dictionary management.
|
||||
* Business logic from @openchatlab/core and @openchatlab/node-runtime.
|
||||
*/
|
||||
|
||||
import * as path from 'path'
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { DatabaseManager } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import type { WordFrequencyParams, SupportedLocale } from '@openchatlab/core'
|
||||
import { POS_TAG_DEFINITIONS } from '@openchatlab/core'
|
||||
import {
|
||||
@@ -24,49 +21,37 @@ import {
|
||||
ensureDefaultDict,
|
||||
} from '@openchatlab/node-runtime'
|
||||
|
||||
function ensureDb(dbManager: DatabaseManager, sessionId: string) {
|
||||
const db = dbManager.open(sessionId)
|
||||
if (!db) {
|
||||
throw Object.assign(new Error(`Session not found: ${sessionId}`), { statusCode: 404 })
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
export function registerNlpRoutes(server: FastifyInstance, dbManager: DatabaseManager): void {
|
||||
const pathProvider = (dbManager as any)['pathProvider']
|
||||
if (pathProvider) {
|
||||
const nlpDir = path.join(pathProvider.getSystemDir(), 'nlp')
|
||||
initNlpDir(nlpDir)
|
||||
ensureDefaultDict(nlpDir).catch((err) => console.warn('[NLP] Auto-download zh-CN dict failed:', err))
|
||||
}
|
||||
export function registerNlpRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const nlpDir = path.join(ctx.pathProvider.getSystemDir(), 'nlp')
|
||||
initNlpDir(nlpDir)
|
||||
ensureDefaultDict(nlpDir).catch((err) => console.warn('[NLP] Auto-download zh-CN dict failed:', err))
|
||||
|
||||
server.get('/_web/nlp/pos-tags', async () => {
|
||||
return POS_TAG_DEFINITIONS
|
||||
})
|
||||
|
||||
server.get('/_web/nlp/dicts', async () => {
|
||||
const nlpDir = resolveNlpDir(dbManager)
|
||||
return getDictList(nlpDir)
|
||||
})
|
||||
|
||||
server.get<{ Params: { id: string } }>('/_web/nlp/dicts/:id/status', async (request) => {
|
||||
const nlpDir = resolveNlpDir(dbManager)
|
||||
return isDictDownloaded(nlpDir, request.params.id)
|
||||
})
|
||||
|
||||
server.post<{ Params: { id: string } }>('/_web/nlp/dicts/:id/download', async (request) => {
|
||||
const nlpDir = resolveNlpDir(dbManager)
|
||||
return downloadDict(nlpDir, request.params.id)
|
||||
})
|
||||
|
||||
server.delete<{ Params: { id: string } }>('/_web/nlp/dicts/:id', async (request) => {
|
||||
const nlpDir = resolveNlpDir(dbManager)
|
||||
return deleteDict(nlpDir, request.params.id)
|
||||
})
|
||||
|
||||
server.post<{ Body: WordFrequencyParams }>('/_web/nlp/word-frequency', async (request) => {
|
||||
const params = request.body
|
||||
const db = ensureDb(dbManager, params.sessionId)
|
||||
const db = ctx.dbManager.open(params.sessionId)
|
||||
if (!db) {
|
||||
throw Object.assign(new Error(`Session not found: ${params.sessionId}`), { statusCode: 404 })
|
||||
}
|
||||
return computeWordFrequency(db, params)
|
||||
})
|
||||
|
||||
@@ -78,8 +63,3 @@ export function registerNlpRoutes(server: FastifyInstance, dbManager: DatabaseMa
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function resolveNlpDir(dbManager: DatabaseManager): string {
|
||||
const pathProvider = (dbManager as any)['pathProvider']
|
||||
return path.join(pathProvider.getSystemDir(), 'nlp')
|
||||
}
|
||||
+5
-9
@@ -5,23 +5,19 @@
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { PreferencesManager, type Preferences } from '@openchatlab/node-runtime'
|
||||
import { loadConfig, writeConfigField, type UiConfig } from '@openchatlab/config'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
|
||||
let prefManager: PreferencesManager | null = null
|
||||
|
||||
export function registerPreferencesRoutes(server: FastifyInstance, pathProvider: PathProvider): void {
|
||||
if (!prefManager) {
|
||||
prefManager = new PreferencesManager(pathProvider.getSystemDir())
|
||||
}
|
||||
export function registerPreferencesRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const prefManager = ctx.preferencesManager ?? new PreferencesManager(ctx.pathProvider.getSystemDir())
|
||||
|
||||
server.get('/_web/preferences', async () => {
|
||||
return prefManager!.load()
|
||||
return prefManager.load()
|
||||
})
|
||||
|
||||
server.patch<{ Body: Partial<Preferences> }>('/_web/preferences', async (request) => {
|
||||
return prefManager!.save(request.body)
|
||||
return prefManager.save(request.body)
|
||||
})
|
||||
|
||||
server.get('/_web/preferences/ui-config', async () => {
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { sessionIndexService, type SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { sessionIndexService } from '@openchatlab/node-runtime'
|
||||
|
||||
export function registerSessionIndexRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
|
||||
export function registerSessionIndexRoutes(server: FastifyInstance, adapter: SessionRuntimeAdapter): void {
|
||||
server.get('/_web/sessions/index-stats', async () => {
|
||||
return sessionIndexService.getAllIndexStats(adapter)
|
||||
})
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { sessionService, type SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { sessionService } from '@openchatlab/node-runtime'
|
||||
|
||||
export function registerSessionRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
|
||||
export function registerSessionRoutes(server: FastifyInstance, adapter: SessionRuntimeAdapter): void {
|
||||
server.get('/_web/sessions', async () => {
|
||||
return sessionService.listAnalysisSessions(adapter)
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { SessionRuntimeAdapter } from '@openchatlab/node-runtime'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
import { executeSql, getSchemaDetailed } from '@openchatlab/core'
|
||||
|
||||
export function registerSqlRoutes(server: FastifyInstance, adapter: SessionRuntimeAdapter): void {
|
||||
export function registerSqlRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
const { sessionAdapter: adapter } = ctx
|
||||
|
||||
server.post<{ Params: { id: string }; Body: { sql: string } }>('/_web/sessions/:id/sql', async (request, reply) => {
|
||||
const db = adapter.ensureReadonly(request.params.id)
|
||||
const { sql } = request.body || {}
|
||||
Generated
+21
@@ -140,6 +140,9 @@ importers:
|
||||
'@openchatlab/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
'@openchatlab/http-routes':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/http-routes
|
||||
'@openchatlab/node-runtime':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/node-runtime
|
||||
@@ -231,6 +234,24 @@ importers:
|
||||
|
||||
packages/core: {}
|
||||
|
||||
packages/http-routes:
|
||||
dependencies:
|
||||
'@openchatlab/config':
|
||||
specifier: workspace:*
|
||||
version: link:../config
|
||||
'@openchatlab/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@openchatlab/node-runtime':
|
||||
specifier: workspace:*
|
||||
version: link:../node-runtime
|
||||
'@openchatlab/shared-types':
|
||||
specifier: workspace:*
|
||||
version: link:../shared-types
|
||||
fastify:
|
||||
specifier: ^5.8.4
|
||||
version: 5.8.4
|
||||
|
||||
packages/mcp-server:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
|
||||
Reference in new issue
Block a user