feat(desktop): 实现一些功能

1. 实现了用户阅读画像

2. 实现了全局检索功能
This commit is contained in:
2026-01-11 14:40:31 +08:00
parent 75cc9dc06d
commit 48fb287aa7
25 changed files with 1059 additions and 145 deletions

View File

@@ -0,0 +1,100 @@
import { Repository } from 'typeorm'
import { ReadingPersona } from '@main/db/entities/ReadingPersona'
import {
IReadingReflectionTaskBatch,
IReadingReflectionTaskItem
} from '@shared/types/IReadingReflectionTask'
import { IUserReadingPersona } from '@shared/types/IUserReadingPersona'
export class PersonaService {
constructor(private personaRepo: Repository<ReadingPersona>) {}
/**
* 刷新画像并保存到数据库
*/
async refreshPersona(
items: IReadingReflectionTaskItem[],
batches: IReadingReflectionTaskBatch[]
) {
const rawResult = await this.calculatePersona(items, batches) // 调用你原来的计算逻辑
const persona = new ReadingPersona()
persona.id = 'current_user_persona'
persona.cognition = rawResult.cognition
persona.breadth = rawResult.breadth
persona.practicality = rawResult.practicality
persona.output = rawResult.output
persona.global = rawResult.global
persona.topKeywords = JSON.stringify(rawResult.topKeywords)
// 存储完整的 stats 结构以便前端适配
persona.rawStats = {
totalWords: items.reduce((sum, i) => sum + (i.content?.length || 0), 0),
totalBooks: batches.length,
topKeywords: rawResult.topKeywords
}
return await this.personaRepo.save(persona)
}
/**
* 从数据库聚合数据并计算画像分值
*/
async calculatePersona(
items: IReadingReflectionTaskItem[],
batches: IReadingReflectionTaskBatch[]
) {
// 1. 计算认知深度:根据关键词频次
const allKeywords = items.flatMap((i) => i.keywords || [])
const keywordMap = new Map<string, number>()
allKeywords.forEach((k) => keywordMap.set(k, (keywordMap.get(k) || 0) + 1))
// 逻辑:去重后的关键词越多且重复越高,分值越高 (示例算法)
const cognitionScore = Math.min(100, keywordMap.size * 2 + allKeywords.length / 5)
// 2. 计算知识广度:根据书籍数量
const breadthScore = Math.min(100, batches.length * 10)
// 3. 计算产出效率:根据总字数
const totalWords = items.reduce((sum, i) => sum + (i.content?.length || 0), 0)
const outputScore = Math.min(100, totalWords / 500) // 每 5万字满分
// 4. 计算 Top 10 关键词
const sortedKeywords = [...keywordMap.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map((entry) => entry[0])
return {
cognition: Math.round(cognitionScore),
breadth: Math.round(breadthScore),
output: Math.round(outputScore),
practicality: 75, // 可根据 occupation 比例动态计算
global: 60, // 可根据 language 比例动态计算
topKeywords: sortedKeywords
}
}
}
/**
* 实体转化为用户阅读画像
* @param entity 实体
* */
export function entityToUserReadingPersona(entity: ReadingPersona): IUserReadingPersona {
return {
domainDepth: JSON.parse(entity.topKeywords || '[]').map((name: string) => ({
name,
score: entity.cognition, // 简易算法:共用认知深度分
bookCount: 1 // 可根据数据库详细统计进一步细化
})),
breadthScore: entity.breadth,
efficiencyScore: entity.output,
maturityScore: entity.practicality,
languageScore: entity.global,
stats: entity.rawStats || {
totalWords: 0,
totalBooks: 0,
topKeywords: [],
mostUsedOccupation: 'other'
}
}
}

View File

@@ -0,0 +1,104 @@
import { Document } from 'flexsearch'
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
import { ISearch, HighlightInfo } from '@shared/types/ISearch'
export class SearchService {
private index: any
constructor() {
// 初始化 FlexSearch 文档索引
this.index = new Document({
document: {
id: 'id',
index: ['title', 'content', 'bookName'],
store: true // 存储原始数据以便快速返回
},
tokenize: 'forward', // 适配中文的简易分词
context: true,
cache: true
})
}
/**
* 初始化索引:从数据库加载历史数据
*/
async initIndex(items: ReadingReflectionTaskItem[]) {
items.forEach((item) => {
this.index.add({
id: item.id,
title: item.title,
content: item.content,
bookName: (item as any).bookName || '未知书籍'
})
})
}
/**
* 全局搜索核心方法
*/
async search(query: string): Promise<ISearch[]> {
if (!query) return []
// 执行搜索
const results = await this.index.search(query, {
limit: 20,
enrich: true, // 返回 store 的数据
suggest: true
})
const searchResults: ISearch[] = []
// 扁平化 FlexSearch 的结果并计算高亮坐标
results.forEach((category: any) => {
category.result.forEach((item: any) => {
const doc = item.doc
// 避免重复项(因为一个文档可能在多个字段命中)
if (searchResults.find((r) => r.id === doc.id)) return
searchResults.push({
id: doc.id,
title: doc.title,
content: doc.content,
bookName: doc.bookName,
createdAt: new Date().toISOString(), // 实际应从数据库字段获取
// 计算高亮坐标
titleHighlights: this.calculateHighlights(doc.title, query),
contentHighlights: this.calculateHighlights(doc.content, query),
bookHighlights: this.calculateHighlights(doc.bookName, query),
contentSnippet: this.createSnippet(doc.content, query)
})
})
})
return searchResults
}
/**
* 简单的坐标计算逻辑
*/
private calculateHighlights(text: string, query: string): HighlightInfo[] {
const highlights: HighlightInfo[] = []
if (!text || !query) return highlights
let index = text.indexOf(query)
while (index !== -1) {
highlights.push({ start: index, length: query.length })
index = text.indexOf(query, index + query.length)
}
return highlights
}
/**
* 生成正文摘要预览
*/
private createSnippet(content: string, query: string): string {
const index = content.indexOf(query)
if (index === -1) return content.substring(0, 100)
const start = Math.max(0, index - 40)
const end = Math.min(content.length, index + 60)
return (
(start > 0 ? '...' : '') + content.substring(start, end) + (end < content.length ? '...' : '')
)
}
}