diff --git a/apps/desktop/main/worker/dbWorker.ts b/apps/desktop/main/worker/dbWorker.ts index 9eca39b5..62319b64 100644 --- a/apps/desktop/main/worker/dbWorker.ts +++ b/apps/desktop/main/worker/dbWorker.ts @@ -38,6 +38,7 @@ import { getLanguagePreferenceAnalysis, getMentionAnalysis, getMentionGraph, + getGroupRelationshipGalaxy, getLaughAnalysis, getClusterGraph, getRelationshipStats, @@ -211,6 +212,7 @@ const syncHandlers: Record any> = { getLanguagePreferenceAnalysis: (p) => getLanguagePreferenceAnalysis(p), getMentionAnalysis: (p) => getMentionAnalysis(p.sessionId, p.filter), getMentionGraph: (p) => getMentionGraph(p.sessionId, p.filter), + getGroupRelationshipGalaxy: (p) => getGroupRelationshipGalaxy(p.sessionId, p.filter), getLaughAnalysis: (p) => getLaughAnalysis(p.sessionId, p.filter, p.keywords), getClusterGraph: (p) => getClusterGraph(p.sessionId, p.filter, p.options), getRelationshipStats: (p) => getRelationshipStats(p.sessionId, p.filter, p.options), diff --git a/apps/desktop/main/worker/query/advanced/index.ts b/apps/desktop/main/worker/query/advanced/index.ts index d5c677e5..86d3ef6d 100644 --- a/apps/desktop/main/worker/query/advanced/index.ts +++ b/apps/desktop/main/worker/query/advanced/index.ts @@ -5,7 +5,13 @@ export { getCatchphraseAnalysis } from './repeat' -export { getMentionAnalysis, getMentionGraph, getLaughAnalysis, getClusterGraph } from './social' +export { + getMentionAnalysis, + getMentionGraph, + getGroupRelationshipGalaxy, + getLaughAnalysis, + getClusterGraph, +} from './social' export type { MentionGraphData, MentionGraphNode, diff --git a/apps/desktop/main/worker/query/advanced/social.ts b/apps/desktop/main/worker/query/advanced/social.ts index b04178f2..5a8069fa 100644 --- a/apps/desktop/main/worker/query/advanced/social.ts +++ b/apps/desktop/main/worker/query/advanced/social.ts @@ -6,9 +6,12 @@ import { openDatabaseAdapter, type TimeFilter } from '../../core' import { getMentionAnalysis as coreGetMentionAnalysis, getMentionGraph as coreGetMentionGraph, + getGroupRelationshipGalaxy as coreGetGroupRelationshipGalaxy, + GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION, getLaughAnalysis as coreGetLaughAnalysis, getClusterGraph as coreGetClusterGraph, } from '@openchatlab/core' +import type { GroupRelationshipGalaxyData } from '@openchatlab/shared-types' import type { MentionGraphData, MentionGraphNode, @@ -41,6 +44,20 @@ export function getMentionGraph(sessionId: string, filter?: TimeFilter): Mention return coreGetMentionGraph(db, filter) } +export function getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter): GroupRelationshipGalaxyData { + const db = openDatabaseAdapter(sessionId) + if (!db) { + return { + graph: { nodes: [], edges: [], communities: [] }, + members: [], + edges: [], + stats: { totalMembers: 0, activeMembers: 0, displayedMembers: 0, displayedEdges: 0, communityCount: 0 }, + algorithmVersion: GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION, + } + } + return coreGetGroupRelationshipGalaxy(db, filter) +} + export function getLaughAnalysis(sessionId: string, filter?: TimeFilter, keywords?: string[]): any { const db = openDatabaseAdapter(sessionId) if (!db) diff --git a/apps/desktop/main/worker/query/index.ts b/apps/desktop/main/worker/query/index.ts index 04668302..c5c92f85 100644 --- a/apps/desktop/main/worker/query/index.ts +++ b/apps/desktop/main/worker/query/index.ts @@ -36,6 +36,7 @@ export { getLanguagePreferenceAnalysis, getMentionAnalysis, getMentionGraph, + getGroupRelationshipGalaxy, getLaughAnalysis, getClusterGraph, getRelationshipStats, diff --git a/apps/desktop/main/worker/workerManager.ts b/apps/desktop/main/worker/workerManager.ts index 4c4ee74a..48eab680 100644 --- a/apps/desktop/main/worker/workerManager.ts +++ b/apps/desktop/main/worker/workerManager.ts @@ -406,6 +406,10 @@ export async function getMentionGraph(sessionId: string, filter?: any): Promise< return sendToWorker('getMentionGraph', { sessionId, filter }) } +export async function getGroupRelationshipGalaxy(sessionId: string, filter?: any): Promise { + return sendToWorker('getGroupRelationshipGalaxy', { sessionId, filter }) +} + export async function getLaughAnalysis(sessionId: string, filter?: any, keywords?: string[]): Promise { return sendToWorker('getLaughAnalysis', { sessionId, filter, keywords }) } diff --git a/apps/desktop/main/worker/workerTimeoutPolicy.test.ts b/apps/desktop/main/worker/workerTimeoutPolicy.test.ts index ed4e6c09..293c4ecb 100644 --- a/apps/desktop/main/worker/workerTimeoutPolicy.test.ts +++ b/apps/desktop/main/worker/workerTimeoutPolicy.test.ts @@ -8,6 +8,7 @@ describe('worker timeout policy', () => { assert.equal(isRestartableReadOnlyRequestType('executeRawSQL'), true) assert.equal(isRestartableReadOnlyRequestType('getSchema'), true) assert.equal(isRestartableReadOnlyRequestType('getChatOverview'), true) + assert.equal(isRestartableReadOnlyRequestType('getGroupRelationshipGalaxy'), true) assert.equal(isRestartableReadOnlyRequestType('searchMessages'), true) assert.equal(isRestartableReadOnlyRequestType('getSegmentSummaries'), true) assert.equal(isRestartableReadOnlyRequestType('analyzePushImport'), true) diff --git a/apps/desktop/main/worker/workerTimeoutPolicy.ts b/apps/desktop/main/worker/workerTimeoutPolicy.ts index b2732d90..3b72a03b 100644 --- a/apps/desktop/main/worker/workerTimeoutPolicy.ts +++ b/apps/desktop/main/worker/workerTimeoutPolicy.ts @@ -17,6 +17,7 @@ const RESTARTABLE_READ_ONLY_REQUEST_TYPES = new Set([ 'getLanguagePreferenceAnalysis', 'getMentionAnalysis', 'getMentionGraph', + 'getGroupRelationshipGalaxy', 'getLaughAnalysis', 'getClusterGraph', 'getRelationshipStats', diff --git a/packages/http-routes/src/routes/web/analytics.test.ts b/packages/http-routes/src/routes/web/analytics.test.ts index 3503eaff..f9b1d9a0 100644 --- a/packages/http-routes/src/routes/web/analytics.test.ts +++ b/packages/http-routes/src/routes/web/analytics.test.ts @@ -62,6 +62,7 @@ const SESSION_ID = 'chat-1' const MEMBER_ACTIVITY_URL = `/_web/sessions/${SESSION_ID}/stats/member-activity` const JOURNEY_URL = `/_web/sessions/${SESSION_ID}/analytics/journey` const KEYWORD_URL = `/_web/sessions/${SESSION_ID}/analytics/laugh` +const RELATIONSHIP_GALAXY_URL = `/_web/sessions/${SESSION_ID}/analytics/relationship-galaxy` const nativeBinding = path.resolve('apps/cli/native/better_sqlite3.node') describe('analytics routes', () => { @@ -76,8 +77,27 @@ describe('analytics routes', () => { dbFile = path.join(root, `${SESSION_ID}.db`) raw = new Database(dbFile, { nativeBinding }) raw.exec(` - CREATE TABLE member (id INTEGER PRIMARY KEY, platform_id TEXT, account_name TEXT, group_nickname TEXT, avatar TEXT); - CREATE TABLE message (id INTEGER PRIMARY KEY, sender_id INTEGER, ts INTEGER, type INTEGER, content TEXT, platform_message_id TEXT); + CREATE TABLE meta (name TEXT, platform TEXT, type TEXT, imported_at INTEGER, owner_id TEXT); + CREATE TABLE member ( + id INTEGER PRIMARY KEY, + platform_id TEXT, + account_name TEXT, + group_nickname TEXT, + aliases TEXT DEFAULT '[]', + avatar TEXT + ); + CREATE TABLE member_name_history (member_id INTEGER, name TEXT); + CREATE TABLE message ( + id INTEGER PRIMARY KEY, + sender_id INTEGER, + ts INTEGER, + type INTEGER, + content TEXT, + platform_message_id TEXT, + reply_to_message_id TEXT + ); + INSERT INTO meta (name, platform, type, imported_at, owner_id) + VALUES ('Group', 'wechat', 'group', 100, 'alice'); INSERT INTO member (id, platform_id, account_name) VALUES (1, 'alice', 'Alice'), (2, 'bob', 'Bob'); INSERT INTO message (id, sender_id, ts, type, content, platform_message_id) VALUES (1, 1, 100, 0, 'a', 'm-1'), (2, 2, 200, 0, 'b', 'm-2'), (3, 1, 300, 0, 'c', 'm-3'); @@ -207,4 +227,24 @@ describe('analytics routes', () => { const differentRange = await app.inject({ method: 'GET', url: `${JOURNEY_URL}?startTs=200&endTs=300` }) assert.equal(differentRange.statusCode, 500) }) + + it('serves the shared group relationship galaxy and keys its cache by time range', async () => { + const first = await app.inject({ method: 'GET', url: `${RELATIONSHIP_GALAXY_URL}?startTs=100&endTs=300` }) + assert.equal(first.statusCode, 200) + assert.equal(first.json().algorithmVersion, 'group-relationship-galaxy-v1') + assert.equal(first.json().graph.nodes.length, 2) + assert.ok(first.json().graph.edges.length > 0) + + raw.close() + + const same = await app.inject({ method: 'GET', url: `${RELATIONSHIP_GALAXY_URL}?startTs=100&endTs=300` }) + assert.equal(same.statusCode, 200) + assert.deepEqual(same.json(), first.json()) + + const differentRange = await app.inject({ + method: 'GET', + url: `${RELATIONSHIP_GALAXY_URL}?startTs=200&endTs=300`, + }) + assert.equal(differentRange.statusCode, 500) + }) }) diff --git a/packages/http-routes/src/routes/web/analytics.ts b/packages/http-routes/src/routes/web/analytics.ts index d78c2452..7dfe7cc9 100644 --- a/packages/http-routes/src/routes/web/analytics.ts +++ b/packages/http-routes/src/routes/web/analytics.ts @@ -24,6 +24,8 @@ import { getCatchphraseAnalysis, getMentionAnalysis, getMentionGraph, + getGroupRelationshipGalaxy, + GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION, getLaughAnalysis, getClusterGraph, getLanguagePreferenceAnalysis, @@ -158,6 +160,21 @@ export function registerAnalyticsRoutes(server: FastifyInstance, ctx: AnalyticsR } ) + server.get<{ Params: { id: string }; Querystring: FilteredQuery }>( + '/_web/sessions/:id/analytics/relationship-galaxy', + async (request) => { + const id = request.params.id + const filter = parseTimeFilter(request.query) + return cached( + 'relationship-galaxy', + id, + { ...filter }, + () => getGroupRelationshipGalaxy(adapter.ensureReadonly(id), filter), + { extraVersion: GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION } + ) + } + ) + server.post<{ Params: { id: string }; Querystring: FilteredQuery; Body: KeywordAnalysisBody }>( '/_web/sessions/:id/analytics/laugh', async (request) => { diff --git a/packages/web-runtime/src/import/session-runtime.test.ts b/packages/web-runtime/src/import/session-runtime.test.ts index df61c917..c83f06d0 100644 --- a/packages/web-runtime/src/import/session-runtime.test.ts +++ b/packages/web-runtime/src/import/session-runtime.test.ts @@ -1044,30 +1044,43 @@ describe('BrowserSessionRuntime', () => { await runtime.importSource(source('insight-query.json', fixture), { formatId: 'chatlab' }) - const [monthly, yearly, memberTrend, members, mentions, mentionGraph, clusterGraph, language, wordFrequency] = - await Promise.all([ - runtime.getMonthlyActivity('insight-query-session'), - runtime.getYearlyActivity('insight-query-session'), - runtime.getMemberMonthlyTrend('insight-query-session'), - runtime.getMembers('insight-query-session'), - runtime.getMentionAnalysis('insight-query-session'), - runtime.getMentionGraph('insight-query-session'), - runtime.getClusterGraph('insight-query-session'), - runtime.getLanguagePreferenceAnalysis('insight-query-session', 'en-US'), - runtime.getWordFrequency('insight-query-session', { - locale: 'en-US', - topN: 10, - minCount: 2, - posFilterMode: 'all', - enableStopwords: false, - }), - ]) + const [ + monthly, + yearly, + memberTrend, + members, + mentions, + mentionGraph, + relationshipGalaxy, + clusterGraph, + language, + wordFrequency, + ] = await Promise.all([ + runtime.getMonthlyActivity('insight-query-session'), + runtime.getYearlyActivity('insight-query-session'), + runtime.getMemberMonthlyTrend('insight-query-session'), + runtime.getMembers('insight-query-session'), + runtime.getMentionAnalysis('insight-query-session'), + runtime.getMentionGraph('insight-query-session'), + runtime.getGroupRelationshipGalaxy('insight-query-session'), + runtime.getClusterGraph('insight-query-session'), + runtime.getLanguagePreferenceAnalysis('insight-query-session', 'en-US'), + runtime.getWordFrequency('insight-query-session', { + locale: 'en-US', + topN: 10, + minCount: 2, + posFilterMode: 'all', + enableStopwords: false, + }), + ]) assert.ok(monthly.length > 0) assert.ok(yearly.length > 0) assert.ok(memberTrend.length > 0) assert.ok(members.length > 0) assert.ok(mentions.totalMentions > 0) assert.ok(mentionGraph.links.length > 0) + assert.ok(relationshipGalaxy.graph.edges.length > 0) + assert.equal(relationshipGalaxy.algorithmVersion, 'group-relationship-galaxy-v1') assert.ok(clusterGraph.links.length > 0) assert.ok(language.members.length > 0) assert.ok(wordFrequency.words.length > 0) diff --git a/packages/web-runtime/src/import/session-runtime.ts b/packages/web-runtime/src/import/session-runtime.ts index 6ef71b97..f2d4acf7 100644 --- a/packages/web-runtime/src/import/session-runtime.ts +++ b/packages/web-runtime/src/import/session-runtime.ts @@ -9,6 +9,7 @@ import { getMembersWithAliases as queryMembersWithAliases, getMentionAnalysis as queryMentionAnalysis, getMentionGraph as queryMentionGraph, + getGroupRelationshipGalaxy as queryGroupRelationshipGalaxy, getAvailableYears as queryAvailableYears, getDailyActivity as queryDailyActivity, getHourlyActivity as queryHourlyActivity, @@ -47,6 +48,7 @@ import { type WordFrequencyResult, type YearlyActivity, } from '@openchatlab/core' +import type { GroupRelationshipGalaxyData } from '@openchatlab/shared-types' import { PARSER_FORMAT_IDS } from '@openchatlab/parser/browser' import { WebRuntimeError } from '../runtime-error' import type { WorkspaceDatabasePort, WorkspaceDatabaseStage } from '../storage/workspace-database' @@ -425,6 +427,10 @@ export class BrowserSessionRuntime { return this.withSessionDatabase(id, (db) => queryMentionGraph(db, filter)) } + async getGroupRelationshipGalaxy(id: string, filter?: BrowserTimeFilter): Promise { + return this.withSessionDatabase(id, (db) => queryGroupRelationshipGalaxy(db, filter)) + } + async getClusterGraph( id: string, filter?: BrowserTimeFilter, diff --git a/packages/web-runtime/src/rpc/protocol.ts b/packages/web-runtime/src/rpc/protocol.ts index 33791c68..5bc35dbd 100644 --- a/packages/web-runtime/src/rpc/protocol.ts +++ b/packages/web-runtime/src/rpc/protocol.ts @@ -30,6 +30,7 @@ import type { WordFrequencyResult, YearlyActivity, } from '@openchatlab/core' +import type { GroupRelationshipGalaxyData } from '@openchatlab/shared-types' import type { BrowserImportFormatInfo, BrowserMultiChatEntry, @@ -231,6 +232,10 @@ export interface WebRuntimeTaskMap { payload: { sessionId: string; filter?: BrowserTimeFilter } result: MentionGraphData } + 'analysis.groupRelationshipGalaxy': { + payload: { sessionId: string; filter?: BrowserTimeFilter } + result: GroupRelationshipGalaxyData + } 'analysis.clusterGraph': { payload: { sessionId: string; filter?: BrowserTimeFilter; options?: ClusterGraphOptions } result: ClusterGraphData @@ -381,6 +386,7 @@ const WEB_RUNTIME_TASK_TYPES: Record = { 'analysis.memberList': true, 'analysis.mentions': true, 'analysis.mentionGraph': true, + 'analysis.groupRelationshipGalaxy': true, 'analysis.clusterGraph': true, 'analysis.relationship': true, 'analysis.journey': true, diff --git a/packages/web-runtime/src/worker/controller.test.ts b/packages/web-runtime/src/worker/controller.test.ts index 55fb4c59..71e7e19b 100644 --- a/packages/web-runtime/src/worker/controller.test.ts +++ b/packages/web-runtime/src/worker/controller.test.ts @@ -274,6 +274,22 @@ class FakeSessionRuntime implements WorkerSessionRuntime { return { nodes: [], links: [], maxLinkValue: 0 } } + async getGroupRelationshipGalaxy(_id: string, _filter?: BrowserTimeFilter) { + return { + graph: { nodes: [], edges: [], communities: [] }, + members: [], + edges: [], + stats: { + totalMembers: 0, + activeMembers: 0, + displayedMembers: 0, + displayedEdges: 0, + communityCount: 0, + }, + algorithmVersion: 'group-relationship-galaxy-v1', + } + } + async getClusterGraph(_id: string, _filter?: BrowserTimeFilter) { return { nodes: [], @@ -804,6 +820,11 @@ describe('WebRuntimeWorkerController', () => { { id: 'members', type: 'analysis.memberList', payload: { sessionId: 'session-one' } }, { id: 'mentions', type: 'analysis.mentions', payload: { sessionId: 'session-one' } }, { id: 'mention-graph', type: 'analysis.mentionGraph', payload: { sessionId: 'session-one' } }, + { + id: 'relationship-galaxy', + type: 'analysis.groupRelationshipGalaxy', + payload: { sessionId: 'session-one' }, + }, { id: 'cluster', type: 'analysis.clusterGraph', payload: { sessionId: 'session-one' } }, { id: 'relationship', type: 'analysis.relationship', payload: { sessionId: 'session-one' } }, { id: 'journey', type: 'analysis.journey', payload: { sessionId: 'session-one' } }, diff --git a/packages/web-runtime/src/worker/controller.ts b/packages/web-runtime/src/worker/controller.ts index 70e87244..7d8ad064 100644 --- a/packages/web-runtime/src/worker/controller.ts +++ b/packages/web-runtime/src/worker/controller.ts @@ -53,6 +53,7 @@ export type WorkerSessionRuntime = Pick< | 'getMembers' | 'getMentionAnalysis' | 'getMentionGraph' + | 'getGroupRelationshipGalaxy' | 'getClusterGraph' | 'getRelationshipStats' | 'getJourneyStats' @@ -500,6 +501,15 @@ export class WebRuntimeWorkerController { this.emitAnalysisQueryCompleted(request, startedAt) return result } + case 'analysis.groupRelationshipGalaxy': { + const startedAt = this.startAnalysisQuery() + const result = await this.sessionRuntime.getGroupRelationshipGalaxy( + request.payload.sessionId, + request.payload.filter + ) + this.emitAnalysisQueryCompleted(request, startedAt) + return result + } case 'analysis.clusterGraph': { const startedAt = this.startAnalysisQuery() const result = await this.sessionRuntime.getClusterGraph( diff --git a/src/services/data/browser.test.ts b/src/services/data/browser.test.ts index f3d559c4..0646100e 100644 --- a/src/services/data/browser.test.ts +++ b/src/services/data/browser.test.ts @@ -201,6 +201,19 @@ describe('BrowserDataAdapter', () => { ], 'analysis.mentions': { topMentioners: [], topMentioned: [], totalMentions: 0 }, 'analysis.mentionGraph': { nodes: [], links: [], maxLinkValue: 0 }, + 'analysis.groupRelationshipGalaxy': { + graph: { nodes: [], edges: [], communities: [] }, + members: [], + edges: [], + stats: { + totalMembers: 0, + activeMembers: 0, + displayedMembers: 0, + displayedEdges: 0, + communityCount: 0, + }, + algorithmVersion: 'group-relationship-galaxy-v1', + }, 'analysis.clusterGraph': { nodes: [], links: [], @@ -225,6 +238,7 @@ describe('BrowserDataAdapter', () => { assert.equal((await adapter.getMembers('session-one'))[0].accountName, 'Alice') assert.equal((await adapter.getMentionAnalysis('session-one')).totalMentions, 0) assert.equal((await adapter.getMentionGraph('session-one')).links.length, 0) + assert.equal((await adapter.getGroupRelationshipGalaxy('session-one')).stats.displayedEdges, 0) assert.equal((await adapter.getClusterGraph('session-one')).stats.edgeCount, 0) assert.equal((await adapter.getRelationshipStats('session-one')).hasSessionIndex, true) assert.equal((await adapter.getJourneyStats('session-one')).hasSessionIndex, true) @@ -240,6 +254,7 @@ describe('BrowserDataAdapter', () => { 'analysis.memberList', 'analysis.mentions', 'analysis.mentionGraph', + 'analysis.groupRelationshipGalaxy', 'analysis.clusterGraph', 'analysis.relationship', 'analysis.journey', diff --git a/src/services/data/browser.ts b/src/services/data/browser.ts index bc313c83..f2802248 100644 --- a/src/services/data/browser.ts +++ b/src/services/data/browser.ts @@ -48,6 +48,7 @@ type BrowserSessionDataAdapter = Pick< | 'getMembers' | 'getMentionAnalysis' | 'getMentionGraph' + | 'getGroupRelationshipGalaxy' | 'getClusterGraph' | 'getRelationshipStats' | 'getJourneyStats' @@ -156,6 +157,10 @@ export class BrowserDataAdapter implements BrowserSessionDataAdapter { return this.requestAnalysis('analysis.mentionGraph', { sessionId, filter }) } + getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter) { + return this.requestAnalysis('analysis.groupRelationshipGalaxy', { sessionId, filter }) + } + getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise { return this.requestAnalysis('analysis.clusterGraph', { sessionId, filter, options }) } diff --git a/src/services/data/fetch.ts b/src/services/data/fetch.ts index bb53fd36..d40ff99c 100644 --- a/src/services/data/fetch.ts +++ b/src/services/data/fetch.ts @@ -14,6 +14,7 @@ import type { PeopleRelationshipsGraphResponse, PeopleRelationshipsNeighborhoodResponse, AnnualSummaryResponse, + GroupRelationshipGalaxyData, } from '@openchatlab/shared-types' import type { MemberActivity, @@ -396,6 +397,10 @@ export class FetchDataAdapter implements DataAdapter { return analyticsGet(`/sessions/${sessionId}/analytics/mention-graph${buildFilterParams(filter)}`) } + getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter): Promise { + return analyticsGet(`/sessions/${sessionId}/analytics/relationship-galaxy${buildFilterParams(filter)}`) + } + getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise { const params = new URLSearchParams() if (filter?.startTs) params.set('startTs', String(filter.startTs)) diff --git a/src/services/data/types.ts b/src/services/data/types.ts index 733278f6..f4f2b14f 100644 --- a/src/services/data/types.ts +++ b/src/services/data/types.ts @@ -19,6 +19,7 @@ import type { PeopleRelationshipsNeighborhoodResponse, AnnualSummaryMode, AnnualSummaryResponse, + GroupRelationshipGalaxyData, } from '@openchatlab/shared-types' import type { JourneyStats, WordFrequencyParams, WordFrequencyResult } from '@openchatlab/core' import type { @@ -235,6 +236,7 @@ export interface DataAdapter { ): Promise getMentionAnalysis(sessionId: string, filter?: TimeFilter): Promise getMentionGraph(sessionId: string, filter?: TimeFilter): Promise + getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter): Promise getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise getLaughAnalysis(sessionId: string, filter?: TimeFilter, keywords?: string[]): Promise getRelationshipStats(