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:
diguaandCursor committed 2026-06-02 15:41:21 +08:00
1 parent cd3ae8c5ba
commit a8f0d5cb88
26 files changed
+347 -128

No files matched your search

+1
View File
@@ -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 -1
View File
@@ -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
+1 -12
View File
@@ -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'
+10 -12
View File
@@ -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 () => {
+14 -3
View File
@@ -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()
+1 -2
View File
@@ -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
+2 -1
View File
@@ -13,6 +13,7 @@
"../../packages/parser/**/*",
"../../packages/tools/**/*",
"../../packages/sync/**/*",
"../../packages/mcp-server/**/*"
"../../packages/mcp-server/**/*",
"../../packages/http-routes/**/*"
]
}
+16
View File
@@ -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
+23
View File
@@ -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 {
+15
View File
@@ -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
}
+29
View File
@@ -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'
+35
View File
@@ -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)
}
@@ -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,
})
@@ -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) => {
@@ -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: {
@@ -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,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 () => {
@@ -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)
})
@@ -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 || {}
+21
View File
@@ -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':