mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-17 20:13:48 +08:00
fix: 修复词库刷新与合并ID碰撞等问题
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
- 文档:开始开发前,请先查看 `./.docs/README.md`,并阅读与当前需求相关的文档
|
||||
- 目标:用最小改动快速交付正确、可维护、可回归的业务结果
|
||||
- 边界:只解决当前需求,不做与需求无关的重构和“顺手优化”
|
||||
- 每次完成任务后,进行类型检查、lint检查和format格式化,确保代码质量
|
||||
- 每次完成任务后,对产生修改的文件进行类型检查、lint检查和format格式化,指定修改的文件路径去执行,确保代码质量
|
||||
|
||||
@@ -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') || ''
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user