feat(web): implement FetchAdapter and /_web/ internal API routes

Phase 3A.1: Create the HTTP bridge between the Vue SPA frontend
and chatlab serve backend for the CLI serve Web version.

Server side (packages/server):
- Add /_web/ route group (routes/web.ts) with no auth requirement,
  covering: sessions CRUD, time range, member-activity/hourly/daily/
  weekday/message-type stats, members CRUD+pagination+merge+history,
  SQL Lab execute+schema.
- Skip auth hook for /_web/ prefix (same-origin internal API).
- Add getTimeRange() to @openchatlab/core basic queries.

Client side (src/adapters):
- FetchAdapter wrapping all /_web/ endpoints with proper HTTP verbs,
  TimeFilter query param serialization, and error handling.
- Unimplemented analysis methods (catchphrase, mention, cluster, etc.)
  throw descriptive errors for progressive enhancement.
- Wire FetchAdapter into adapter factory (non-Electron, non-browser).
This commit is contained in:
digua committed 2026-05-13 19:53:01 +08:00
1 parent 9ac4276465
commit 086bd882a7
8 files changed
+685 -15

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ export {
getSessionMeta,
getSessionOverview,
getDatabaseSchema,
getTimeRange,
getAvailableYears,
getMemberActivity,
getHourlyActivity,
+11
View File
@@ -38,6 +38,17 @@ export interface MessageTypeStats {
count: number
}
/**
* 获取消息时间范围
*/
export function getTimeRange(db: DatabaseAdapter): { start: number; end: number } | null {
const row = db.prepare('SELECT MIN(ts) as start, MAX(ts) as end FROM message').get() as
| { start: number | null; end: number | null }
| undefined
if (!row || row.start == null || row.end == null) return null
return { start: row.start, end: row.end }
}
/**
* 获取可用的年份列表
*/
+3 -13
View File
@@ -1,14 +1,10 @@
export { buildTimeFilter, buildSystemMessageFilter } from './filters'
export {
isChatSessionDb,
getSessionMeta,
getSessionOverview,
getDatabaseSchema,
} from './session-queries'
export { isChatSessionDb, getSessionMeta, getSessionOverview, getDatabaseSchema } from './session-queries'
export type { SessionMeta, SessionOverview, SessionInfo } from './session-queries'
export {
getTimeRange,
getAvailableYears,
getMemberActivity,
getHourlyActivity,
@@ -16,13 +12,7 @@ export {
getWeekdayActivity,
getMessageTypeStats,
} from './basic-queries'
export type {
MemberActivity,
HourlyActivity,
DailyActivity,
WeekdayActivity,
MessageTypeStats,
} from './basic-queries'
export type { MemberActivity, HourlyActivity, DailyActivity, WeekdayActivity, MessageTypeStats } from './basic-queries'
export {
queryMessages,
+3
View File
@@ -27,6 +27,9 @@ function safeTokenCompare(a: string, b: string): boolean {
export async function authHook(request: FastifyRequest, reply: FastifyReply): Promise<void> {
if (!cachedToken) return
// /_web/ 内部 API 不需要认证(仅限同源 Web UI 使用)
if (request.url.startsWith('/_web/')) return
const authHeader = request.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
const err = unauthorized()
+3
View File
@@ -16,6 +16,7 @@ import { createServer } from './server'
import { setAuthToken } from './auth'
import { registerSystemRoutes } from './routes/system'
import { registerSessionRoutes } from './routes/sessions'
import { registerWebRoutes } from './routes/web'
let server: FastifyInstance | null = null
let dbManager: DatabaseManager | null = null
@@ -85,6 +86,7 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
server = createServer()
registerSystemRoutes(server, dbManager)
registerSessionRoutes(server, dbManager)
registerWebRoutes(server, dbManager)
await server.listen({ port, host })
@@ -111,3 +113,4 @@ 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'
+374
View File
@@ -0,0 +1,374 @@
/**
* ChatLab Internal Web API — /_web/ routes
*
* 供 CLI serve Web 前端使用的内部 API(无认证、UI 友好的响应格式)。
* 数据格式直接对齐 QueryAdapter 接口,避免前端二次转换。
*/
import type { FastifyInstance } from 'fastify'
import type { DatabaseManager } from '@openchatlab/node-runtime'
import type { TimeFilter } from '@openchatlab/shared-types'
import {
getSessionMeta,
getSessionOverview,
getTimeRange,
getAvailableYears,
getMemberActivity,
getHourlyActivity,
getDailyActivity,
getWeekdayActivity,
getMessageTypeStats,
getDatabaseSchema,
executeReadonlySql,
} from '@openchatlab/core'
function ensureDb(dbManager: DatabaseManager, sessionId: string) {
const db = dbManager.open(sessionId)
if (!db) {
const err = new Error(`Session not found: ${sessionId}`)
;(err as any).statusCode = 404
throw err
}
return db
}
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 registerWebRoutes(server: FastifyInstance, dbManager: DatabaseManager): void {
// ==================== 会话管理 ====================
server.get('/_web/sessions', async () => {
const sessionIds = dbManager.listSessionIds()
return sessionIds
.map((id) => {
const db = dbManager.open(id)
if (!db) return null
const meta = getSessionMeta(db)
if (!meta) return null
const overview = getSessionOverview(db)
return {
id,
name: meta.name,
platform: meta.platform,
type: meta.type,
importedAt: meta.importedAt ?? 0,
messageCount: overview.totalMessages,
memberCount: overview.totalMembers,
dbPath: '',
groupId: meta.groupId ?? null,
groupAvatar: meta.groupAvatar ?? null,
ownerId: meta.ownerId ?? null,
memberAvatar: null,
lastMessageTs: overview.lastMessageTs ?? null,
summaryCount: 0,
aiConversationCount: 0,
}
})
.filter(Boolean)
})
server.get<{ Params: { id: string } }>('/_web/sessions/:id', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const meta = getSessionMeta(db)
if (!meta) return null
const overview = getSessionOverview(db)
return {
id: request.params.id,
name: meta.name,
platform: meta.platform,
type: meta.type,
importedAt: meta.importedAt ?? 0,
messageCount: overview.totalMessages,
memberCount: overview.totalMembers,
dbPath: '',
groupId: meta.groupId ?? null,
groupAvatar: meta.groupAvatar ?? null,
ownerId: meta.ownerId ?? null,
memberAvatar: null,
lastMessageTs: overview.lastMessageTs ?? null,
summaryCount: 0,
aiConversationCount: 0,
}
})
server.delete<{ Params: { id: string } }>('/_web/sessions/:id', async (request, reply) => {
const { id } = request.params
try {
dbManager.close(id)
const dbPath = dbManager.getDbPath(id)
const fs = await import('fs')
if (fs.existsSync(dbPath)) {
fs.unlinkSync(dbPath)
return { success: true }
}
return reply.code(404).send({ success: false, error: 'File not found' })
} catch (err) {
return reply.code(500).send({ success: false, error: String(err) })
}
})
server.patch<{ Params: { id: string }; Body: { name: string } }>('/_web/sessions/:id/name', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const { name } = request.body
db.prepare('UPDATE meta SET name = ?').run(name)
return { success: true }
})
server.patch<{ Params: { id: string }; Body: { ownerId: string | null } }>(
'/_web/sessions/:id/owner',
async (request) => {
const db = ensureDb(dbManager, request.params.id)
const { ownerId } = request.body
db.prepare('UPDATE meta SET owner_id = ?').run(ownerId ?? null)
return { success: true }
}
)
// ==================== 时间范围 ====================
server.get<{ Params: { id: string } }>('/_web/sessions/:id/years', async (request) => {
const db = ensureDb(dbManager, request.params.id)
return getAvailableYears(db)
})
server.get<{ Params: { id: string } }>('/_web/sessions/:id/time-range', async (request) => {
const db = ensureDb(dbManager, request.params.id)
return getTimeRange(db)
})
// ==================== 统计分析 ====================
server.get<{
Params: { id: string }
Querystring: { startTs?: string; endTs?: string; memberId?: string }
}>('/_web/sessions/:id/stats/member-activity', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const filter = parseTimeFilter(request.query)
return getMemberActivity(db, filter)
})
server.get<{
Params: { id: string }
Querystring: { startTs?: string; endTs?: string; memberId?: string }
}>('/_web/sessions/:id/stats/hourly', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const filter = parseTimeFilter(request.query)
return getHourlyActivity(db, filter)
})
server.get<{
Params: { id: string }
Querystring: { startTs?: string; endTs?: string; memberId?: string }
}>('/_web/sessions/:id/stats/daily', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const filter = parseTimeFilter(request.query)
return getDailyActivity(db, filter)
})
server.get<{
Params: { id: string }
Querystring: { startTs?: string; endTs?: string; memberId?: string }
}>('/_web/sessions/:id/stats/weekday', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const filter = parseTimeFilter(request.query)
return getWeekdayActivity(db, filter)
})
server.get<{
Params: { id: string }
Querystring: { startTs?: string; endTs?: string; memberId?: string }
}>('/_web/sessions/:id/stats/message-types', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const filter = parseTimeFilter(request.query)
return getMessageTypeStats(db, filter)
})
// ==================== 成员管理 ====================
server.get<{ Params: { id: string } }>('/_web/sessions/:id/members', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const rows = db
.prepare(
`SELECT
m.id, m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases, m.avatar,
(SELECT COUNT(*) FROM message WHERE sender_id = m.id) as messageCount
FROM member m
WHERE COALESCE(m.account_name, '') != '系统消息'
ORDER BY messageCount DESC`
)
.all() as any[]
return rows.map((r: any) => ({
...r,
aliases: r.aliases ? (typeof r.aliases === 'string' ? JSON.parse(r.aliases) : r.aliases) : [],
}))
})
server.get<{
Params: { id: string }
Querystring: { page?: string; pageSize?: string; search?: string; sortOrder?: string }
}>('/_web/sessions/:id/members/paginated', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const page = Math.max(1, parseInt(request.query.page || '1', 10))
const pageSize = Math.min(100, Math.max(1, parseInt(request.query.pageSize || '20', 10)))
const search = request.query.search?.trim() || ''
const sortOrder = request.query.sortOrder === 'asc' ? 'ASC' : 'DESC'
let whereClause = ''
const params: unknown[] = []
if (search) {
whereClause = `WHERE (m.account_name LIKE ? OR m.group_nickname LIKE ? OR m.platform_id LIKE ? OR m.aliases LIKE ?)`
const searchParam = `%${search}%`
params.push(searchParam, searchParam, searchParam, searchParam)
}
const countRow = db.prepare(`SELECT COUNT(*) as total FROM member m ${whereClause}`).get(...params) as {
total: number
}
const total = countRow.total
const totalPages = Math.ceil(total / pageSize)
const offset = (page - 1) * pageSize
const rows = db
.prepare(
`SELECT
m.id, m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases, m.avatar,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
${whereClause}
GROUP BY m.id
ORDER BY messageCount ${sortOrder}
LIMIT ? OFFSET ?`
)
.all(...params, pageSize, offset) as any[]
return {
items: rows.map((r: any) => ({
...r,
aliases: r.aliases ? (typeof r.aliases === 'string' ? JSON.parse(r.aliases) : r.aliases) : [],
})),
total,
page,
pageSize,
totalPages,
}
})
server.patch<{ Params: { id: string; memberId: string }; Body: { aliases: string[] } }>(
'/_web/sessions/:id/members/:memberId/aliases',
async (request) => {
const db = ensureDb(dbManager, request.params.id)
const memberId = parseInt(request.params.memberId, 10)
const { aliases } = request.body
db.prepare('UPDATE member SET aliases = ? WHERE id = ?').run(JSON.stringify(aliases), memberId)
return { success: true }
}
)
server.delete<{ Params: { id: string; memberId: string } }>(
'/_web/sessions/:id/members/:memberId',
async (request) => {
const db = ensureDb(dbManager, request.params.id)
const memberId = parseInt(request.params.memberId, 10)
db.prepare('DELETE FROM message WHERE sender_id = ?').run(memberId)
db.prepare('DELETE FROM member WHERE id = ?').run(memberId)
return { success: true }
}
)
server.post<{ Params: { id: string }; Body: { memberId1: number; memberId2: number } }>(
'/_web/sessions/:id/members/merge',
async (request) => {
const db = ensureDb(dbManager, request.params.id)
const { memberId1, memberId2 } = request.body
db.prepare('UPDATE message SET sender_id = ? WHERE sender_id = ?').run(memberId1, memberId2)
db.prepare('DELETE FROM member WHERE id = ?').run(memberId2)
return { success: true }
}
)
server.get<{ Params: { id: string; memberId: string } }>(
'/_web/sessions/:id/members/:memberId/history',
async (request) => {
const db = ensureDb(dbManager, request.params.id)
const memberId = parseInt(request.params.memberId, 10)
const rows = db
.prepare(
`SELECT
sender_account_name as accountName,
sender_group_nickname as groupNickname,
MIN(ts) as startTs,
MAX(ts) as endTs
FROM message
WHERE sender_id = ?
GROUP BY sender_account_name, sender_group_nickname
ORDER BY startTs`
)
.all(memberId) as any[]
const history: Array<{ nameType: string; name: string; startTs: number; endTs: number | null }> = []
for (const row of rows) {
if (row.accountName) {
history.push({ nameType: 'account_name', name: row.accountName, startTs: row.startTs, endTs: row.endTs })
}
if (row.groupNickname) {
history.push({ nameType: 'group_nickname', name: row.groupNickname, startTs: row.startTs, endTs: row.endTs })
}
}
return history
}
)
// ==================== SQL Lab ====================
server.post<{ Params: { id: string }; Body: { sql: string } }>('/_web/sessions/:id/sql', async (request, reply) => {
const db = ensureDb(dbManager, request.params.id)
const { sql } = request.body || {}
if (!sql || typeof sql !== 'string') {
return reply.code(400).send({ error: 'Missing sql parameter' })
}
try {
return executeReadonlySql(db, sql)
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'SQL execution error'
return reply.code(400).send({ error: message })
}
})
server.get<{ Params: { id: string } }>('/_web/sessions/:id/schema', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const tables = getDatabaseSchema(db)
return tables.map((t) => {
const columns = db.prepare(`PRAGMA table_info('${t.name}')`).all() as Array<{
name: string
type: string
notnull: number
pk: number
}>
return {
name: t.name,
columns: columns.map((c) => ({
name: c.name,
type: c.type,
notnull: c.notnull === 1,
pk: c.pk === 1,
})),
}
})
})
}
+288
View File
@@ -0,0 +1,288 @@
/**
* FetchAdapter — 通过 HTTP 调用 /_web/ 内部 API
*
* 用于 CLI serve Web 场景:前端 SPA 通过 fetch 访问 chatlab serve 后端。
* 所有请求发往同源 /_web/ 前缀,无需 token 认证。
*/
import type {
QueryAdapter,
AdapterCapabilities,
SQLResult,
TableSchema,
PaginationParams,
PaginatedResult,
MentionGraphData,
MessageLengthDistribution,
} from './types'
import type { AnalysisSession, MessageType } from '@/types/base'
import type { TimeFilter } from '@openchatlab/shared-types'
import type {
MemberActivity,
MemberWithStats,
MemberNameHistory,
HourlyActivity,
DailyActivity,
WeekdayActivity,
MonthlyActivity,
CatchphraseAnalysis,
MentionAnalysis,
LaughAnalysis,
ClusterGraphData,
ClusterGraphOptions,
RelationshipStats,
} from '@/types/analysis'
import type { LanguagePreferenceResult } from '@/types/quotes/languagePreference'
function buildFilterParams(filter?: TimeFilter): string {
if (!filter) return ''
const params = new URLSearchParams()
if (filter.startTs) params.set('startTs', String(filter.startTs))
if (filter.endTs) params.set('endTs', String(filter.endTs))
if (filter.memberId) params.set('memberId', String(filter.memberId))
const qs = params.toString()
return qs ? `?${qs}` : ''
}
async function get<T>(url: string): Promise<T> {
const res = await fetch(url)
if (!res.ok) {
const body = await res.text()
throw new Error(`HTTP ${res.status}: ${body}`)
}
return res.json() as Promise<T>
}
async function post<T>(url: string, body: unknown): Promise<T> {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) {
const text = await res.text()
throw new Error(`HTTP ${res.status}: ${text}`)
}
return res.json() as Promise<T>
}
async function patch<T>(url: string, body: unknown): Promise<T> {
const res = await fetch(url, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) {
const text = await res.text()
throw new Error(`HTTP ${res.status}: ${text}`)
}
return res.json() as Promise<T>
}
async function del<T>(url: string): Promise<T> {
const res = await fetch(url, { method: 'DELETE' })
if (!res.ok) {
const text = await res.text()
throw new Error(`HTTP ${res.status}: ${text}`)
}
return res.json() as Promise<T>
}
function notImplemented(method: string): never {
throw new Error(
`[FetchAdapter] ${method} is not yet implemented on the server side. ` +
'This feature requires additional backend query support.'
)
}
export class FetchAdapter implements QueryAdapter {
getCapabilities(): AdapterCapabilities {
return {
ai: false,
nlp: false,
fts: true,
nativeFileDialog: false,
osIntegration: false,
demoImport: false,
plugin: false,
migration: false,
merge: false,
incrementalImport: false,
}
}
// ==================== 会话管理 ====================
getSessions(): Promise<AnalysisSession[]> {
return get('/_web/sessions')
}
getSession(sessionId: string): Promise<AnalysisSession | null> {
return get(`/_web/sessions/${sessionId}`)
}
async deleteSession(sessionId: string): Promise<boolean> {
const result = await del<{ success: boolean }>(`/_web/sessions/${sessionId}`)
return result.success
}
async renameSession(sessionId: string, newName: string): Promise<boolean> {
const result = await patch<{ success: boolean }>(`/_web/sessions/${sessionId}/name`, {
name: newName,
})
return result.success
}
async updateSessionOwnerId(sessionId: string, ownerId: string | null): Promise<boolean> {
const result = await patch<{ success: boolean }>(`/_web/sessions/${sessionId}/owner`, {
ownerId,
})
return result.success
}
// ==================== 时间范围 ====================
getAvailableYears(sessionId: string): Promise<number[]> {
return get(`/_web/sessions/${sessionId}/years`)
}
getTimeRange(sessionId: string): Promise<{ start: number; end: number } | null> {
return get(`/_web/sessions/${sessionId}/time-range`)
}
// ==================== 统计分析 ====================
getMemberActivity(sessionId: string, filter?: TimeFilter): Promise<MemberActivity[]> {
return get(`/_web/sessions/${sessionId}/stats/member-activity${buildFilterParams(filter)}`)
}
getHourlyActivity(sessionId: string, filter?: TimeFilter): Promise<HourlyActivity[]> {
return get(`/_web/sessions/${sessionId}/stats/hourly${buildFilterParams(filter)}`)
}
getDailyActivity(sessionId: string, filter?: TimeFilter): Promise<DailyActivity[]> {
return get(`/_web/sessions/${sessionId}/stats/daily${buildFilterParams(filter)}`)
}
getWeekdayActivity(sessionId: string, filter?: TimeFilter): Promise<WeekdayActivity[]> {
return get(`/_web/sessions/${sessionId}/stats/weekday${buildFilterParams(filter)}`)
}
getMonthlyActivity(_sessionId: string, _filter?: TimeFilter): Promise<MonthlyActivity[]> {
notImplemented('getMonthlyActivity')
}
getYearlyActivity(_sessionId: string, _filter?: TimeFilter): Promise<Array<{ year: number; messageCount: number }>> {
notImplemented('getYearlyActivity')
}
getMessageLengthDistribution(_sessionId: string, _filter?: TimeFilter): Promise<MessageLengthDistribution> {
notImplemented('getMessageLengthDistribution')
}
getMessageTypeDistribution(
sessionId: string,
filter?: TimeFilter
): Promise<Array<{ type: MessageType; count: number }>> {
return get(`/_web/sessions/${sessionId}/stats/message-types${buildFilterParams(filter)}`)
}
// ==================== 成员管理 ====================
getMembers(sessionId: string): Promise<MemberWithStats[]> {
return get(`/_web/sessions/${sessionId}/members`)
}
getMembersPaginated(sessionId: string, params: PaginationParams): Promise<PaginatedResult<MemberWithStats>> {
const qs = new URLSearchParams({
page: String(params.page),
pageSize: String(params.pageSize),
})
if (params.search) qs.set('search', params.search)
if (params.sortOrder) qs.set('sortOrder', params.sortOrder)
return get(`/_web/sessions/${sessionId}/members/paginated?${qs}`)
}
getMemberNameHistory(sessionId: string, memberId: number): Promise<MemberNameHistory[]> {
return get(`/_web/sessions/${sessionId}/members/${memberId}/history`)
}
async updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): Promise<boolean> {
const result = await patch<{ success: boolean }>(`/_web/sessions/${sessionId}/members/${memberId}/aliases`, {
aliases,
})
return result.success
}
async mergeMembers(sessionId: string, memberId1: number, memberId2: number): Promise<boolean> {
const result = await post<{ success: boolean }>(`/_web/sessions/${sessionId}/members/merge`, {
memberId1,
memberId2,
})
return result.success
}
async deleteMember(sessionId: string, memberId: number): Promise<boolean> {
const result = await del<{ success: boolean }>(`/_web/sessions/${sessionId}/members/${memberId}`)
return result.success
}
// ==================== 社交分析(渐进实现) ====================
getCatchphraseAnalysis(_sessionId: string, _filter?: TimeFilter): Promise<CatchphraseAnalysis> {
notImplemented('getCatchphraseAnalysis')
}
getLanguagePreferenceAnalysis(
_sessionId: string,
_locale: string,
_filter?: TimeFilter,
_dictType?: string
): Promise<LanguagePreferenceResult> {
notImplemented('getLanguagePreferenceAnalysis')
}
getMentionAnalysis(_sessionId: string, _filter?: TimeFilter): Promise<MentionAnalysis> {
notImplemented('getMentionAnalysis')
}
getMentionGraph(_sessionId: string, _filter?: TimeFilter): Promise<MentionGraphData> {
notImplemented('getMentionGraph')
}
getClusterGraph(_sessionId: string, _filter?: TimeFilter, _options?: ClusterGraphOptions): Promise<ClusterGraphData> {
notImplemented('getClusterGraph')
}
getLaughAnalysis(_sessionId: string, _filter?: TimeFilter, _keywords?: string[]): Promise<LaughAnalysis> {
notImplemented('getLaughAnalysis')
}
getRelationshipStats(
_sessionId: string,
_filter?: TimeFilter,
_options?: { perseveranceThreshold?: number }
): Promise<RelationshipStats> {
notImplemented('getRelationshipStats')
}
// ==================== SQL Lab ====================
executeSQL(sessionId: string, sql: string): Promise<SQLResult> {
return post(`/_web/sessions/${sessionId}/sql`, { sql })
}
getSchema(sessionId: string): Promise<TableSchema[]> {
return get(`/_web/sessions/${sessionId}/schema`)
}
// ==================== 插件系统(不支持) ====================
pluginQuery<T = Record<string, unknown>>(_sessionId: string, _sql: string, _params?: unknown[]): Promise<T[]> {
notImplemented('pluginQuery')
}
pluginCompute<T = unknown>(_fnString: string, _input: unknown): Promise<T> {
notImplemented('pluginCompute')
}
}
+2 -2
View File
@@ -28,8 +28,8 @@ export async function initAdapter(): Promise<QueryAdapter> {
// Phase 3B: BrowserSqlAdapter
throw new Error('BrowserSqlAdapter is not yet implemented.')
} else {
// Phase 3A: FetchAdapter
throw new Error('FetchAdapter is not yet implemented.')
const { FetchAdapter } = await import('./fetch')
_adapter = new FetchAdapter()
}
return _adapter!