refactor(insight): remove legacy mention graph

This commit is contained in:
digua committed 2026-08-09 10:57:24 +08:00
1 parent 2f5f97f0bf
commit cd2a401c3b
25 files changed
+61 -703

No files matched your search

-4
View File
@@ -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')
+2 -10
View File
@@ -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'
-117
View File
@@ -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 {
-4
View File
@@ -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))
}
-6
View File
@@ -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(