From 086bd882a72b990b222d976fdc7c321817305976 Mon Sep 17 00:00:00 2001 From: digua Date: Wed, 13 May 2026 19:53:01 +0800 Subject: [PATCH] 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). --- packages/core/src/index.ts | 1 + packages/core/src/query/basic-queries.ts | 11 + packages/core/src/query/index.ts | 16 +- packages/server/src/http/auth.ts | 3 + packages/server/src/http/index.ts | 3 + packages/server/src/http/routes/web.ts | 374 +++++++++++++++++++++++ src/adapters/fetch.ts | 288 +++++++++++++++++ src/adapters/index.ts | 4 +- 8 files changed, 685 insertions(+), 15 deletions(-) create mode 100644 packages/server/src/http/routes/web.ts create mode 100644 src/adapters/fetch.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3983d708..57594d3f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,6 +23,7 @@ export { getSessionMeta, getSessionOverview, getDatabaseSchema, + getTimeRange, getAvailableYears, getMemberActivity, getHourlyActivity, diff --git a/packages/core/src/query/basic-queries.ts b/packages/core/src/query/basic-queries.ts index 286b6084..69a43a20 100644 --- a/packages/core/src/query/basic-queries.ts +++ b/packages/core/src/query/basic-queries.ts @@ -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 } +} + /** * 获取可用的年份列表 */ diff --git a/packages/core/src/query/index.ts b/packages/core/src/query/index.ts index a408aa2d..9c72ae0f 100644 --- a/packages/core/src/query/index.ts +++ b/packages/core/src/query/index.ts @@ -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, diff --git a/packages/server/src/http/auth.ts b/packages/server/src/http/auth.ts index e2c1a0dc..603a0fc7 100644 --- a/packages/server/src/http/auth.ts +++ b/packages/server/src/http/auth.ts @@ -27,6 +27,9 @@ function safeTokenCompare(a: string, b: string): boolean { export async function authHook(request: FastifyRequest, reply: FastifyReply): Promise { 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() diff --git a/packages/server/src/http/index.ts b/packages/server/src/http/index.ts index d6f668af..aa7461ff 100644 --- a/packages/server/src/http/index.ts +++ b/packages/server/src/http/index.ts @@ -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 { export { createServer } from './server' export { registerSystemRoutes } from './routes/system' export { registerSessionRoutes } from './routes/sessions' +export { registerWebRoutes } from './routes/web' diff --git a/packages/server/src/http/routes/web.ts b/packages/server/src/http/routes/web.ts new file mode 100644 index 00000000..25d324e4 --- /dev/null +++ b/packages/server/src/http/routes/web.ts @@ -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): 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, + })), + } + }) + }) +} diff --git a/src/adapters/fetch.ts b/src/adapters/fetch.ts new file mode 100644 index 00000000..974446f9 --- /dev/null +++ b/src/adapters/fetch.ts @@ -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(url: string): Promise { + 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 +} + +async function post(url: string, body: unknown): Promise { + 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 +} + +async function patch(url: string, body: unknown): Promise { + 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 +} + +async function del(url: string): Promise { + 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 +} + +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 { + return get('/_web/sessions') + } + + getSession(sessionId: string): Promise { + return get(`/_web/sessions/${sessionId}`) + } + + async deleteSession(sessionId: string): Promise { + const result = await del<{ success: boolean }>(`/_web/sessions/${sessionId}`) + return result.success + } + + async renameSession(sessionId: string, newName: string): Promise { + const result = await patch<{ success: boolean }>(`/_web/sessions/${sessionId}/name`, { + name: newName, + }) + return result.success + } + + async updateSessionOwnerId(sessionId: string, ownerId: string | null): Promise { + const result = await patch<{ success: boolean }>(`/_web/sessions/${sessionId}/owner`, { + ownerId, + }) + return result.success + } + + // ==================== 时间范围 ==================== + + getAvailableYears(sessionId: string): Promise { + 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 { + return get(`/_web/sessions/${sessionId}/stats/member-activity${buildFilterParams(filter)}`) + } + + getHourlyActivity(sessionId: string, filter?: TimeFilter): Promise { + return get(`/_web/sessions/${sessionId}/stats/hourly${buildFilterParams(filter)}`) + } + + getDailyActivity(sessionId: string, filter?: TimeFilter): Promise { + return get(`/_web/sessions/${sessionId}/stats/daily${buildFilterParams(filter)}`) + } + + getWeekdayActivity(sessionId: string, filter?: TimeFilter): Promise { + return get(`/_web/sessions/${sessionId}/stats/weekday${buildFilterParams(filter)}`) + } + + getMonthlyActivity(_sessionId: string, _filter?: TimeFilter): Promise { + notImplemented('getMonthlyActivity') + } + + getYearlyActivity(_sessionId: string, _filter?: TimeFilter): Promise> { + notImplemented('getYearlyActivity') + } + + getMessageLengthDistribution(_sessionId: string, _filter?: TimeFilter): Promise { + notImplemented('getMessageLengthDistribution') + } + + getMessageTypeDistribution( + sessionId: string, + filter?: TimeFilter + ): Promise> { + return get(`/_web/sessions/${sessionId}/stats/message-types${buildFilterParams(filter)}`) + } + + // ==================== 成员管理 ==================== + + getMembers(sessionId: string): Promise { + return get(`/_web/sessions/${sessionId}/members`) + } + + getMembersPaginated(sessionId: string, params: PaginationParams): Promise> { + 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 { + return get(`/_web/sessions/${sessionId}/members/${memberId}/history`) + } + + async updateMemberAliases(sessionId: string, memberId: number, aliases: string[]): Promise { + 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 { + const result = await post<{ success: boolean }>(`/_web/sessions/${sessionId}/members/merge`, { + memberId1, + memberId2, + }) + return result.success + } + + async deleteMember(sessionId: string, memberId: number): Promise { + const result = await del<{ success: boolean }>(`/_web/sessions/${sessionId}/members/${memberId}`) + return result.success + } + + // ==================== 社交分析(渐进实现) ==================== + + getCatchphraseAnalysis(_sessionId: string, _filter?: TimeFilter): Promise { + notImplemented('getCatchphraseAnalysis') + } + + getLanguagePreferenceAnalysis( + _sessionId: string, + _locale: string, + _filter?: TimeFilter, + _dictType?: string + ): Promise { + notImplemented('getLanguagePreferenceAnalysis') + } + + getMentionAnalysis(_sessionId: string, _filter?: TimeFilter): Promise { + notImplemented('getMentionAnalysis') + } + + getMentionGraph(_sessionId: string, _filter?: TimeFilter): Promise { + notImplemented('getMentionGraph') + } + + getClusterGraph(_sessionId: string, _filter?: TimeFilter, _options?: ClusterGraphOptions): Promise { + notImplemented('getClusterGraph') + } + + getLaughAnalysis(_sessionId: string, _filter?: TimeFilter, _keywords?: string[]): Promise { + notImplemented('getLaughAnalysis') + } + + getRelationshipStats( + _sessionId: string, + _filter?: TimeFilter, + _options?: { perseveranceThreshold?: number } + ): Promise { + notImplemented('getRelationshipStats') + } + + // ==================== SQL Lab ==================== + + executeSQL(sessionId: string, sql: string): Promise { + return post(`/_web/sessions/${sessionId}/sql`, { sql }) + } + + getSchema(sessionId: string): Promise { + return get(`/_web/sessions/${sessionId}/schema`) + } + + // ==================== 插件系统(不支持) ==================== + + pluginQuery>(_sessionId: string, _sql: string, _params?: unknown[]): Promise { + notImplemented('pluginQuery') + } + + pluginCompute(_fnString: string, _input: unknown): Promise { + notImplemented('pluginCompute') + } +} diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 85030f97..72cc3145 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -28,8 +28,8 @@ export async function initAdapter(): Promise { // 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!