refactor(http): organize shared route package

This commit is contained in:
digua committed 2026-07-16 20:09:18 +08:00
1 parent bf7004d0e0
commit 4b52a7d2f8
35 files changed
+84 -217

No files matched your search

+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 '@openchatlab/http-routes/auth'
import { setAuthToken, setRequireAuth, authHook } from './auth'
function fakeRequest(url: string, authorization?: string) {
return { url, headers: { authorization } } as never
+21
View File
@@ -0,0 +1,21 @@
/** CLI HTTP authentication policy built on the shared Bearer hook. */
import { createBearerAuthHook } from '@openchatlab/http-routes/auth'
let cachedToken: string | null = null
let requireAuthEnabled = false
export function setAuthToken(token: string): void {
cachedToken = token
}
/** Require Bearer auth for Web UI routes in headless or remote deployments. */
export function setRequireAuth(enabled: boolean): void {
requireAuthEnabled = enabled
}
export const authHook = createBearerAuthHook({
getToken: () => cachedToken,
shouldAuthenticate: (request) =>
request.url.startsWith('/api/') || (requireAuthEnabled && request.url.startsWith('/_web/')),
})
+2 -2
View File
@@ -17,6 +17,7 @@ import {
CustomProviderStore,
CustomModelStore,
createFileConfigStorage,
createAuthProfileLlmConfigStore,
applyPendingNodeDataDirMigrationIfNeeded,
hasPendingElectronDataWarning,
verifyCliDataPath,
@@ -27,8 +28,7 @@ import {
} from '@openchatlab/node-runtime'
import type { SemanticIndexRuntime } from '@openchatlab/node-runtime'
import { createServer } from './server'
import { setAuthToken, setRequireAuth } from '@openchatlab/http-routes/auth'
import { createAuthProfileLlmConfigStore } from '@openchatlab/http-routes/ai-config'
import { setAuthToken, setRequireAuth } from './auth'
import { registerWebRoutes } from './routes/web'
import { registerProxyRoutes } from './routes/proxy'
import { initServerAiLogger, closeServerAiLogger } from '../ai/logger'
-19
View File
@@ -3,20 +3,9 @@
*/
import * as fs from 'fs'
import type { TimeFilter } from '@openchatlab/shared-types'
import type { DatabaseManager } from '@openchatlab/node-runtime'
import { resolveCliPath } from '../../../paths'
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
}
export function resolveNativeBinding(): string | undefined {
if (process.versions.electron) return undefined
const nativePath = resolveCliPath('native/better_sqlite3.node')
@@ -31,11 +20,3 @@ export function getAiDataDir(dbManager: DatabaseManager): string {
}
return pathProvider.getAiDataDir()
}
export 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
}
@@ -1,45 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import Fastify from 'fastify'
import type { SessionRuntimeAdapter } from '@openchatlab/node-runtime'
import type { HttpRouteContext } from '@openchatlab/http-routes'
import { registerSessionRoutes } from '@openchatlab/http-routes'
function createMissingSessionAdapter(): SessionRuntimeAdapter {
return {
listSessionIds: () => [],
openReadonly: () => null,
openWritable: () => null,
closeSession: () => {},
getDbPath: (sessionId) => `/tmp/${sessionId}.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 })
},
}
}
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()
const ctx = createMockContext(createMissingSessionAdapter())
registerSessionRoutes(app, ctx)
const response = await app.inject({ method: 'GET', url: '/_web/sessions/missing' })
await app.close()
assert.equal(response.statusCode, 404)
})
})
+1 -1
View File
@@ -5,9 +5,9 @@
*/
import type { FastifyInstance } from 'fastify'
import { authHook } from '@openchatlab/http-routes/auth'
import { createApiServer } from '@openchatlab/http-routes/server'
import { appLogger } from '@openchatlab/node-runtime'
import { authHook } from './auth'
export function createServer(): FastifyInstance {
return createApiServer({
+2 -3
View File
@@ -8,11 +8,10 @@ const rootDir = resolve(__dirname, '../..')
const rootPkg = JSON.parse(readFileSync(resolve(rootDir, 'package.json'), 'utf-8'))
const appVersion: string = rootPkg.version
const openChatLabAliases = {
'@openchatlab/http-routes/ai-config': resolve(rootDir, 'packages/http-routes/src/ai-config.ts'),
'@openchatlab/http-routes/auth': resolve(rootDir, 'packages/http-routes/src/auth.ts'),
'@openchatlab/http-routes/errors': resolve(rootDir, 'packages/http-routes/src/errors.ts'),
'@openchatlab/http-routes/import': resolve(rootDir, 'packages/http-routes/src/import.ts'),
'@openchatlab/http-routes/rest': resolve(rootDir, 'packages/http-routes/src/rest.ts'),
'@openchatlab/http-routes/import': resolve(rootDir, 'packages/http-routes/src/import/json-push-handler.ts'),
'@openchatlab/http-routes/rest': resolve(rootDir, 'packages/http-routes/src/routes/rest/index.ts'),
'@openchatlab/http-routes/server': resolve(rootDir, 'packages/http-routes/src/server.ts'),
'@openchatlab/node-runtime/import/archive/source-manager': resolve(
rootDir,
+1 -1
View File
@@ -19,6 +19,7 @@ import {
CustomProviderStore,
CustomModelStore,
createFileConfigStorage,
createAuthProfileLlmConfigStore,
MergeSessionCache,
withDataDirImportLock,
raiseChatDbCompatibilityGate,
@@ -30,7 +31,6 @@ import type { StreamImportDeps, SemanticIndexRuntime } from '@openchatlab/node-r
import { getLoadablePath as getSqliteVecLoadablePath } from 'sqlite-vec'
import multipart from '@fastify/multipart'
import { registerSharedRoutes, registerAutomationRoutes } from '@openchatlab/http-routes'
import { createAuthProfileLlmConfigStore } from '@openchatlab/http-routes/ai-config'
import { createApiServer } from '@openchatlab/http-routes/server'
import type { HttpRouteContext } from '@openchatlab/http-routes'
import { reloadTimer, stopTimer, type DataSourceManager, type PullEngine } from '@openchatlab/sync'
+3 -4
View File
@@ -2,17 +2,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",
"description": "ChatLab shared HTTP routes and infrastructure for CLI and Electron servers",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./ai-config": "./src/ai-config.ts",
"./auth": "./src/auth.ts",
"./errors": "./src/errors.ts",
"./import": "./src/import.ts",
"./rest": "./src/rest.ts",
"./import": "./src/import/json-push-handler.ts",
"./rest": "./src/routes/rest/index.ts",
"./server": "./src/server.ts"
},
"dependencies": {
-1
View File
@@ -1 +0,0 @@
export { createAuthProfileLlmConfigStore, type AuthProfileLlmConfigStoreDeps } from './ai/auth-profile-llm-config-store'
+1 -24
View File
@@ -1,18 +1,13 @@
/**
* ChatLab HTTP API — Bearer Token authentication hook
*
* Shared auth middleware for CLI Server and Electron APIs.
* URL classification: /api/* always requires token, /_web/* conditionally,
* static files and SPA fallback are public.
* Shared auth middleware factory for CLI and Electron APIs.
*/
import type { FastifyRequest, FastifyReply } from 'fastify'
import { timingSafeEqual, createHmac, randomBytes } from 'crypto'
import { unauthorized, errorResponse } from './errors'
let cachedToken: string | null = null
let requireAuthEnabled = false
export interface BearerAuthOptions {
getToken: () => string | null | undefined
shouldAuthenticate?: (request: FastifyRequest) => boolean
@@ -21,18 +16,6 @@ export interface BearerAuthOptions {
invalidTokenMessage?: string
}
export function setAuthToken(token: string): void {
cachedToken = token
}
/**
* When enabled, /_web/* routes also require Bearer token (same as /api/*).
* Used for server/headless deployments where same-origin assumption doesn't hold.
*/
export function setRequireAuth(enabled: boolean): void {
requireAuthEnabled = enabled
}
export function createBearerAuthHook(options: BearerAuthOptions) {
// Compare via HMAC digests (fixed 32-byte length) to avoid leaking token length.
const hmacKey = randomBytes(32)
@@ -62,9 +45,3 @@ export function createBearerAuthHook(options: BearerAuthOptions) {
}
}
}
export const authHook = createBearerAuthHook({
getToken: () => cachedToken,
shouldAuthenticate: (request) =>
request.url.startsWith('/api/') || (requireAuthEnabled && request.url.startsWith('/_web/')),
})
+1 -1
View File
@@ -30,7 +30,7 @@ import type {
GlobalInsightService,
} from '@openchatlab/node-runtime'
import type { RuntimeIdentity } from '@openchatlab/node-runtime'
import type { RestSessionProvider } from './routes/rest-session-provider'
import type { RestSessionProvider } from './routes/rest/session-provider'
export interface HttpRouteContext {
dbManager: DatabaseManager
-10
View File
@@ -1,10 +0,0 @@
export { registerImportRoutes } from './routes/imports'
export {
buildImportIdempotencyCacheKey,
createJsonPushImportHandler,
type ImportSuccessResponse,
type JsonPushImportHandler,
type JsonPushImportHandlerOptions,
type JsonPushImportHttpResult,
type JsonPushImportRequest,
} from './import/json-push-handler'
-69
View File
@@ -8,75 +8,6 @@ export type {
} from './context'
export { registerSharedRoutes } from './register'
export type { SharedRouteOptions } from './register'
export { setAuthToken, setRequireAuth, authHook } from './auth'
export {
ApiError,
ApiErrorCode,
unauthorized,
sessionNotFound,
invalidFormat,
invalidPayload,
sqlReadonlyViolation,
sqlExecutionError,
exportTooLarge,
serverError,
importInProgress,
idempotencyConflict,
idempotencyPending,
importFailed,
dataDirIncompatible,
apiErrorFromUnknown,
successResponse,
errorResponse,
} from './errors'
export { parseTimeFilter } from './helpers'
export { createApiServer, configureApiErrorHandler, DEFAULT_JSON_BODY_LIMIT } from './server'
export type { ApiServerOptions } from './server'
export {
buildImportIdempotencyCacheKey,
createJsonPushImportHandler,
type ImportSuccessResponse,
type JsonPushImportHandler,
type JsonPushImportHandlerOptions,
type JsonPushImportHttpResult,
type JsonPushImportRequest,
} from './import/json-push-handler'
export { buildAnalyticsCacheKey, withAnalyticsCache } from './analytics-cache'
export { executeRegistryTool } from './ai/tool-executor'
export type { AiToolExecutionDeps } from './ai/tool-executor'
export { createAuthProfileLlmConfigStore } from './ai/auth-profile-llm-config-store'
export type { AuthProfileLlmConfigStoreDeps } from './ai/auth-profile-llm-config-store'
// Individual route registration for granular testing or selective registration
export { registerSystemRoutes } from './routes/system'
export type { SystemRouteContext } from './routes/system'
export { registerRestSessionRoutes } from './routes/sessions'
export { createDatabaseRestSessionProvider } from './routes/rest-session-provider'
export type {
RestMessageQuery,
RestMessagePage,
RestSessionDetail,
RestSessionExportData,
RestSessionOverview,
RestSessionProvider,
RestSessionSummary,
} from './routes/rest-session-provider'
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'
export { registerAiAssistantRoutes } from './routes/web/ai-assistants'
export { registerAiSkillRoutes } from './routes/web/ai-skills'
export { registerAiLlmRoutes } from './routes/web/ai-llm'
export { registerAiChatRoutes } from './routes/web/ai-chats'
export { registerAiSummaryRoutes } from './routes/web/ai-summaries'
export { registerAiLlmStreamRoutes } from './routes/web/ai-llm-stream'
export { registerAiAgentStreamRoutes } from './routes/web/ai-agent-stream'
export { registerAiToolRoutes } from './routes/web/ai-tools'
export { registerTelemetryRoutes } from './routes/web/telemetry'
export { registerAutomationRoutes } from './routes/web/automation'
export { registerGlobalInsightRoutes } from './routes/web/global-insight'
+4 -4
View File
@@ -8,10 +8,10 @@
import type { FastifyInstance } from 'fastify'
import type { HttpRouteContext } from './context'
import { PreferencesManager } from '@openchatlab/node-runtime'
import { registerSystemRoutes } from './routes/system'
import { registerRestSessionRoutes } from './routes/sessions'
import { createDatabaseRestSessionProvider } from './routes/rest-session-provider'
import { registerImportRoutes } from './routes/imports'
import { registerSystemRoutes } from './routes/rest/system'
import { registerRestSessionRoutes } from './routes/rest/sessions'
import { createDatabaseRestSessionProvider } from './routes/rest/session-provider'
import { registerImportRoutes } from './routes/rest/imports'
import { registerSessionRoutes } from './routes/web/sessions'
import { registerMemberRoutes } from './routes/web/members'
import { registerContactsRoutes } from './routes/web/contacts'
-13
View File
@@ -1,13 +0,0 @@
export type {
RestMessageQuery,
RestMessagePage,
RestSessionDetail,
RestSessionExportData,
RestSessionOverview,
RestSessionProvider,
RestSessionSummary,
} from './routes/rest-session-provider'
export { createDatabaseRestSessionProvider } from './routes/rest-session-provider'
export { registerSystemRoutes } from './routes/system'
export type { SystemRouteContext } from './routes/system'
export { registerRestSessionRoutes } from './routes/sessions'
@@ -6,7 +6,7 @@ import test from 'node:test'
import Fastify from 'fastify'
import type { PathProvider } from '@openchatlab/core'
import { DatabaseManager } from '@openchatlab/node-runtime'
import type { HttpRouteContext } from '../context'
import type { HttpRouteContext } from '../../context'
import { registerImportRoutes } from './imports'
const nativeBinding = path.resolve('apps/cli/native/better_sqlite3.node')
@@ -7,10 +7,10 @@
*/
import type { FastifyInstance } from 'fastify'
import type { HttpRouteContext } from '../context'
import type { HttpRouteContext } from '../../context'
import { appLogger, pushImport } from '@openchatlab/node-runtime'
import type { PushImportPayload } from '@openchatlab/node-runtime'
import { createJsonPushImportHandler } from '../import/json-push-handler'
import { createJsonPushImportHandler } from '../../import/json-push-handler'
export function registerImportRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
const handleJsonPushImport = createJsonPushImportHandler({
@@ -0,0 +1,13 @@
export type {
RestMessageQuery,
RestMessagePage,
RestSessionDetail,
RestSessionExportData,
RestSessionOverview,
RestSessionProvider,
RestSessionSummary,
} from './session-provider'
export { createDatabaseRestSessionProvider } from './session-provider'
export { registerSystemRoutes } from './system'
export type { SystemRouteContext } from './system'
export { registerRestSessionRoutes } from './sessions'
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import Fastify from 'fastify'
import type { RestSessionProvider } from './rest-session-provider'
import type { RestSessionProvider } from './session-provider'
import { registerRestSessionRoutes } from './sessions'
import { registerSystemRoutes } from './system'
@@ -1,3 +1,4 @@
/** Data access contract for the public REST session routes. */
import {
executeReadonlySql,
getLastPlatformMessageId,
@@ -6,8 +6,15 @@
*/
import type { FastifyInstance } from 'fastify'
import { successResponse, errorResponse, sessionNotFound, exportTooLarge, sqlExecutionError, ApiError } from '../errors'
import type { RestSessionProvider } from './rest-session-provider'
import {
successResponse,
errorResponse,
sessionNotFound,
exportTooLarge,
sqlExecutionError,
ApiError,
} from '../../errors'
import type { RestSessionProvider } from './session-provider'
const EXPORT_MESSAGE_LIMIT = 100_000
@@ -6,7 +6,7 @@
*/
import type { FastifyInstance } from 'fastify'
import { successResponse } from '../errors'
import { successResponse } from '../../errors'
export interface SystemRouteContext {
getVersion: () => string
@@ -16,8 +16,8 @@ import type { DatabaseAdapter, PathProvider, PreparedStatement, RunResult } from
import { PreferencesManager, type DatabaseManager, type SessionRuntimeAdapter } from '@openchatlab/node-runtime'
import type { HttpRouteContext } from '../context'
import { registerSharedRoutes } from '../register'
import { registerRestSessionRoutes } from './sessions'
import { createDatabaseRestSessionProvider } from './rest-session-provider'
import { registerRestSessionRoutes } from './rest/sessions'
import { createDatabaseRestSessionProvider } from './rest/session-provider'
import { registerAutomationRoutes } from './web/automation'
import { registerSessionRoutes } from './web/sessions'
@@ -1,7 +1,7 @@
/**
* buildAnalyticsCacheKey tests.
*
* 运行:node --import tsx --test packages/http-routes/src/analytics-cache.test.ts
* Run: pnpm test -- packages/http-routes/src/routes/web/analytics-cache.test.ts
*
* 键的确定性直接决定缓存命中率与正确性:等价请求必须同键,不同请求必须异键。
*/
@@ -12,7 +12,7 @@ import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { buildAnalyticsCacheKey, withAnalyticsCache } from './analytics-cache'
import type { HttpRouteContext } from './context'
import type { HttpRouteContext } from '../../context'
describe('buildAnalyticsCacheKey', () => {
it('is independent of param key order', () => {
@@ -11,7 +11,7 @@
import * as path from 'path'
import { getDbFileVersion, getOrComputeAnalysisCache } from '@openchatlab/node-runtime'
import type { HttpRouteContext } from './context'
import type { HttpRouteContext } from '../../context'
/** Deterministic stringify: sorts object keys recursively and drops `undefined`. */
function canonical(value: unknown): string {
@@ -33,8 +33,8 @@ import {
getRepeatAnalysis,
} from '@openchatlab/core'
import type { ClusterGraphOptions } from '@openchatlab/core'
import { parseTimeFilter } from '../../helpers'
import { withAnalyticsCache } from '../../analytics-cache'
import { parseTimeFilter } from './time-filter'
import { withAnalyticsCache } from './analytics-cache'
type FilteredQuery = { startTs?: string; endTs?: string; memberId?: string }
+1 -1
View File
@@ -9,7 +9,7 @@ import * as fs from 'fs'
import * as path from 'path'
import type { FastifyInstance } from 'fastify'
import type { HttpRouteContext } from '../../context'
import { withAnalyticsCache } from '../../analytics-cache'
import { withAnalyticsCache } from './analytics-cache'
import type { WordFrequencyParams, SupportedLocale } from '@openchatlab/core'
import { POS_TAG_DEFINITIONS } from '@openchatlab/core'
import {
@@ -1,5 +1,5 @@
/**
* Shared helpers for route modules.
* Time-filter query parsing for Web analytics routes.
*/
import type { TimeFilter } from '@openchatlab/shared-types'
+1
View File
@@ -17,6 +17,7 @@
"@earendil-works/pi-agent-core": "0.74.2",
"@earendil-works/pi-ai": "0.74.2",
"@huggingface/transformers": "^4.2.0",
"@openchatlab/config": "workspace:*",
"@openchatlab/core": "workspace:*",
"better-sqlite3": "^12.4.6",
"gray-matter": "^4.0.3",
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { ConfigStorage } from '@openchatlab/node-runtime'
import type { ConfigStorage } from './llm-config-store'
import { createAuthProfileLlmConfigStore } from './auth-profile-llm-config-store'
function createMemoryStorage(): ConfigStorage & { data: Map<string, unknown> } {
@@ -1,5 +1,5 @@
import { deleteAuthProfile, resolveApiKey, writeAuthProfile, type AuthProfile } from '@openchatlab/config'
import { LLMConfigStore, type AIServiceConfig, type ConfigStorage } from '@openchatlab/node-runtime'
import { LLMConfigStore, type AIServiceConfig, type ConfigStorage } from './llm-config-store'
export interface AuthProfileLlmConfigStoreDeps {
resolveApiKey: (provider: string, authProfile?: string) => string | undefined
+2
View File
@@ -164,6 +164,8 @@ export type { SummaryDeps, SummaryMessage, SummaryOptions, SummaryResult, Summar
export { LLMConfigStore, MAX_CONFIG_COUNT } from './llm-config-store'
export type { AIServiceConfig, AIConfigStore, ConfigStorage, LLMConfigStoreDeps } from './llm-config-store'
export { createFileConfigStorage } from './file-config-storage'
export { createAuthProfileLlmConfigStore } from './auth-profile-llm-config-store'
export type { AuthProfileLlmConfigStoreDeps } from './auth-profile-llm-config-store'
// Custom Provider/Model Store
export { CustomProviderStore, CustomModelStore } from './custom-store'
+2 -1
View File
@@ -157,7 +157,8 @@ export type { AssistantConfig, AssistantSummary, SkillDef, SkillSummary } from '
export { parseAssistantFile, serializeAssistant, parseSkillFile, extractSkillId } from './ai'
export { AIChatManager } from './ai'
export { countTokens, countMessagesTokens, initTokenizer } from './ai'
export { createFileConfigStorage } from './ai'
export { createFileConfigStorage, createAuthProfileLlmConfigStore } from './ai'
export type { AuthProfileLlmConfigStoreDeps } from './ai'
// Assistant Manager
export { AssistantManager } from './ai'
+3
View File
@@ -331,6 +331,9 @@ importers:
'@huggingface/transformers':
specifier: ^4.2.0
version: 4.2.0
'@openchatlab/config':
specifier: workspace:*
version: link:../config
'@openchatlab/core':
specifier: workspace:*
version: link:../core