fix: 修复词库刷新与合并ID碰撞等问题

This commit is contained in:
digua
2026-04-15 00:10:08 +08:00
committed by digua
parent 726396733a
commit 165ae83ba2
5 changed files with 68 additions and 10 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
- 文档:开始开发前,请先查看 `./.docs/README.md`,并阅读与当前需求相关的文档
- 目标:用最小改动快速交付正确、可维护、可回归的业务结果
- 边界:只解决当前需求,不做与需求无关的重构和“顺手优化”
- 每次完成任务后,进行类型检查、lint检查和format格式化,确保代码质量
- 每次完成任务后,对产生修改的文件进行类型检查、lint检查和format格式化,指定修改的文件路径去执行,确保代码质量
+41 -3
View File
@@ -118,10 +118,48 @@ export function registerWindowHandlers(ctx: IpcContext): void {
const timeout = setTimeout(() => abortController.abort(), REMOTE_CONFIG_TIMEOUT_MS)
try {
const response = await fetch(normalizedUrl, { signal: abortController.signal })
const finalUrl = response.url || normalizedUrl
// 使用 manual 重定向模式,手动验证每个重定向目标
let currentUrl = normalizedUrl
let response = await fetch(currentUrl, {
signal: abortController.signal,
redirect: 'manual',
})
// 处理重定向链(最多跟随3次重定向,避免无限循环)
let redirectCount = 0
const maxRedirects = 3
while (response.status >= 300 && response.status < 400 && redirectCount < maxRedirects) {
redirectCount++
const location = response.headers.get('location')
if (!location) {
return { success: false, error: `Redirect response without location header (hop ${redirectCount})` }
}
// 构建完整的重定向 URL
const redirectUrl = new URL(location, currentUrl).href
if (!isAllowedRemoteConfigUrl(redirectUrl)) {
return { success: false, error: `Redirect URL is not allowed (hop ${redirectCount}): ${redirectUrl}` }
}
// 跟随重定向
currentUrl = redirectUrl
response = await fetch(currentUrl, {
signal: abortController.signal,
redirect: 'manual',
})
}
// 检查是否超过最大重定向次数(严格大于,允许恰好等于最大次数)
if (redirectCount > maxRedirects) {
return { success: false, error: `Too many redirects (exceeded ${maxRedirects})` }
}
// 验证最终响应的 URL
const finalUrl = response.url || currentUrl
if (!isAllowedRemoteConfigUrl(finalUrl)) {
return { success: false, error: 'Redirect URL is not allowed' }
return { success: false, error: 'Final URL is not allowed' }
}
const contentType = response.headers.get('content-type') || ''
+4 -1
View File
@@ -118,7 +118,10 @@ function getCollidingPlatformIds(
function normalizePlatformId(platformId: string, platform: string, collidingIds: Set<string>): string {
if (!collidingIds.has(platformId)) return platformId
return `${platform || 'unknown'}:${platformId}`
// 使用可编码且带命名空间的格式,避免与原始 platformId(如 "qq:123")发生键碰撞。
const normalizedPlatform = encodeURIComponent(platform || 'unknown')
const normalizedId = encodeURIComponent(platformId)
return `__chatlab_platform__${normalizedPlatform}__${normalizedId}`
}
function getCollidingPlatformIdsFromMessages(
+20 -3
View File
@@ -29,6 +29,15 @@ export function initNlpDir(nlpDir: string): void {
_nlpDir = nlpDir
}
/**
* 检查词库文件是否存在
*/
function existsDictOnDisk(dictId: string): boolean {
if (!_nlpDir) return false
const dictPath = path.join(_nlpDir, `${dictId}.dict`)
return fs.existsSync(dictPath)
}
/**
* 尝试从 nlpDir 加载词库文件,返回 Buffer 或 null
*/
@@ -52,24 +61,32 @@ function tryLoadDictFromDisk(dictId: string): Buffer | null {
export function getJieba(dictType: DictType = 'default'): JiebaInstance {
const effectiveType = dictType === 'default' ? 'zh-CN' : dictType
const cached = jiebaInstances.get(effectiveType)
if (cached) return cached
if (cached) {
// 仅当磁盘词库仍存在时复用缓存;词库被删除后立即失效,避免继续使用陈旧实例。
// 使用 existsDictOnDisk 而不是 tryLoadDictFromDisk,避免在缓存检查时读取文件内容
if (existsDictOnDisk(effectiveType)) return cached
jiebaInstances.delete(effectiveType)
console.log(`[NLP] jieba cache invalidated (dict missing): ${effectiveType}`)
}
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Jieba } = require('@node-rs/jieba')
const diskDict = tryLoadDictFromDisk(effectiveType)
let instance: JiebaInstance
const diskDict = tryLoadDictFromDisk(effectiveType)
if (diskDict) {
instance = Jieba.withDict(diskDict)
console.log(`[NLP] jieba dict loaded: ${effectiveType} (${diskDict.length} bytes)`)
jiebaInstances.set(effectiveType, instance)
} else {
// 词库缺失时仍保留 jieba 能力,避免 FTS 退化为空格切分。
// 缓存 fallback 实例以提升性能,但在词库下载/删除时会通过 clearJiebaInstance 主动失效缓存
instance = new Jieba()
console.warn(`[NLP] jieba dict missing: ${effectiveType}, fallback to built-in tokenizer`)
jiebaInstances.set(effectiveType, instance)
}
jiebaInstances.set(effectiveType, instance)
return instance
} catch (error) {
console.error(`[NLP] Failed to load jieba module (dict=${effectiveType}):`, error)
@@ -206,10 +206,10 @@ const chartOption = computed<EChartsOption>(() => ({
const cellPoint = api.coord(api.value(0))
const cellWidth = params.coordSys.cellWidth
const cellHeight = params.coordSys.cellHeight
// 每个格子的边长减去 3 像素,从而形成真正的透明物理间隙,透出底部光效
const size = Math.min(cellWidth, cellHeight) - 3
return {
type: 'rect',
shape: {