mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
refactor(insight): remove legacy mention graph
This commit is contained in:
1 parent
2f5f97f0bf
commit
cd2a401c3b
25 files changed
+61
-703
No files matched your search
@@ -37,7 +37,6 @@ import {
|
||||
getCatchphraseAnalysis,
|
||||
getLanguagePreferenceAnalysis,
|
||||
getMentionAnalysis,
|
||||
getMentionGraph,
|
||||
getGroupRelationshipGalaxy,
|
||||
getLaughAnalysis,
|
||||
getClusterGraph,
|
||||
@@ -126,7 +125,6 @@ const CACHEABLE_QUERIES = new Set([
|
||||
'getCatchphraseAnalysis',
|
||||
'getLanguagePreferenceAnalysis',
|
||||
'getMentionAnalysis',
|
||||
'getMentionGraph',
|
||||
'getLaughAnalysis',
|
||||
'getClusterGraph',
|
||||
'getWordFrequency',
|
||||
@@ -211,7 +209,6 @@ const syncHandlers: Record<string, (payload: any) => any> = {
|
||||
getCatchphraseAnalysis: (p) => getCatchphraseAnalysis(p.sessionId, p.filter),
|
||||
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),
|
||||
|
||||
@@ -5,22 +5,8 @@
|
||||
|
||||
export { getCatchphraseAnalysis } from './repeat'
|
||||
|
||||
export {
|
||||
getMentionAnalysis,
|
||||
getMentionGraph,
|
||||
getGroupRelationshipGalaxy,
|
||||
getLaughAnalysis,
|
||||
getClusterGraph,
|
||||
} from './social'
|
||||
export type {
|
||||
MentionGraphData,
|
||||
MentionGraphNode,
|
||||
MentionGraphLink,
|
||||
ClusterGraphData,
|
||||
ClusterGraphNode,
|
||||
ClusterGraphLink,
|
||||
ClusterGraphOptions,
|
||||
} from './social'
|
||||
export { getMentionAnalysis, getGroupRelationshipGalaxy, getLaughAnalysis, getClusterGraph } from './social'
|
||||
export type { ClusterGraphData, ClusterGraphNode, ClusterGraphLink, ClusterGraphOptions } from './social'
|
||||
|
||||
export { getLanguagePreferenceAnalysis } from './languagePreference'
|
||||
|
||||
|
||||
@@ -5,32 +5,15 @@
|
||||
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,
|
||||
MentionGraphLink,
|
||||
ClusterGraphData,
|
||||
ClusterGraphNode,
|
||||
ClusterGraphLink,
|
||||
ClusterGraphOptions,
|
||||
} from '@openchatlab/core'
|
||||
import type { ClusterGraphData, ClusterGraphNode, ClusterGraphLink, ClusterGraphOptions } from '@openchatlab/core'
|
||||
|
||||
export type {
|
||||
MentionGraphData,
|
||||
MentionGraphNode,
|
||||
MentionGraphLink,
|
||||
ClusterGraphData,
|
||||
ClusterGraphNode,
|
||||
ClusterGraphLink,
|
||||
ClusterGraphOptions,
|
||||
}
|
||||
export type { ClusterGraphData, ClusterGraphNode, ClusterGraphLink, ClusterGraphOptions }
|
||||
|
||||
export function getMentionAnalysis(sessionId: string, filter?: TimeFilter): any {
|
||||
const db = openDatabaseAdapter(sessionId)
|
||||
@@ -38,12 +21,6 @@ export function getMentionAnalysis(sessionId: string, filter?: TimeFilter): any
|
||||
return coreGetMentionAnalysis(db, filter)
|
||||
}
|
||||
|
||||
export function getMentionGraph(sessionId: string, filter?: TimeFilter): MentionGraphData {
|
||||
const db = openDatabaseAdapter(sessionId)
|
||||
if (!db) return { nodes: [], links: [], maxLinkValue: 0 }
|
||||
return coreGetMentionGraph(db, filter)
|
||||
}
|
||||
|
||||
export function getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter): GroupRelationshipGalaxyData {
|
||||
const db = openDatabaseAdapter(sessionId)
|
||||
if (!db) {
|
||||
|
||||
@@ -35,7 +35,6 @@ export {
|
||||
getCatchphraseAnalysis,
|
||||
getLanguagePreferenceAnalysis,
|
||||
getMentionAnalysis,
|
||||
getMentionGraph,
|
||||
getGroupRelationshipGalaxy,
|
||||
getLaughAnalysis,
|
||||
getClusterGraph,
|
||||
|
||||
@@ -402,10 +402,6 @@ export async function getMentionAnalysis(sessionId: string, filter?: any): Promi
|
||||
return sendToWorker('getMentionAnalysis', { sessionId, filter })
|
||||
}
|
||||
|
||||
export async function getMentionGraph(sessionId: string, filter?: any): Promise<any> {
|
||||
return sendToWorker('getMentionGraph', { sessionId, filter })
|
||||
}
|
||||
|
||||
export async function getGroupRelationshipGalaxy(sessionId: string, filter?: any): Promise<any> {
|
||||
return sendToWorker('getGroupRelationshipGalaxy', { sessionId, filter })
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ const RESTARTABLE_READ_ONLY_REQUEST_TYPES = new Set([
|
||||
'getCatchphraseAnalysis',
|
||||
'getLanguagePreferenceAnalysis',
|
||||
'getMentionAnalysis',
|
||||
'getMentionGraph',
|
||||
'getGroupRelationshipGalaxy',
|
||||
'getLaughAnalysis',
|
||||
'getClusterGraph',
|
||||
|
||||
@@ -94,7 +94,6 @@ export {
|
||||
executeParameterizedSql,
|
||||
getCatchphraseAnalysis,
|
||||
getMentionAnalysis,
|
||||
getMentionGraph,
|
||||
getGroupRelationshipGalaxy,
|
||||
GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION,
|
||||
getLaughAnalysis,
|
||||
@@ -190,9 +189,6 @@ export type {
|
||||
CatchphraseAnalysis,
|
||||
MemberCatchphrase,
|
||||
CatchphraseItem,
|
||||
MentionGraphData,
|
||||
MentionGraphNode,
|
||||
MentionGraphLink,
|
||||
ClusterGraphData,
|
||||
ClusterGraphNode,
|
||||
ClusterGraphLink,
|
||||
|
||||
@@ -176,6 +176,42 @@ describe('group relationship galaxy', () => {
|
||||
assert.deepEqual(first, second)
|
||||
})
|
||||
|
||||
it('keeps a large group within the default interactive graph limits', () => {
|
||||
raw.exec(`
|
||||
WITH RECURSIVE members(id) AS (
|
||||
SELECT 100
|
||||
UNION ALL
|
||||
SELECT id + 1 FROM members WHERE id < 549
|
||||
)
|
||||
INSERT INTO member (id, platform_id, account_name, aliases)
|
||||
SELECT id, 'large-' || id, 'LargeMember' || id, '[]' FROM members;
|
||||
|
||||
WITH RECURSIVE messages(id) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT id + 1 FROM messages WHERE id < 9000
|
||||
)
|
||||
INSERT INTO message (id, sender_id, ts, type, content, platform_message_id, reply_to_message_id)
|
||||
SELECT
|
||||
id,
|
||||
100 + ((id - 1) % 450),
|
||||
100000 + id,
|
||||
0,
|
||||
'large group message',
|
||||
'large-' || id,
|
||||
CASE WHEN id > 1 AND id % 37 = 0 THEN 'large-' || (id - 1) ELSE NULL END
|
||||
FROM messages;
|
||||
`)
|
||||
|
||||
const first = getGroupRelationshipGalaxy(db)
|
||||
const second = getGroupRelationshipGalaxy(db)
|
||||
|
||||
assert.equal(first.stats.activeMembers, 450)
|
||||
assert.ok(first.stats.displayedMembers <= 400)
|
||||
assert.ok(first.stats.displayedEdges <= 1200)
|
||||
assert.deepEqual(first, second)
|
||||
})
|
||||
|
||||
it('returns an explicit empty graph when the range has fewer than two active members', () => {
|
||||
insertMessage(1, 1, 100, 'only owner', 'm1')
|
||||
|
||||
|
||||
@@ -7,16 +7,8 @@
|
||||
export { getCatchphraseAnalysis } from './repeat'
|
||||
export type { CatchphraseAnalysis, MemberCatchphrase, CatchphraseItem } from './repeat'
|
||||
|
||||
export { getMentionAnalysis, getMentionGraph, getLaughAnalysis, getClusterGraph } from './social'
|
||||
export type {
|
||||
MentionGraphData,
|
||||
MentionGraphNode,
|
||||
MentionGraphLink,
|
||||
ClusterGraphData,
|
||||
ClusterGraphNode,
|
||||
ClusterGraphLink,
|
||||
ClusterGraphOptions,
|
||||
} from './social'
|
||||
export { getMentionAnalysis, getLaughAnalysis, getClusterGraph } from './social'
|
||||
export type { ClusterGraphData, ClusterGraphNode, ClusterGraphLink, ClusterGraphOptions } from './social'
|
||||
|
||||
export { getGroupRelationshipGalaxy, GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION } from './group-relationship-galaxy'
|
||||
export type { GroupRelationshipGalaxyOptions } from './group-relationship-galaxy'
|
||||
|
||||
@@ -112,123 +112,6 @@ export function getMentionAnalysis(db: DatabaseAdapter, filter?: TimeFilter): an
|
||||
return { topMentioners, topMentioned, totalMentions }
|
||||
}
|
||||
|
||||
// ==================== @ 互动关系图数据 ====================
|
||||
|
||||
export interface MentionGraphNode {
|
||||
id: number
|
||||
name: string
|
||||
value: number
|
||||
symbolSize: number
|
||||
}
|
||||
|
||||
export interface MentionGraphLink {
|
||||
source: string
|
||||
target: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface MentionGraphData {
|
||||
nodes: MentionGraphNode[]
|
||||
links: MentionGraphLink[]
|
||||
maxLinkValue: number
|
||||
}
|
||||
|
||||
export function getMentionGraph(db: DatabaseAdapter, filter?: TimeFilter): MentionGraphData {
|
||||
const emptyResult: MentionGraphData = { nodes: [], links: [], maxLinkValue: 0 }
|
||||
|
||||
const { clause, params } = buildTimeFilter(filter)
|
||||
const msgFilterBase = clause ? clause.replace('WHERE', 'AND') : ''
|
||||
const msgFilterWithSystem = msgFilterBase + " AND COALESCE(m.account_name, '') != '系统消息'"
|
||||
|
||||
const members = db
|
||||
.prepare(
|
||||
`SELECT m.id, m.platform_id as platformId, COALESCE(m.group_nickname, m.account_name, m.platform_id) as name,
|
||||
COUNT(msg.id) as messageCount
|
||||
FROM member m LEFT JOIN message msg ON m.id = msg.sender_id ${msgFilterWithSystem}
|
||||
WHERE COALESCE(m.account_name, '') != '系统消息' GROUP BY m.id`
|
||||
)
|
||||
.all(...params) as Array<{ id: number; platformId: string; name: string; messageCount: number }>
|
||||
|
||||
if (members.length === 0) return emptyResult
|
||||
|
||||
const nameToMemberId = new Map<string, number>()
|
||||
const memberIdToInfo = new Map<number, { name: string; messageCount: number }>()
|
||||
|
||||
for (const member of members) {
|
||||
memberIdToInfo.set(member.id, { name: member.name, messageCount: member.messageCount })
|
||||
nameToMemberId.set(member.name, member.id)
|
||||
const history = db.prepare('SELECT name FROM member_name_history WHERE member_id = ?').all(member.id) as Array<{
|
||||
name: string
|
||||
}>
|
||||
for (const h of history) {
|
||||
if (!nameToMemberId.has(h.name)) nameToMemberId.set(h.name, member.id)
|
||||
}
|
||||
}
|
||||
|
||||
let whereClause = clause
|
||||
if (whereClause.includes('WHERE')) {
|
||||
whereClause +=
|
||||
" AND COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content LIKE '%@%'"
|
||||
} else {
|
||||
whereClause =
|
||||
" WHERE COALESCE(m.account_name, '') != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND msg.content LIKE '%@%'"
|
||||
}
|
||||
|
||||
const messages = db
|
||||
.prepare(
|
||||
`SELECT msg.sender_id as senderId, msg.content FROM message msg JOIN member m ON msg.sender_id = m.id ${whereClause}`
|
||||
)
|
||||
.all(...params) as Array<{ senderId: number; content: string }>
|
||||
|
||||
const mentionMatrix = new Map<number, Map<number, number>>()
|
||||
const mentionRegex = /@([^\s@]+)/g
|
||||
|
||||
for (const msg of messages) {
|
||||
const matches = msg.content.matchAll(mentionRegex)
|
||||
const mentionedInThisMsg = new Set<number>()
|
||||
for (const match of matches) {
|
||||
const mentionedId = nameToMemberId.get(match[1])
|
||||
if (mentionedId && mentionedId !== msg.senderId && !mentionedInThisMsg.has(mentionedId)) {
|
||||
mentionedInThisMsg.add(mentionedId)
|
||||
if (!mentionMatrix.has(msg.senderId)) mentionMatrix.set(msg.senderId, new Map())
|
||||
const fromMap = mentionMatrix.get(msg.senderId)!
|
||||
fromMap.set(mentionedId, (fromMap.get(mentionedId) || 0) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const involvedMemberIds = new Set<number>()
|
||||
for (const [fromId, toMap] of mentionMatrix.entries()) {
|
||||
involvedMemberIds.add(fromId)
|
||||
for (const toId of toMap.keys()) involvedMemberIds.add(toId)
|
||||
}
|
||||
|
||||
const maxMessageCount = Math.max(...members.filter((m) => involvedMemberIds.has(m.id)).map((m) => m.messageCount), 1)
|
||||
const nodes: MentionGraphNode[] = []
|
||||
for (const memberId of involvedMemberIds) {
|
||||
const info = memberIdToInfo.get(memberId)
|
||||
if (info) {
|
||||
const symbolSize = 20 + (info.messageCount / maxMessageCount) * 40
|
||||
nodes.push({ id: memberId, name: info.name, value: info.messageCount, symbolSize: Math.round(symbolSize) })
|
||||
}
|
||||
}
|
||||
|
||||
const links: MentionGraphLink[] = []
|
||||
let maxLinkValue = 0
|
||||
for (const [fromId, toMap] of mentionMatrix.entries()) {
|
||||
const fromInfo = memberIdToInfo.get(fromId)
|
||||
if (!fromInfo) continue
|
||||
for (const [toId, count] of toMap.entries()) {
|
||||
const toInfo = memberIdToInfo.get(toId)
|
||||
if (!toInfo) continue
|
||||
links.push({ source: fromInfo.name, target: toInfo.name, value: count })
|
||||
maxLinkValue = Math.max(maxLinkValue, count)
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, links, maxLinkValue }
|
||||
}
|
||||
|
||||
// ==================== 含笑量分析 ====================
|
||||
|
||||
function keywordToPattern(keyword: string): string {
|
||||
|
||||
@@ -216,7 +216,6 @@ export {
|
||||
export {
|
||||
getCatchphraseAnalysis,
|
||||
getMentionAnalysis,
|
||||
getMentionGraph,
|
||||
getGroupRelationshipGalaxy,
|
||||
GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION,
|
||||
getLaughAnalysis,
|
||||
@@ -235,9 +234,6 @@ export type {
|
||||
CatchphraseAnalysis,
|
||||
MemberCatchphrase,
|
||||
CatchphraseItem,
|
||||
MentionGraphData,
|
||||
MentionGraphNode,
|
||||
MentionGraphLink,
|
||||
GroupRelationshipGalaxyOptions,
|
||||
ClusterGraphData,
|
||||
ClusterGraphNode,
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
getJourneyStats,
|
||||
getCatchphraseAnalysis,
|
||||
getMentionAnalysis,
|
||||
getMentionGraph,
|
||||
getGroupRelationshipGalaxy,
|
||||
GROUP_RELATIONSHIP_GALAXY_ALGORITHM_VERSION,
|
||||
getLaughAnalysis,
|
||||
@@ -151,15 +150,6 @@ export function registerAnalyticsRoutes(server: FastifyInstance, ctx: AnalyticsR
|
||||
}
|
||||
)
|
||||
|
||||
server.get<{ Params: { id: string }; Querystring: FilteredQuery }>(
|
||||
'/_web/sessions/:id/analytics/mention-graph',
|
||||
async (request) => {
|
||||
const id = request.params.id
|
||||
const filter = parseTimeFilter(request.query)
|
||||
return cached('mention-graph', id, { ...filter }, () => getMentionGraph(adapter.ensureReadonly(id), filter))
|
||||
}
|
||||
)
|
||||
|
||||
server.get<{ Params: { id: string }; Querystring: FilteredQuery }>(
|
||||
'/_web/sessions/:id/analytics/relationship-galaxy',
|
||||
async (request) => {
|
||||
|
||||
@@ -1044,41 +1044,29 @@ describe('BrowserSessionRuntime', () => {
|
||||
|
||||
await runtime.importSource(source('insight-query.json', fixture), { formatId: 'chatlab' })
|
||||
|
||||
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,
|
||||
}),
|
||||
])
|
||||
const [monthly, yearly, memberTrend, members, mentions, 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.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)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
getLanguagePreferenceAnalysis as queryLanguagePreferenceAnalysis,
|
||||
getMembersWithAliases as queryMembersWithAliases,
|
||||
getMentionAnalysis as queryMentionAnalysis,
|
||||
getMentionGraph as queryMentionGraph,
|
||||
getGroupRelationshipGalaxy as queryGroupRelationshipGalaxy,
|
||||
getAvailableYears as queryAvailableYears,
|
||||
getDailyActivity as queryDailyActivity,
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
type MemberWithAliases,
|
||||
type MessageLengthDistribution,
|
||||
type MessageTypeStats,
|
||||
type MentionGraphData,
|
||||
type MonthlyActivity,
|
||||
type RelationshipStats,
|
||||
type JourneyStats,
|
||||
@@ -423,10 +421,6 @@ export class BrowserSessionRuntime {
|
||||
return this.withSessionDatabase(id, (db) => queryMentionAnalysis(db, filter))
|
||||
}
|
||||
|
||||
async getMentionGraph(id: string, filter?: BrowserTimeFilter): Promise<MentionGraphData> {
|
||||
return this.withSessionDatabase(id, (db) => queryMentionGraph(db, filter))
|
||||
}
|
||||
|
||||
async getGroupRelationshipGalaxy(id: string, filter?: BrowserTimeFilter): Promise<GroupRelationshipGalaxyData> {
|
||||
return this.withSessionDatabase(id, (db) => queryGroupRelationshipGalaxy(db, filter))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
MemberWithAliases,
|
||||
MessageLengthDistribution,
|
||||
MessageTypeStats,
|
||||
MentionGraphData,
|
||||
MonthlyActivity,
|
||||
ClusterGraphData,
|
||||
ClusterGraphOptions,
|
||||
@@ -228,10 +227,6 @@ export interface WebRuntimeTaskMap {
|
||||
totalMentions: number
|
||||
}
|
||||
}
|
||||
'analysis.mentionGraph': {
|
||||
payload: { sessionId: string; filter?: BrowserTimeFilter }
|
||||
result: MentionGraphData
|
||||
}
|
||||
'analysis.groupRelationshipGalaxy': {
|
||||
payload: { sessionId: string; filter?: BrowserTimeFilter }
|
||||
result: GroupRelationshipGalaxyData
|
||||
@@ -385,7 +380,6 @@ const WEB_RUNTIME_TASK_TYPES: Record<WebRuntimeTaskType, true> = {
|
||||
'analysis.memberMonthlyTrend': true,
|
||||
'analysis.memberList': true,
|
||||
'analysis.mentions': true,
|
||||
'analysis.mentionGraph': true,
|
||||
'analysis.groupRelationshipGalaxy': true,
|
||||
'analysis.clusterGraph': true,
|
||||
'analysis.relationship': true,
|
||||
|
||||
@@ -270,10 +270,6 @@ class FakeSessionRuntime implements WorkerSessionRuntime {
|
||||
return { topMentioners: [], topMentioned: [], totalMentions: 0 }
|
||||
}
|
||||
|
||||
async getMentionGraph(_id: string, _filter?: BrowserTimeFilter) {
|
||||
return { nodes: [], links: [], maxLinkValue: 0 }
|
||||
}
|
||||
|
||||
async getGroupRelationshipGalaxy(_id: string, _filter?: BrowserTimeFilter) {
|
||||
return {
|
||||
graph: { nodes: [], edges: [], communities: [] },
|
||||
@@ -819,7 +815,6 @@ describe('WebRuntimeWorkerController', () => {
|
||||
{ id: 'trend', type: 'analysis.memberMonthlyTrend', payload: { sessionId: 'session-one' } },
|
||||
{ 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',
|
||||
|
||||
@@ -52,7 +52,6 @@ export type WorkerSessionRuntime = Pick<
|
||||
| 'getMemberMonthlyTrend'
|
||||
| 'getMembers'
|
||||
| 'getMentionAnalysis'
|
||||
| 'getMentionGraph'
|
||||
| 'getGroupRelationshipGalaxy'
|
||||
| 'getClusterGraph'
|
||||
| 'getRelationshipStats'
|
||||
@@ -495,12 +494,6 @@ export class WebRuntimeWorkerController {
|
||||
this.emitAnalysisQueryCompleted(request, startedAt)
|
||||
return result
|
||||
}
|
||||
case 'analysis.mentionGraph': {
|
||||
const startedAt = this.startAnalysisQuery()
|
||||
const result = await this.sessionRuntime.getMentionGraph(request.payload.sessionId, request.payload.filter)
|
||||
this.emitAnalysisQueryCompleted(request, startedAt)
|
||||
return result
|
||||
}
|
||||
case 'analysis.groupRelationshipGalaxy': {
|
||||
const startedAt = this.startAnalysisQuery()
|
||||
const result = await this.sessionRuntime.getGroupRelationshipGalaxy(
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 互动分析视图(群聊专属)
|
||||
* 展示成员间的 @ 互动关系图
|
||||
*/
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EChartGraph } from '@/components/charts'
|
||||
import type { EChartGraphData } from '@/components/charts'
|
||||
import { useInsightViewLoading } from '@/components/UI/insight-view-loading'
|
||||
import { useDataService } from '@/services/data/service'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 数据状态
|
||||
const isLoading = ref(true)
|
||||
useInsightViewLoading(isLoading)
|
||||
const graphData = ref<EChartGraphData>({ nodes: [], links: [], maxLinkValue: 0 })
|
||||
|
||||
// 布局切换
|
||||
const layoutType = ref<'circular' | 'force'>('circular')
|
||||
|
||||
// 方向切换(有向图 vs 无向图)
|
||||
const showDirection = ref(false)
|
||||
|
||||
// 图表引用
|
||||
const graphRef = ref<InstanceType<typeof EChartGraph> | null>(null)
|
||||
|
||||
// 重置视图
|
||||
function handleResetView() {
|
||||
graphRef.value?.resetView()
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
if (!props.sessionId) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await useDataService().getMentionGraph(props.sessionId, props.timeFilter)
|
||||
graphData.value = {
|
||||
nodes: data.nodes,
|
||||
links: data.links,
|
||||
maxLinkValue: data.maxLinkValue,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[chart-interaction] 加载互动关系图数据失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 props 变化
|
||||
watch(
|
||||
() => [props.sessionId, props.timeFilter],
|
||||
() => {
|
||||
loadData()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- 顶部工具栏(仅右上角控制) -->
|
||||
<div class="flex flex-wrap items-center justify-end px-3 py-2 sm:px-4">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 sm:gap-4">
|
||||
<!-- 布局切换 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-400">{{ t('views.interaction.layout') }}:</span>
|
||||
<div class="inline-flex items-center" role="group">
|
||||
<UButton
|
||||
size="xs"
|
||||
:color="layoutType === 'circular' ? 'primary' : 'neutral'"
|
||||
:variant="layoutType === 'circular' ? 'solid' : 'ghost'"
|
||||
@click="layoutType = 'circular'"
|
||||
>
|
||||
{{ t('views.interaction.circular') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
:color="layoutType === 'force' ? 'primary' : 'neutral'"
|
||||
:variant="layoutType === 'force' ? 'solid' : 'ghost'"
|
||||
@click="layoutType = 'force'"
|
||||
>
|
||||
{{ t('views.interaction.force') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 方向切换 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-400">{{ t('views.interaction.directed') }}:</span>
|
||||
<USwitch v-model="showDirection" size="xs" />
|
||||
</div>
|
||||
<!-- 重置视图 -->
|
||||
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-arrow-path" @click="handleResetView">
|
||||
{{ t('views.interaction.reset') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域(全屏) -->
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="absolute inset-0 flex items-center justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-pink-500" />
|
||||
</div>
|
||||
|
||||
<!-- 图表 -->
|
||||
<template v-else>
|
||||
<div v-if="graphData.nodes.length > 0" class="h-full">
|
||||
<EChartGraph
|
||||
ref="graphRef"
|
||||
:data="graphData"
|
||||
:layout="layoutType"
|
||||
:directed="showDirection"
|
||||
:height="'100%'"
|
||||
/>
|
||||
<!-- 底部统计信息 -->
|
||||
<div
|
||||
class="absolute bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-gray-100/80 px-3 py-1 text-xs text-gray-500 backdrop-blur-sm dark:bg-gray-800/80 dark:text-gray-400"
|
||||
>
|
||||
{{ t('views.interaction.graphHint', { nodes: graphData.nodes.length, links: graphData.links.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex h-full items-center justify-center text-gray-400">
|
||||
<div class="text-center">
|
||||
<UIcon name="i-heroicons-user-group" class="mx-auto h-10 w-10 text-gray-300 dark:text-gray-600" />
|
||||
<p class="mt-2 text-sm">{{ t('views.interaction.noInteraction') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,281 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ECharts 关系图组件(支持 circular 和 force 布局)
|
||||
*/
|
||||
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { GraphChart } from 'echarts/charts'
|
||||
import { TooltipComponent, LegendComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
|
||||
// 注册必要的组件
|
||||
echarts.use([GraphChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||
|
||||
type ECOption = EChartsOption
|
||||
|
||||
export interface GraphNode {
|
||||
id: number | string
|
||||
name: string
|
||||
value?: number
|
||||
symbolSize?: number
|
||||
category?: number
|
||||
}
|
||||
|
||||
export interface GraphLink {
|
||||
source: string
|
||||
target: string
|
||||
value?: number
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[]
|
||||
links: GraphLink[]
|
||||
maxLinkValue?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: GraphData
|
||||
height?: number | string
|
||||
layout?: 'circular' | 'force' // 布局类型
|
||||
directed?: boolean // 是否显示箭头(有向图)
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 400,
|
||||
layout: 'circular',
|
||||
directed: false,
|
||||
})
|
||||
|
||||
// 计算高度样式
|
||||
const heightStyle = computed(() => {
|
||||
if (typeof props.height === 'number') {
|
||||
return `${props.height}px`
|
||||
}
|
||||
return props.height
|
||||
})
|
||||
|
||||
const isDark = useDark()
|
||||
const chartRef = ref<HTMLElement | null>(null)
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 丰富的调色板(为每个节点分配不同颜色)
|
||||
const colorPalette = [
|
||||
'#ee4567', // 粉色(主题色)
|
||||
'#5470c6', // 蓝色
|
||||
'#91cc75', // 绿色
|
||||
'#fac858', // 黄色
|
||||
'#ee6666', // 红色
|
||||
'#73c0de', // 青色
|
||||
'#9a60b4', // 紫色
|
||||
'#fc8452', // 橙色
|
||||
'#3ba272', // 深绿
|
||||
'#ea7ccc', // 粉紫
|
||||
'#6e7074', // 灰色
|
||||
'#546570', // 深灰蓝
|
||||
]
|
||||
|
||||
// 去重后的节点(ECharts 要求节点名称唯一)
|
||||
const uniqueNodes = computed(() => {
|
||||
const seen = new Set<string>()
|
||||
return props.data.nodes.filter((node) => {
|
||||
if (seen.has(node.name)) {
|
||||
return false
|
||||
}
|
||||
seen.add(node.name)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// 节点名称到颜色的映射
|
||||
const nodeColorMap = computed(() => {
|
||||
const map = new Map<string, string>()
|
||||
uniqueNodes.value.forEach((node, index) => {
|
||||
map.set(node.name, colorPalette[index % colorPalette.length])
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
// 计算边的宽度(根据 value 归一化)
|
||||
function getLinkWidth(value: number, maxValue: number): number {
|
||||
if (maxValue <= 0) return 1
|
||||
// 宽度范围 1-6
|
||||
return 1 + (value / maxValue) * 5
|
||||
}
|
||||
|
||||
const option = computed<ECOption>(() => {
|
||||
const maxLinkValue = props.data.maxLinkValue || Math.max(...props.data.links.map((l) => l.value || 1), 1)
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: isDark.value ? 'rgba(30, 30, 30, 0.9)' : 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: isDark.value ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
||||
textStyle: {
|
||||
color: isDark.value ? '#e5e7eb' : '#374151',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
if (params.dataType === 'node') {
|
||||
return `<b>${params.data.name}</b><br/>消息数: ${params.data.value || 0}`
|
||||
} else if (params.dataType === 'edge') {
|
||||
return `${params.data.source} → ${params.data.target}<br/>艾特次数: ${params.data.value || 0}`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
// 动画效果
|
||||
animationDuration: 1000,
|
||||
animationDurationUpdate: 500,
|
||||
animationEasingUpdate: 'quinticInOut',
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: props.layout,
|
||||
circular: props.layout === 'circular' ? { rotateLabel: true } : undefined,
|
||||
force:
|
||||
props.layout === 'force'
|
||||
? {
|
||||
repulsion: 300,
|
||||
gravity: 0.1,
|
||||
edgeLength: [80, 200],
|
||||
friction: 0.6,
|
||||
}
|
||||
: undefined,
|
||||
roam: true,
|
||||
scaleLimit: {
|
||||
min: 0.3, // 最小缩放 30%
|
||||
max: 3, // 最大缩放 300%
|
||||
},
|
||||
draggable: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: '{b}',
|
||||
color: isDark.value ? '#e5e7eb' : '#374151',
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
edgeSymbol: props.directed ? ['none', 'arrow'] : ['none', 'none'],
|
||||
edgeSymbolSize: props.directed ? [0, 10] : [0, 0],
|
||||
lineStyle: {
|
||||
curveness: 0.3, // 始终使用曲线
|
||||
opacity: 0.5,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'adjacency',
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 4,
|
||||
opacity: 0.9,
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 15,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
},
|
||||
// 节点数据(使用去重后的节点)
|
||||
data: uniqueNodes.value.map((node) => {
|
||||
const color = nodeColorMap.value.get(node.name) || colorPalette[0]
|
||||
return {
|
||||
name: node.name,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
// circular 布局显示所有标签,force 布局只显示大节点的标签
|
||||
label: {
|
||||
show: props.layout === 'circular' ? true : (node.symbolSize || 30) > 30,
|
||||
},
|
||||
itemStyle: {
|
||||
color: color,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
shadowBlur: 5,
|
||||
shadowColor: `${color}66`, // 同色系阴影
|
||||
},
|
||||
}
|
||||
}),
|
||||
// 连接线数据(颜色跟随源节点,过滤掉引用不存在节点的链接)
|
||||
links: props.data.links
|
||||
.filter((link) => nodeColorMap.value.has(link.source) && nodeColorMap.value.has(link.target))
|
||||
.map((link) => {
|
||||
const sourceColor = nodeColorMap.value.get(link.source) || colorPalette[0]
|
||||
return {
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
value: link.value,
|
||||
lineStyle: {
|
||||
color: sourceColor,
|
||||
width: getLinkWidth(link.value || 1, maxLinkValue),
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value, isDark.value ? 'dark' : undefined, {
|
||||
renderer: 'canvas',
|
||||
})
|
||||
chartInstance.setOption({ backgroundColor: 'transparent', ...option.value })
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateChart() {
|
||||
if (!chartInstance) return
|
||||
chartInstance.setOption({ backgroundColor: 'transparent', ...option.value }, { notMerge: true })
|
||||
}
|
||||
|
||||
// 响应窗口大小变化
|
||||
function handleResize() {
|
||||
chartInstance?.resize()
|
||||
}
|
||||
|
||||
// 重置视图(居中 + 重置缩放)
|
||||
function resetView() {
|
||||
if (!chartInstance) return
|
||||
chartInstance.dispatchAction({
|
||||
type: 'restore',
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
resetView,
|
||||
})
|
||||
|
||||
// 监听数据和主题变化
|
||||
watch(
|
||||
[() => props.data, () => props.layout, () => props.directed, isDark],
|
||||
() => {
|
||||
if (chartInstance) {
|
||||
updateChart()
|
||||
} else {
|
||||
initChart()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height: heightStyle, width: '100%' }" />
|
||||
</template>
|
||||
@@ -7,7 +7,6 @@ export { default as EChartBar } from './EChartBar.vue'
|
||||
export { default as EChartLine } from './EChartLine.vue'
|
||||
export { default as EChartHeatmap } from './EChartHeatmap.vue'
|
||||
export { default as EChartCalendar } from './EChartCalendar.vue'
|
||||
export { default as EChartGraph } from './EChartGraph.vue'
|
||||
export { default as EChartWordcloud } from './EChartWordcloud.vue'
|
||||
export { default as EChartRank } from './EChartRank.vue'
|
||||
|
||||
@@ -24,7 +23,6 @@ export type { EChartBarData } from './EChartBar.vue'
|
||||
export type { EChartLineData } from './EChartLine.vue'
|
||||
export type { EChartHeatmapData } from './EChartHeatmap.vue'
|
||||
export type { CalendarData as EChartCalendarData } from './EChartCalendar.vue'
|
||||
export type { GraphData as EChartGraphData, GraphNode, GraphLink } from './EChartGraph.vue'
|
||||
export type { WordcloudData as EChartWordcloudData } from './EChartWordcloud.vue'
|
||||
|
||||
// 其他类型
|
||||
|
||||
@@ -200,7 +200,6 @@ describe('BrowserDataAdapter', () => {
|
||||
},
|
||||
],
|
||||
'analysis.mentions': { topMentioners: [], topMentioned: [], totalMentions: 0 },
|
||||
'analysis.mentionGraph': { nodes: [], links: [], maxLinkValue: 0 },
|
||||
'analysis.groupRelationshipGalaxy': {
|
||||
graph: { nodes: [], edges: [], communities: [] },
|
||||
members: [],
|
||||
@@ -237,7 +236,6 @@ describe('BrowserDataAdapter', () => {
|
||||
assert.equal((await adapter.getMemberMonthlyTrend('session-one'))[0].memberName, 'Alice')
|
||||
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)
|
||||
@@ -253,7 +251,6 @@ describe('BrowserDataAdapter', () => {
|
||||
'analysis.memberMonthlyTrend',
|
||||
'analysis.memberList',
|
||||
'analysis.mentions',
|
||||
'analysis.mentionGraph',
|
||||
'analysis.groupRelationshipGalaxy',
|
||||
'analysis.clusterGraph',
|
||||
'analysis.relationship',
|
||||
|
||||
@@ -47,7 +47,6 @@ type BrowserSessionDataAdapter = Pick<
|
||||
| 'getMemberMonthlyTrend'
|
||||
| 'getMembers'
|
||||
| 'getMentionAnalysis'
|
||||
| 'getMentionGraph'
|
||||
| 'getGroupRelationshipGalaxy'
|
||||
| 'getClusterGraph'
|
||||
| 'getRelationshipStats'
|
||||
@@ -153,10 +152,6 @@ export class BrowserDataAdapter implements BrowserSessionDataAdapter {
|
||||
return this.requestAnalysis('analysis.mentions', { sessionId, filter })
|
||||
}
|
||||
|
||||
getMentionGraph(sessionId: string, filter?: TimeFilter) {
|
||||
return this.requestAnalysis('analysis.mentionGraph', { sessionId, filter })
|
||||
}
|
||||
|
||||
getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter) {
|
||||
return this.requestAnalysis('analysis.groupRelationshipGalaxy', { sessionId, filter })
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ import type {
|
||||
PaginatedResult,
|
||||
SQLResult,
|
||||
TableSchema,
|
||||
MentionGraphData,
|
||||
MessageLengthDistribution,
|
||||
} from './types'
|
||||
import { get, post, del, put, patch, analyticsGet, analyticsPost } from '../utils/http'
|
||||
@@ -393,10 +392,6 @@ export class FetchDataAdapter implements DataAdapter {
|
||||
return analyticsGet(`/sessions/${sessionId}/analytics/mention${buildFilterParams(filter)}`)
|
||||
}
|
||||
|
||||
getMentionGraph(sessionId: string, filter?: TimeFilter): Promise<MentionGraphData> {
|
||||
return analyticsGet(`/sessions/${sessionId}/analytics/mention-graph${buildFilterParams(filter)}`)
|
||||
}
|
||||
|
||||
getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter): Promise<GroupRelationshipGalaxyData> {
|
||||
return analyticsGet(`/sessions/${sessionId}/analytics/relationship-galaxy${buildFilterParams(filter)}`)
|
||||
}
|
||||
|
||||
@@ -134,14 +134,6 @@ export interface AnnualSummaryFetchOptions {
|
||||
acceptStale?: boolean
|
||||
}
|
||||
|
||||
// ==================== Mention Graph ====================
|
||||
|
||||
export interface MentionGraphData {
|
||||
nodes: Array<{ id: number; name: string; value: number; symbolSize: number }>
|
||||
links: Array<{ source: string; target: string; value: number }>
|
||||
maxLinkValue: number
|
||||
}
|
||||
|
||||
// ==================== 消息长度分布 ====================
|
||||
|
||||
export interface MessageLengthDistribution {
|
||||
@@ -235,7 +227,6 @@ export interface DataAdapter {
|
||||
dictType?: string
|
||||
): Promise<LanguagePreferenceResult>
|
||||
getMentionAnalysis(sessionId: string, filter?: TimeFilter): Promise<MentionAnalysis>
|
||||
getMentionGraph(sessionId: string, filter?: TimeFilter): Promise<MentionGraphData>
|
||||
getGroupRelationshipGalaxy(sessionId: string, filter?: TimeFilter): Promise<GroupRelationshipGalaxyData>
|
||||
getClusterGraph(sessionId: string, filter?: TimeFilter, options?: ClusterGraphOptions): Promise<ClusterGraphData>
|
||||
getLaughAnalysis(sessionId: string, filter?: TimeFilter, keywords?: string[]): Promise<LaughAnalysis>
|
||||
|
||||
@@ -12,14 +12,7 @@ export type { BrowserRuntimeServiceAdapter } from './browser-runtime/types'
|
||||
|
||||
export { useDataService } from './data/service'
|
||||
export type { DataAdapter } from './data/types'
|
||||
export type {
|
||||
PaginationParams,
|
||||
PaginatedResult,
|
||||
SQLResult,
|
||||
TableSchema,
|
||||
MentionGraphData,
|
||||
MessageLengthDistribution,
|
||||
} from './data/types'
|
||||
export type { PaginationParams, PaginatedResult, SQLResult, TableSchema, MessageLengthDistribution } from './data/types'
|
||||
|
||||
export { useImportService } from './import/service'
|
||||
export type {
|
||||
|
||||
Reference in new issue
Block a user