mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +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
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user