feat: 主进程配置国际化

This commit is contained in:
digua
2026-02-12 23:12:32 +08:00
committed by digua
parent 83c0641bf3
commit 1b8bbb6e49
15 changed files with 856 additions and 167 deletions
+33 -96
View File
@@ -9,6 +9,7 @@ import { getAllToolDefinitions, executeToolCalls } from './tools'
import type { ToolContext, OwnerInfo } from './tools/types'
import { aiLogger } from './logger'
import { randomUUID } from 'crypto'
import { t as i18nT } from '../i18n'
// 思考类标签列表(可按需扩展)
const THINK_TAGS = ['think', 'analysis', 'reasoning', 'reflection', 'thought', 'thinking']
@@ -311,78 +312,11 @@ export interface PromptConfig {
responseRules: string
}
// 国际化内容
const i18nContent = {
'zh-CN': {
currentDateIs: '当前日期是',
chatTypeDesc: { private: '私聊记录', group: '群聊记录' },
chatContext: { private: '对话', group: '群聊' },
ownerNote: (displayName: string, platformId: string, chatContext: string) => `当前用户身份:
- 用户在${chatContext}中的身份是「${displayName}」(platformId: ${platformId}
- 当用户提到"我"、"我的"时,指的就是「${displayName}
- 查询"我"的发言时,使用 sender_id 参数筛选该成员
`,
memberNotePrivate: `成员查询策略:
- 私聊只有两个人,可以直接获取成员列表
- 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息
`,
memberNoteGroup: `成员查询策略:
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表
- 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言
`,
timeParamsIntro: '时间参数:按用户提到的精度组合 year/month/day/hour',
timeParamExample1: (year: number) => `"10月" → year: ${year}, month: 10`,
timeParamExample2: (year: number) => `"10月1号" → year: ${year}, month: 10, day: 1`,
timeParamExample3: (year: number) => `"10月1号下午3点" → year: ${year}, month: 10, day: 1, hour: 15`,
defaultYearNote: (year: number, prevYear: number) => `未指定年份默认${year}年,若该月份未到则用${prevYear}`,
responseInstruction: '根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。',
responseRulesTitle: '回答要求:',
// Fallback 角色定义:适中幽默,允许 B 站/网络热梗与表情
fallbackRoleDefinition: (chatType: string) => `你是一个专业但风格轻松的${chatType}记录分析助手。
你的任务是帮助用户理解和分析他们的${chatType}记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。`,
// Fallback 回答要求:强调严谨优先,适度玩梗
fallbackResponseRules: `1. 基于工具返回的数据回答,不要编造信息
2. 如果数据不足以回答问题,请说明
3. 回答要简洁明了,使用 Markdown 格式
4. 可以适度加入 B 站/网络热梗、表情/颜文字(强度适中)
5. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达`,
},
'en-US': {
currentDateIs: 'Current date is',
chatTypeDesc: { private: 'private chat records', group: 'group chat records' },
chatContext: { private: 'conversation', group: 'group chat' },
ownerNote: (displayName: string, platformId: string, chatContext: string) => `Current user identity:
- The user's identity in this ${chatContext} is "${displayName}" (platformId: ${platformId})
- When the user refers to "I" or "my", it refers to "${displayName}"
- When querying "my" messages, use the sender_id parameter to filter for this member
`,
memberNotePrivate: `Member query strategy:
- Private chats only have two participants, so the member list can be directly obtained
- When the user refers to "the other party" or "he/she", get the other participant's information via get_group_members
`,
memberNoteGroup: `Member query strategy:
- When the user refers to specific group members (e.g., "what did John say", "Mary's messages"), first call get_group_members to get the member list
- Group members have three names: accountName (original nickname), groupNickname (group nickname), aliases (user-defined aliases)
- The search parameter of get_group_members can be used for fuzzy searching these three names
- Once a member is found, use their id field as the sender_id parameter for search_messages to retrieve their messages
`,
timeParamsIntro: 'Time parameters: combine year/month/day/hour based on user mention',
timeParamExample1: (year: number) => `"October" → year: ${year}, month: 10`,
timeParamExample2: (year: number) => `"October 1st" → year: ${year}, month: 10, day: 1`,
timeParamExample3: (year: number) => `"October 1st 3 PM" → year: ${year}, month: 10, day: 1, hour: 15`,
defaultYearNote: (year: number, prevYear: number) =>
`If year is not specified, defaults to ${year}. If the month has not yet occurred, ${prevYear} is used.`,
responseInstruction:
"Based on the user's question, select appropriate tools to retrieve data, then provide an answer based on the data.",
responseRulesTitle: 'Response requirements:',
fallbackRoleDefinition: (chatType: string) => `You are a professional ${chatType} analysis assistant.
Your task is to help users understand and analyze their ${chatType} data.`,
fallbackResponseRules: `1. Answer based on data returned by tools, do not fabricate information
2. If data is insufficient to answer, please state so
3. Keep answers concise and clear, use Markdown format`,
},
// ==================== 国际化辅助(使用 i18next ====================
/** 获取 Agent 翻译,根据传入的 locale 参数 */
function agentT(key: string, locale: string, options?: Record<string, unknown>): string {
return i18nT(key, { lng: locale, ...options })
}
/**
@@ -400,9 +334,8 @@ function getLockedPromptSection(
ownerInfo?: OwnerInfo,
locale: string = 'zh-CN'
): string {
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
const now = new Date()
const dateLocale = locale === 'zh-CN' ? 'zh-CN' : 'en-US'
const dateLocale = locale.startsWith('zh') ? 'zh-CN' : 'en-US'
const currentDate = now.toLocaleDateString(dateLocale, {
year: 'numeric',
month: 'long',
@@ -411,27 +344,35 @@ function getLockedPromptSection(
})
const isPrivate = chatType === 'private'
const chatContext = content.chatContext[chatType]
const chatContext = agentT(`ai.agent.chatContext.${chatType}`, locale)
// Owner 说明(当用户设置了"我是谁"时)
const ownerNote = ownerInfo ? content.ownerNote(ownerInfo.displayName, ownerInfo.platformId, chatContext) : ''
const ownerNote = ownerInfo
? agentT('ai.agent.ownerNote', locale, {
displayName: ownerInfo.displayName,
platformId: ownerInfo.platformId,
chatContext,
})
: ''
// 成员说明(私聊只有2人)
const memberNote = isPrivate ? content.memberNotePrivate : content.memberNoteGroup
const memberNote = isPrivate
? agentT('ai.agent.memberNotePrivate', locale)
: agentT('ai.agent.memberNoteGroup', locale)
const year = now.getFullYear()
const prevYear = year - 1
return `${content.currentDateIs} ${currentDate}
return `${agentT('ai.agent.currentDateIs', locale)} ${currentDate}
${ownerNote}
${memberNote}
${content.timeParamsIntro}
- ${content.timeParamExample1(year)}
- ${content.timeParamExample2(year)}
- ${content.timeParamExample3(year)}
${content.defaultYearNote(year, prevYear)}
${agentT('ai.agent.timeParamsIntro', locale)}
- ${agentT('ai.agent.timeParamExample1', locale, { year })}
- ${agentT('ai.agent.timeParamExample2', locale, { year })}
- ${agentT('ai.agent.timeParamExample3', locale, { year })}
${agentT('ai.agent.defaultYearNote', locale, { year, prevYear })}
${content.responseInstruction}`
${agentT('ai.agent.responseInstruction', locale)}`
}
/**
@@ -439,10 +380,7 @@ ${content.responseInstruction}`
* 仅在前端未传递 promptConfig 时使用
*/
function getFallbackRoleDefinition(chatType: 'group' | 'private', locale: string = 'zh-CN'): string {
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
const chatTypeDesc =
chatType === 'private' ? (locale === 'zh-CN' ? '私聊' : 'private chat') : locale === 'zh-CN' ? '群聊' : 'group chat'
return content.fallbackRoleDefinition(chatTypeDesc)
return agentT(`ai.agent.fallbackRoleDefinition.${chatType}`, locale)
}
/**
@@ -450,8 +388,7 @@ function getFallbackRoleDefinition(chatType: 'group' | 'private', locale: string
* 仅在前端未传递 promptConfig 时使用
*/
function getFallbackResponseRules(locale: string = 'zh-CN'): string {
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
return content.fallbackResponseRules
return agentT('ai.agent.fallbackResponseRules', locale)
}
/**
@@ -471,8 +408,6 @@ function buildSystemPrompt(
ownerInfo?: OwnerInfo,
locale: string = 'zh-CN'
): string {
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
// 使用用户配置或 fallback
const roleDefinition = promptConfig?.roleDefinition || getFallbackRoleDefinition(chatType, locale)
const responseRules = promptConfig?.responseRules || getFallbackResponseRules(locale)
@@ -485,7 +420,7 @@ function buildSystemPrompt(
${lockedSection}
${content.responseRulesTitle}
${agentT('ai.agent.responseRulesTitle', locale)}
${responseRules}`
}
@@ -634,7 +569,7 @@ export class Agent {
aiLogger.warn('Agent', '达到最大工具调用轮数', { maxRounds: this.config.maxToolRounds })
this.messages.push({
role: 'user',
content: '请根据已获取的信息给出回答,不要再调用工具。',
content: agentT('ai.agent.answerWithoutTools', this.locale),
})
const finalResponse = await chat(this.messages, this.config.llmOptions)
@@ -877,7 +812,7 @@ export class Agent {
this.messages.push({
role: 'user',
content: '请根据已获取的信息给出回答,不要再调用工具。',
content: agentT('ai.agent.answerWithoutTools', this.locale),
})
// 最后一轮不带 tools(传入 abortSignal
@@ -993,7 +928,9 @@ export class Agent {
// 添加工具结果消息
this.messages.push({
role: 'tool',
content: result.success ? JSON.stringify(result.result) : `错误: ${result.error}`,
content: result.success
? JSON.stringify(result.result)
: agentT('ai.agent.toolError', this.locale, { error: result.error }),
tool_call_id: tc.id,
})
}
+7 -6
View File
@@ -23,6 +23,7 @@ import { GeminiService, GEMINI_INFO } from './gemini'
import { OpenAICompatibleService, OPENAI_COMPATIBLE_INFO } from './openai-compatible'
import { aiLogger, extractErrorInfo, extractErrorStack } from '../logger'
import { encryptApiKey, decryptApiKey, isEncrypted } from './crypto'
import { t } from '../../i18n'
// 导出类型
export * from './types'
@@ -299,7 +300,7 @@ export function addConfig(config: Omit<AIServiceConfig, 'id' | 'createdAt' | 'up
const store = loadConfigStore()
if (store.configs.length >= MAX_CONFIG_COUNT) {
return { success: false, error: `最多只能添加 ${MAX_CONFIG_COUNT} 个配置` }
return { success: false, error: t('llm.maxConfigs', { count: MAX_CONFIG_COUNT }) }
}
const now = Date.now()
@@ -332,7 +333,7 @@ export function updateConfig(
const index = store.configs.findIndex((c) => c.id === id)
if (index === -1) {
return { success: false, error: '配置不存在' }
return { success: false, error: t('llm.configNotFound') }
}
store.configs[index] = {
@@ -353,7 +354,7 @@ export function deleteConfig(id: string): { success: boolean; error?: string } {
const index = store.configs.findIndex((c) => c.id === id)
if (index === -1) {
return { success: false, error: '配置不存在' }
return { success: false, error: t('llm.configNotFound') }
}
store.configs.splice(index, 1)
@@ -375,7 +376,7 @@ export function setActiveConfig(id: string): { success: boolean; error?: string
const config = store.configs.find((c) => c.id === id)
if (!config) {
return { success: false, error: '配置不存在' }
return { success: false, error: t('llm.configNotFound') }
}
store.activeConfigId = id
@@ -542,7 +543,7 @@ export async function chat(
const service = getCurrentLLMService()
if (!service) {
aiLogger.error('LLM', '服务未配置')
throw new Error('LLM 服务未配置,请先在设置中配置 API Key')
throw new Error(t('llm.notConfigured'))
}
try {
@@ -597,7 +598,7 @@ export async function* chatStream(messages: ChatMessage[], options?: ChatOptions
const service = getCurrentLLMService()
if (!service) {
aiLogger.error('LLM', '服务未配置(流式)')
throw new Error('LLM 服务未配置,请先在设置中配置 API Key')
throw new Error(t('llm.notConfigured'))
}
let chunkCount = 0
+73 -25
View File
@@ -11,6 +11,7 @@ import Database from 'better-sqlite3'
import { chat } from '../llm'
import { getDbPath, openDatabase } from '../../database/core'
import { aiLogger } from '../logger'
import { t } from '../../i18n'
/** 最小消息数阈值(少于此数量不生成摘要) */
const MIN_MESSAGE_COUNT = 3
@@ -122,6 +123,7 @@ function getSummaryLengthLimit(messageCount: number): number {
/**
* 判断消息是否有意义(用于过滤)
* 支持中英文内容过滤
*/
function isValidMessage(content: string): boolean {
const trimmed = content.trim()
@@ -129,19 +131,45 @@ function isValidMessage(content: string): boolean {
// 过滤空内容
if (!trimmed) return false
// 过滤单字/双字无意义回复
// 过滤单字/双字无意义回复(中文)
if (trimmed.length <= 2) {
// 允许一些有意义的短词
const meaningfulShort = ['好的', '不是', '是的', '可以', '不行', '好吧', '明白', '知道', '同意']
if (!meaningfulShort.includes(trimmed)) return false
const meaningfulShortZh = ['好的', '不是', '是的', '可以', '不行', '好吧', '明白', '知道', '同意']
if (!meaningfulShortZh.includes(trimmed)) return false
}
// 过滤短无意义回复(英文,不区分大小写)
const lowerTrimmed = trimmed.toLowerCase()
const meaninglessShortEn = [
'ok',
'k',
'yes',
'no',
'ya',
'yep',
'nope',
'lol',
'haha',
'hehe',
'hmm',
'ah',
'oh',
'wow',
'thx',
'ty',
'np',
'gg',
'brb',
'idk',
]
if (meaninglessShortEn.includes(lowerTrimmed)) return false
// 过滤纯表情消息
const emojiOnlyPattern = /^[\p{Emoji}\s[\]()]+$/u
if (emojiOnlyPattern.test(trimmed)) return false
// 过滤占位符文本
// 过滤占位符文本(中文 + 英文)
const placeholders = [
// 中文占位符(QQ/微信导出格式)
'[图片]',
'[语音]',
'[视频]',
@@ -153,12 +181,38 @@ function isValidMessage(content: string): boolean {
'[红包]',
'[转账]',
'[撤回消息]',
// 英文占位符
'[image]',
'[voice]',
'[video]',
'[file]',
'[sticker]',
'[animated sticker]',
'[location]',
'[contact]',
'[red packet]',
'[transfer]',
'[recalled message]',
'[photo]',
'[audio]',
'[gif]',
]
if (placeholders.some((p) => trimmed === p)) return false
if (placeholders.some((p) => lowerTrimmed === p.toLowerCase())) return false
// 过滤系统消息(入群、退群等)
const systemPatterns = [/^.*邀请.*加入了群聊$/, /^.*退出了群聊$/, /^.*撤回了一条消息$/, /^你撤回了一条消息$/]
if (systemPatterns.some((p) => p.test(trimmed))) return false
// 过滤系统消息(中文:入群、退群等)
const systemPatternsZh = [/^.*邀请.*加入了群聊$/, /^.*退出了群聊$/, /^.*撤回了一条消息$/, /^你撤回了一条消息$/]
if (systemPatternsZh.some((p) => p.test(trimmed))) return false
// 过滤系统消息(英文)
const systemPatternsEn = [
/^.*invited.*to the group$/i,
/^.*left the group$/i,
/^.*recalled a message$/i,
/^you recalled a message$/i,
/^.*joined the group$/i,
/^.*has been removed$/i,
]
if (systemPatternsEn.some((p) => p.test(trimmed))) return false
return true
}
@@ -282,17 +336,14 @@ export async function generateSessionSummary(
// 2. 获取会话消息
const sessionData = getSessionMessagesForSummary(dbSessionId, chatSessionId)
if (!sessionData) {
return { success: false, error: '会话不存在或数据库打开失败' }
return { success: false, error: t('summary.sessionNotFound') }
}
// 3. 检查消息数量
if (sessionData.messageCount < MIN_MESSAGE_COUNT) {
return {
success: false,
error:
locale === 'zh-CN'
? `消息数量少于${MIN_MESSAGE_COUNT}条,无需生成摘要`
: `Message count less than ${MIN_MESSAGE_COUNT}, no need to generate summary`,
error: t('summary.tooFewMessages', { count: MIN_MESSAGE_COUNT }),
}
}
@@ -301,10 +352,7 @@ export async function generateSessionSummary(
if (validMessages.length < MIN_MESSAGE_COUNT) {
return {
success: false,
error:
locale === 'zh-CN'
? `有效消息数量少于${MIN_MESSAGE_COUNT}条,无需生成摘要`
: `Valid message count less than ${MIN_MESSAGE_COUNT}, no need to generate summary`,
error: t('summary.tooFewValidMessages', { count: MIN_MESSAGE_COUNT }),
}
}
@@ -362,7 +410,7 @@ export async function generateSessionSummary(
async function generateDirectSummary(content: string, lengthLimit: number, locale: string): Promise<string> {
const response = await chat(
[
{ role: 'system', content: '你是一个对话摘要专家,擅长用简洁的语言总结对话内容。' },
{ role: 'system', content: t('summary.systemPromptDirect') },
{ role: 'user', content: buildSummaryPrompt(content, lengthLimit, locale) },
],
{
@@ -391,7 +439,7 @@ async function generateMapReduceSummary(
const segmentContent = formatMessages(segments[i])
const response = await chat(
[
{ role: 'system', content: '你是一个对话摘要专家,擅长用简洁的语言总结对话内容。' },
{ role: 'system', content: t('summary.systemPromptDirect') },
{ role: 'user', content: buildSubSummaryPrompt(segmentContent, locale) },
],
{
@@ -409,7 +457,7 @@ async function generateMapReduceSummary(
const mergeResponse = await chat(
[
{ role: 'system', content: '你是一个对话摘要专家,擅长将多个摘要合并成一个连贯的总结。' },
{ role: 'system', content: t('summary.systemPromptMerge') },
{ role: 'user', content: buildMergePrompt(subSummaries, lengthLimit, locale) },
],
{
@@ -447,7 +495,7 @@ export async function generateSessionSummaries(
if (result.success) {
success++
} else if (result.error?.includes('少于') || result.error?.includes('less than')) {
} else if (result.error?.includes('少于') || result.error?.includes('less than') || result.error?.includes('few')) {
skipped++
} else {
failed++
@@ -479,20 +527,20 @@ export function checkSessionsCanGenerateSummary(
const sessionData = getSessionMessagesForSummary(dbSessionId, chatSessionId)
if (!sessionData) {
results.set(chatSessionId, { canGenerate: false, reason: '会话不存在' })
results.set(chatSessionId, { canGenerate: false, reason: t('summary.sessionNotExist') })
continue
}
// 检查原始消息数量
if (sessionData.messageCount < MIN_MESSAGE_COUNT) {
results.set(chatSessionId, { canGenerate: false, reason: '消息太少' })
results.set(chatSessionId, { canGenerate: false, reason: t('summary.messagesTooFew') })
continue
}
// 预处理:过滤无意义消息
const validMessages = preprocessMessages(sessionData.messages)
if (validMessages.length < MIN_MESSAGE_COUNT) {
results.set(chatSessionId, { canGenerate: false, reason: '有效消息太少' })
results.set(chatSessionId, { canGenerate: false, reason: t('summary.validMessagesTooFew') })
continue
}
+49 -6
View File
@@ -6,6 +6,7 @@
import type { ToolDefinition, ToolCall } from '../llm/types'
import type { ToolRegistry, RegisteredTool, ToolContext, ToolExecutionResult, ToolExecutor } from './types'
import { isEmbeddingEnabled } from '../rag'
import { t as i18nT } from '../../i18n'
// 导出类型
export * from './types'
@@ -44,23 +45,65 @@ export async function ensureToolsInitialized(): Promise<void> {
return initPromise
}
/**
* 翻译工具定义的 description 和参数 description
* 使用 i18next 查找翻译,如果未找到则保留原始文本(中文)
*
* i18n 键命名规则:
* - 工具描述:ai.tools.{toolName}.desc
* - 参数描述:ai.tools.{toolName}.params.{paramName}
*/
function translateToolDefinition(tool: ToolDefinition): ToolDefinition {
const name = tool.function.name
const descKey = `ai.tools.${name}.desc`
const translatedDesc = i18nT(descKey)
// 深拷贝并翻译参数描述
const translatedProperties: typeof tool.function.parameters.properties = {}
for (const [paramName, param] of Object.entries(tool.function.parameters.properties)) {
const paramKey = `ai.tools.${name}.params.${paramName}`
const translatedParamDesc = i18nT(paramKey)
translatedProperties[paramName] = {
...param,
// 如果 i18next 返回的是 key 本身,说明没有找到翻译,保留原始文本
description: translatedParamDesc !== paramKey ? translatedParamDesc : param.description,
}
}
return {
type: tool.type,
function: {
name: tool.function.name,
// 如果 i18next 返回的是 key 本身,说明没有找到翻译,保留原始文本
description: translatedDesc !== descKey ? translatedDesc : tool.function.description,
parameters: {
type: tool.function.parameters.type,
properties: translatedProperties,
required: tool.function.parameters.required,
},
},
}
}
/**
* 获取所有已注册的工具定义
* 根据配置动态过滤工具(如:语义搜索工具仅在启用 Embedding 时可用)
* 根据当前 locale 动态翻译工具描述(解决"响应式"陷阱:每次调用时实时翻译)
* @returns 工具定义数组(用于传给 LLM)
*/
export async function getAllToolDefinitions(): Promise<ToolDefinition[]> {
await ensureToolsInitialized()
const allTools = Array.from(toolRegistry.values()).map((t) => t.definition)
const allTools = Array.from(toolRegistry.values()).map((reg) => reg.definition)
// 根据 Embedding 配置决定是否包含语义搜索工具
const embeddingEnabled = isEmbeddingEnabled()
if (!embeddingEnabled) {
return allTools.filter((t) => t.function.name !== 'semantic_search_messages')
}
const filteredTools = embeddingEnabled
? allTools
: allTools.filter((tool) => tool.function.name !== 'semantic_search_messages')
return allTools
// 所有 locale 统一走翻译层,确保 locale 文件同构
return filteredTools.map(translateToolDefinition)
}
/**
@@ -87,7 +130,7 @@ export async function executeToolCall(toolCall: ToolCall, context: ToolContext):
return {
toolName,
success: false,
error: `工具 "${toolName}" 未注册`,
error: i18nT('tools.notRegistered', { toolName }),
}
}
+15 -14
View File
@@ -11,15 +11,16 @@
*/
import type Database from 'better-sqlite3'
import { t } from '../i18n'
/** 迁移脚本接口 */
interface Migration {
/** 版本号(必须递增) */
version: number
/** 迁移描述(技术说明) */
description: string
/** 用户可读的升级原因(显示在弹窗中) */
userMessage: string
/** 迁移描述 i18n key(技术说明) */
descriptionKey: string
/** 用户可读的升级原因 i18n key(显示在弹窗中) */
userMessageKey: string
/** 迁移执行函数 */
up: (db: Database.Database) => void
}
@@ -43,8 +44,8 @@ export const CURRENT_SCHEMA_VERSION = 3
const migrations: Migration[] = [
{
version: 1,
description: '添加 owner_id 字段到 meta 表',
userMessage: '支持「Owner」功能,可在成员列表中设置自己的身份',
descriptionKey: 'database.migrationV1Desc',
userMessageKey: 'database.migrationV1Message',
up: (db) => {
// 检查 owner_id 列是否已存在(防止重复执行)
const tableInfo = db.prepare('PRAGMA table_info(meta)').all() as Array<{ name: string }>
@@ -56,8 +57,8 @@ const migrations: Migration[] = [
},
{
version: 2,
description: '添加 roles、reply_to_message_id、platform_message_id 字段',
userMessage: '支持成员角色、消息回复关系和回复内容预览',
descriptionKey: 'database.migrationV2Desc',
userMessageKey: 'database.migrationV2Message',
up: (db) => {
// 检查 roles 列是否已存在(防止重复执行)
const memberTableInfo = db.prepare('PRAGMA table_info(member)').all() as Array<{ name: string }>
@@ -91,8 +92,8 @@ const migrations: Migration[] = [
},
{
version: 3,
description: '添加会话索引相关表(chat_session、message_context)和 session_gap_threshold 字段',
userMessage: '支持会话时间轴浏览和 AI 增强分析功能',
descriptionKey: 'database.migrationV3Desc',
userMessageKey: 'database.migrationV3Message',
up: (db) => {
// 创建 chat_session 会话表
db.exec(`
@@ -192,14 +193,14 @@ function checkDatabaseIntegrity(db: Database.Database): { valid: boolean; error?
if (tables.length === 0) {
return {
valid: false,
error: '数据库结构不完整:缺少 meta 表。建议删除此数据库文件后重新导入。',
error: t('database.integrityError'),
}
}
return { valid: true }
} catch (error) {
return {
valid: false,
error: `数据库检查失败: ${error instanceof Error ? error.message : String(error)}`,
error: t('database.checkFailed', { error: error instanceof Error ? error.message : String(error) }),
}
}
}
@@ -264,7 +265,7 @@ export function getPendingMigrationInfos(fromVersion = 0): MigrationInfo[] {
.filter((m) => m.version > fromVersion)
.map((m) => ({
version: m.version,
description: m.description,
userMessage: m.userMessage,
description: t(m.descriptionKey),
userMessage: t(m.userMessageKey),
}))
}
+99
View File
@@ -0,0 +1,99 @@
/**
* 主进程国际化模块
*
* 基于 i18next,提供主进程的多语言支持。
* 语言设置持久化在 settings/locale.json 中,
* 并通过 IPC 'locale:change' 与渲染进程同步。
*/
import i18next from 'i18next'
import { app, ipcMain } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import { getSettingsDir, ensureDir } from '../paths'
import zhCN from './locales/zh-CN'
import enUS from './locales/en-US'
const LOCALE_FILE = 'locale.json'
/**
* 获取 locale 配置文件路径
*/
function getLocaleFilePath(): string {
return path.join(getSettingsDir(), LOCALE_FILE)
}
/**
* 保存语言设置到文件
*/
function saveLocale(lng: string): void {
try {
ensureDir(getSettingsDir())
fs.writeFileSync(getLocaleFilePath(), JSON.stringify({ locale: lng }, null, 2), 'utf-8')
} catch (err) {
console.error('[i18n] Failed to save locale:', err)
}
}
/**
* 初始化主进程国际化
*
* 优先级:settings/locale.json > app.getLocale() 系统检测 > en-US 默认
* 同时注册 IPC 监听器接收渲染进程的语言切换请求
*/
export async function initLocale(): Promise<void> {
let lng = 'en-US' // 默认回退
try {
const filePath = getLocaleFilePath()
if (fs.existsSync(filePath)) {
// 读取用户保存的语言偏好
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
if (data.locale) lng = data.locale
} else {
// 无配置文件,探测系统语言
const sysLocale = app.getLocale()
lng = sysLocale.startsWith('zh') ? 'zh-CN' : 'en-US'
}
} catch (e) {
console.error('[i18n] Error loading locale config:', e)
}
await i18next.init({
lng,
fallbackLng: 'en-US',
resources: {
'zh-CN': { translation: zhCN },
'en-US': { translation: enUS },
},
interpolation: { escapeValue: false }, // Node 环境不需要防 XSS
})
console.log(`[i18n] Initialized with locale: ${lng}`)
// 监听渲染进程的语言切换请求(补全半完成的 IPC 机制)
ipcMain.on('locale:change', async (_event, newLocale: string) => {
if (newLocale !== i18next.language) {
await i18next.changeLanguage(newLocale)
saveLocale(newLocale)
console.log(`[i18n] Locale changed to: ${newLocale}`)
}
})
}
/**
* 翻译函数
* @param key 翻译 key,如 'update.newVersionTitle'
* @param options 插值参数,如 { version: '1.0.0' }
*/
export const t = (key: string, options?: Record<string, unknown>): string => i18next.t(key, options)
/**
* 获取当前 locale
*/
export const getLocale = (): string => i18next.language
/**
* 判断当前是否为中文环境(兼容现有 isChineseLocale 模式)
*/
export const isChineseLocale = (): boolean => i18next.language.startsWith('zh')
+266
View File
@@ -0,0 +1,266 @@
/**
* 主进程英文翻译
*/
export default {
// ===== Common =====
common: {
error: 'Error',
},
// ===== P0: Update dialogs =====
update: {
newVersionTitle: 'New version v{{version}} available',
newVersionMessage: 'New version v{{version}} available',
newVersionDetail: 'Would you like to download and install the new version?',
downloadNow: 'Download Now',
cancel: 'Cancel',
downloadComplete: 'Download Complete',
readyToInstall: 'The new version is ready. Install now?',
install: 'Install',
remindLater: 'Remind Later',
installOnQuit: 'Later (auto-install on quit)',
upToDate: 'You are up to date',
},
// ===== P0: File/directory dialogs =====
dialog: {
selectChatFile: 'Select Chat Record File',
chatRecords: 'Chat Records',
allFiles: 'All Files',
import: 'Import',
selectDirectory: 'Select Directory',
selectFolder: 'Select Folder',
selectFolderError: 'Error selecting folder: ',
},
// ===== P1: Database migrations =====
database: {
migrationV1Desc: 'Add owner_id field to meta table',
migrationV1Message: 'Support "Owner" feature to set your identity in the member list',
migrationV2Desc: 'Add roles, reply_to_message_id, platform_message_id fields',
migrationV2Message: 'Support member roles, message reply relationships and reply preview',
migrationV3Desc: 'Add session index tables (chat_session, message_context) and session_gap_threshold field',
migrationV3Message: 'Support session timeline browsing and AI-enhanced analysis',
integrityError:
'Database structure is incomplete: missing meta table. Please delete this database file and re-import.',
checkFailed: 'Database check failed: {{error}}',
},
// ===== Tool system =====
tools: {
notRegistered: 'Tool "{{toolName}}" is not registered',
},
// ===== P2: AI Tool definitions (Function Calling) =====
ai: {
tools: {
search_messages: {
desc: 'Search group chat records by keywords. Suitable for finding specific topics or keyword-related chat content. Can specify time range and sender to filter messages. Supports minute-level time queries.',
params: {
keywords:
'List of search keywords, using OR logic to match messages containing any keyword. Pass an empty array [] to filter by sender only',
sender_id:
'Sender member ID, used to filter messages from a specific member. Can be obtained via the get_group_members tool',
limit: 'Message count limit, default 1000, max 50000',
year: 'Filter messages by year, e.g. 2024',
month: 'Filter messages by month (1-12), use with year',
day: 'Filter messages by day (1-31), use with year and month',
hour: 'Filter messages by hour (0-23), use with year, month, and day',
start_time:
'Start time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 14:00". Overrides year/month/day/hour when specified',
end_time:
'End time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 18:30". Overrides year/month/day/hour when specified',
},
},
get_recent_messages: {
desc: 'Get chat messages within a specified time period. Suitable for overview questions like "what has everyone been chatting about recently" or "what was discussed in month X". Supports minute-level time queries.',
params: {
limit: 'Message count limit, default 100 (saves tokens, can be increased as needed)',
year: 'Filter messages by year, e.g. 2024',
month: 'Filter messages by month (1-12), use with year',
day: 'Filter messages by day (1-31), use with year and month',
hour: 'Filter messages by hour (0-23), use with year, month, and day',
start_time:
'Start time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 14:00". Overrides year/month/day/hour when specified',
end_time:
'End time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 18:30". Overrides year/month/day/hour when specified',
},
},
get_member_stats: {
desc: 'Get member activity statistics. Suitable for questions like "who is the most active" or "who sends the most messages".',
params: {
top_n: 'Return top N members, default 10',
},
},
get_time_stats: {
desc: 'Get time distribution statistics of chat activity. Suitable for questions like "when is the group most active" or "what time do people usually chat".',
params: {
type: 'Statistics type: hourly (by hour), weekday (by day of week), daily (by date)',
},
},
get_group_members: {
desc: 'Get group member list, including basic info, aliases, and message statistics. Suitable for queries like "who is in the group", "what is someone\'s alias", or "whose ID is xxx".',
params: {
search: 'Optional search keyword to filter by member nickname, alias, or platform ID',
limit: 'Member count limit, returns all by default',
},
},
get_member_name_history: {
desc: 'Get member name change history. Suitable for questions like "what was someone\'s previous name", "name changes", or "former names". Requires member ID from get_group_members tool first.',
params: {
member_id: 'Member database ID, can be obtained via get_group_members tool',
},
},
get_conversation_between: {
desc: 'Get conversation records between two group members. Suitable for questions like "what did A and B talk about" or "view the conversation between two people". Requires member IDs from get_group_members first. Supports minute-level time queries.',
params: {
member_id_1: 'Database ID of the first member',
member_id_2: 'Database ID of the second member',
limit: 'Message count limit, default 100',
year: 'Filter messages by year',
month: 'Filter messages by month (1-12), use with year',
day: 'Filter messages by day (1-31), use with year and month',
hour: 'Filter messages by hour (0-23), use with year, month, and day',
start_time:
'Start time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 14:00". Overrides year/month/day/hour when specified',
end_time:
'End time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 18:30". Overrides year/month/day/hour when specified',
},
},
get_message_context: {
desc: 'Get surrounding context messages for a given message ID. Suitable for viewing what was discussed before and after a specific message. Supports single or batch message IDs.',
params: {
message_ids:
'List of message IDs to query context for. Can be single or multiple IDs. Message IDs can be obtained from search_messages and other tool results',
context_size: 'Context size, i.e. how many messages before and after to retrieve, default 20',
},
},
search_sessions: {
desc: 'Search chat sessions (conversation segments). Sessions are conversation units automatically split by message time intervals. Suitable for finding discussions on specific topics or understanding how many conversations occurred in a time period. Returns matching sessions with a 5-message preview each.',
params: {
keywords: 'Optional keyword list, only returns sessions containing these keywords (OR logic)',
limit: 'Session count limit, default 20',
year: 'Filter sessions by year, e.g. 2024',
month: 'Filter sessions by month (1-12), use with year',
day: 'Filter sessions by day (1-31), use with year and month',
start_time: 'Start time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 14:00"',
end_time: 'End time, format "YYYY-MM-DD HH:mm", e.g. "2024-03-15 18:30"',
},
},
get_session_messages: {
desc: 'Get the complete message list for a specific session. Used to get full context after finding a relevant session via search_sessions. Returns all messages and participant information.',
params: {
session_id: 'Session ID, can be obtained from search_sessions results',
limit: 'Message count limit, default 1000. Can be limited for very long sessions to save tokens',
},
},
get_session_summaries: {
desc: `Get session summary list to quickly understand discussion topics in chat history.
Use cases:
1. Understand what topics have been discussed recently
2. Search for discussed topics by keyword
3. Overview questions like "has the group discussed travel"
Returned summaries are brief descriptions of each session, helping quickly locate sessions of interest. Use get_session_messages for details.`,
params: {
keywords: 'Keyword list to search within summaries (OR logic)',
limit: 'Session count limit, default 20',
year: 'Filter sessions by year',
month: 'Filter sessions by month (1-12)',
day: 'Filter sessions by day (1-31)',
start_time: 'Start time, format "YYYY-MM-DD HH:mm"',
end_time: 'End time, format "YYYY-MM-DD HH:mm"',
},
},
semantic_search_messages: {
desc: `Search historical conversations using Embedding vector similarity, understanding semantics rather than keyword matching.
⚠️ Use cases (prefer search_messages for keyword search, consider this tool for the following):
1. Finding "similar expressions": e.g. "has anyone said something like 'I miss you'"
2. Insufficient keyword results: when search_messages returns too few or irrelevant results
3. Vague sentiment/relationship analysis: e.g. "how does the other person feel about me"
❌ Not suitable for (use search_messages):
- Searches with specific keywords (e.g. "travel", "birthday")
- Finding specific person's messages
- Finding messages in specific time periods`,
params: {
query: 'Semantic search query, describe in natural language what type of content you are looking for',
top_k: 'Number of results to return, default 10 (recommended 5-20)',
candidate_limit: 'Number of candidate sessions, default 50 (larger is slower but potentially more accurate)',
year: 'Filter sessions by year',
month: 'Filter sessions by month (1-12)',
day: 'Filter sessions by day (1-31)',
start_time: 'Start time, format "YYYY-MM-DD HH:mm"',
end_time: 'End time, format "YYYY-MM-DD HH:mm"',
},
},
},
// ===== AI Agent system prompts =====
agent: {
answerWithoutTools: 'Please answer based on the information already retrieved, do not call any more tools.',
toolError: 'Error: {{error}}',
currentDateIs: 'Current date is',
chatContext: {
private: 'conversation',
group: 'group chat',
},
ownerNote: `Current user identity:
- The user's identity in this {{chatContext}} is "{{displayName}}" (platformId: {{platformId}})
- When the user refers to "I" or "my", it refers to "{{displayName}}"
- When querying "my" messages, use the sender_id parameter to filter for this member
`,
memberNotePrivate: `Member query strategy:
- Private chats only have two participants, so the member list can be directly obtained
- When the user refers to "the other party" or "he/she", get the other participant's information via get_group_members
`,
memberNoteGroup: `Member query strategy:
- When the user refers to specific group members (e.g., "what did John say", "Mary's messages"), first call get_group_members to get the member list
- Group members have three names: accountName (original nickname), groupNickname (group nickname), aliases (user-defined aliases)
- The search parameter of get_group_members can be used for fuzzy searching these three names
- Once a member is found, use their id field as the sender_id parameter for search_messages to retrieve their messages
`,
timeParamsIntro: 'Time parameters: combine year/month/day/hour based on user mention',
timeParamExample1: '"October" → year: {{year}}, month: 10',
timeParamExample2: '"October 1st" → year: {{year}}, month: 10, day: 1',
timeParamExample3: '"October 1st 3 PM" → year: {{year}}, month: 10, day: 1, hour: 15',
defaultYearNote:
'If year is not specified, defaults to {{year}}. If the month has not yet occurred, {{prevYear}} is used.',
responseInstruction:
"Based on the user's question, select appropriate tools to retrieve data, then provide an answer based on the data.",
responseRulesTitle: 'Response requirements:',
fallbackRoleDefinition: {
group: `You are a professional group chat analysis assistant.
Your task is to help users understand and analyze their group chat data.`,
private: `You are a professional private chat analysis assistant.
Your task is to help users understand and analyze their private chat data.`,
},
fallbackResponseRules: `1. Answer based on data returned by tools, do not fabricate information
2. If data is insufficient to answer, please state so
3. Keep answers concise and clear, use Markdown format`,
},
},
// ===== P3: LLM config =====
llm: {
notConfigured: 'LLM service not configured. Please set up an API Key in settings first.',
maxConfigs: 'Maximum of {{count}} configurations allowed',
configNotFound: 'Configuration not found',
noActiveConfig: 'No active configuration',
},
// ===== P4: Summary generation =====
summary: {
sessionNotFound: 'Session not found or database could not be opened',
tooFewMessages: 'Message count less than {{count}}, no need to generate summary',
tooFewValidMessages: 'Valid message count less than {{count}}, no need to generate summary',
sessionNotExist: 'Session not found',
messagesTooFew: 'Too few messages',
validMessagesTooFew: 'Too few valid messages',
systemPromptDirect: 'You are a conversation summarization expert. Summarize conversations concisely.',
systemPromptMerge:
'You are a conversation summarization expert skilled at merging multiple summaries into a coherent overview.',
},
}
+262
View File
@@ -0,0 +1,262 @@
/**
* 主进程中文翻译
*/
export default {
// ===== 通用 =====
common: {
error: '错误',
},
// ===== P0: 更新弹窗 =====
update: {
newVersionTitle: '发现新版本 v{{version}}',
newVersionMessage: '发现新版本 v{{version}}',
newVersionDetail: '是否立即下载并安装新版本?',
downloadNow: '立即下载',
cancel: '取消',
downloadComplete: '下载完成',
readyToInstall: '新版本已准备就绪,是否现在安装?',
install: '安装',
remindLater: '之后提醒',
installOnQuit: '稍后(应用退出后自动安装)',
upToDate: '已是最新版本',
},
// ===== P0: 文件/目录对话框 =====
dialog: {
selectChatFile: '选择聊天记录文件',
chatRecords: '聊天记录',
allFiles: '所有文件',
import: '导入',
selectDirectory: '选择目录',
selectFolder: '选择文件夹',
selectFolderError: '选择文件夹时发生错误:',
},
// ===== P1: 数据库迁移 =====
database: {
migrationV1Desc: '添加 owner_id 字段到 meta 表',
migrationV1Message: '支持「Owner」功能,可在成员列表中设置自己的身份',
migrationV2Desc: '添加 roles、reply_to_message_id、platform_message_id 字段',
migrationV2Message: '支持成员角色、消息回复关系和回复内容预览',
migrationV3Desc: '添加会话索引相关表(chat_session、message_context)和 session_gap_threshold 字段',
migrationV3Message: '支持会话时间轴浏览和 AI 增强分析功能',
integrityError: '数据库结构不完整:缺少 meta 表。建议删除此数据库文件后重新导入。',
checkFailed: '数据库检查失败: {{error}}',
},
// ===== 工具系统 =====
tools: {
notRegistered: '工具 "{{toolName}}" 未注册',
},
// ===== P2: AI 工具描述(Function Calling =====
ai: {
tools: {
search_messages: {
desc: '根据关键词搜索群聊记录。适用于用户想要查找特定话题、关键词相关的聊天内容。可以指定时间范围和发送者来筛选消息。支持精确到分钟级别的时间查询。',
params: {
keywords: '搜索关键词列表,会用 OR 逻辑匹配包含任一关键词的消息。如果只需要按发送者筛选,可以传空数组 []',
sender_id: '发送者的成员 ID,用于筛选特定成员发送的消息。可以通过 get_group_members 工具获取成员 ID',
limit: '返回消息数量限制,默认 1000,最大 50000',
year: '筛选指定年份的消息,如 2024',
month: '筛选指定月份的消息(1-12),需要配合 year 使用',
day: '筛选指定日期的消息(1-31),需要配合 year 和 month 使用',
hour: '筛选指定小时的消息(0-23),需要配合 year、month 和 day 使用',
start_time:
'开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
end_time:
'结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
},
},
get_recent_messages: {
desc: '获取指定时间段内的群聊消息。适用于回答"最近大家聊了什么"、"X月群里聊了什么"等概览性问题。支持精确到分钟级别的时间查询。',
params: {
limit: '返回消息数量限制,默认 100(节省 token,可根据需要增加)',
year: '筛选指定年份的消息,如 2024',
month: '筛选指定月份的消息(1-12),需要配合 year 使用',
day: '筛选指定日期的消息(1-31),需要配合 year 和 month 使用',
hour: '筛选指定小时的消息(0-23),需要配合 year、month 和 day 使用',
start_time:
'开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
end_time:
'结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
},
},
get_member_stats: {
desc: '获取群成员的活跃度统计数据。适用于回答"谁最活跃"、"发言最多的是谁"等问题。',
params: {
top_n: '返回前 N 名成员,默认 10',
},
},
get_time_stats: {
desc: '获取群聊的时间分布统计。适用于回答"什么时候最活跃"、"大家一般几点聊天"等问题。',
params: {
type: '统计类型:hourly(按小时)、weekday(按星期)、daily(按日期)',
},
},
get_group_members: {
desc: '获取群成员列表,包括成员的基本信息、别名和消息统计。适用于查询"群里有哪些人"、"某人的别名是什么"、"谁的QQ号是xxx"等问题。',
params: {
search: '可选的搜索关键词,用于筛选成员昵称、别名或QQ号',
limit: '返回成员数量限制,默认返回全部',
},
},
get_member_name_history: {
desc: '获取成员的昵称变更历史记录。适用于回答"某人以前叫什么名字"、"某人的昵称变化"、"某人曾用名"等问题。需要先通过 get_group_members 工具获取成员 ID。',
params: {
member_id: '成员的数据库 ID,可以通过 get_group_members 工具获取',
},
},
get_conversation_between: {
desc: '获取两个群成员之间的对话记录。适用于回答"A和B之间聊了什么"、"查看两人的对话"等问题。需要先通过 get_group_members 获取成员 ID。支持精确到分钟级别的时间查询。',
params: {
member_id_1: '第一个成员的数据库 ID',
member_id_2: '第二个成员的数据库 ID',
limit: '返回消息数量限制,默认 100',
year: '筛选指定年份的消息',
month: '筛选指定月份的消息(1-12),需要配合 year 使用',
day: '筛选指定日期的消息(1-31),需要配合 year 和 month 使用',
hour: '筛选指定小时的消息(0-23),需要配合 year、month 和 day 使用',
start_time:
'开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
end_time:
'结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
},
},
get_message_context: {
desc: '根据消息 ID 获取前后的上下文消息。适用于需要查看某条消息前后聊天内容的场景,比如"这条消息的前后在聊什么"、"查看某条消息的上下文"等。支持单个或批量消息 ID。',
params: {
message_ids:
'要查询上下文的消息 ID 列表,可以是单个 ID 或多个 ID。消息 ID 可以从 search_messages 等工具的返回结果中获取',
context_size: '上下文大小,即获取前后各多少条消息,默认 20',
},
},
search_sessions: {
desc: '搜索聊天会话(对话段落)。会话是根据消息时间间隔自动切分的对话单元。适用于查找特定话题的讨论、了解某个时间段内发生了几次对话等场景。返回匹配的会话列表及每个会话的前5条消息预览。',
params: {
keywords: '可选的搜索关键词列表,只返回包含这些关键词的会话(OR 逻辑匹配)',
limit: '返回会话数量限制,默认 20',
year: '筛选指定年份的会话,如 2024',
month: '筛选指定月份的会话(1-12),需要配合 year 使用',
day: '筛选指定日期的会话(1-31),需要配合 year 和 month 使用',
start_time: '开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"',
end_time: '结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"',
},
},
get_session_messages: {
desc: '获取指定会话的完整消息列表。用于在 search_sessions 找到相关会话后,获取该会话的完整上下文。返回会话的所有消息及参与者信息。',
params: {
session_id: '会话 ID,可以从 search_sessions 的返回结果中获取',
limit: '返回消息数量限制,默认 1000。对于超长会话可以限制返回数量以节省 token',
},
},
get_session_summaries: {
desc: `获取会话摘要列表,快速了解群聊历史讨论的主题。
适用场景:
1. 了解群里最近在聊什么话题
2. 按关键词搜索讨论过的话题
3. 概览性问题如"群里有没有讨论过旅游"
返回的摘要是对每个会话的简短总结,可以帮助快速定位感兴趣的会话,然后用 get_session_messages 获取详情。`,
params: {
keywords: '在摘要中搜索的关键词列表(OR 逻辑匹配)',
limit: '返回会话数量限制,默认 20',
year: '筛选指定年份的会话',
month: '筛选指定月份的会话(1-12',
day: '筛选指定日期的会话(1-31',
start_time: '开始时间,格式 "YYYY-MM-DD HH:mm"',
end_time: '结束时间,格式 "YYYY-MM-DD HH:mm"',
},
},
semantic_search_messages: {
desc: `使用 Embedding 向量相似度搜索历史对话,理解语义而非关键词匹配。
⚠️ 使用场景(优先使用 search_messages 关键词搜索,以下场景再考虑本工具):
1. 找"类似的话"或"类似的表达":如"有没有说过类似'我想你了'这样的话"
2. 关键词搜索结果不足:当 search_messages 返回结果太少或不相关时,可用本工具补充
3. 模糊的情感/关系分析:如"对方对我的态度是怎样的"、"我们之间的氛围"
❌ 不适合的场景(请用 search_messages):
- 有明确关键词的搜索(如"旅游"、"生日"、"加班"
- 查找特定人物的发言
- 查找特定时间段的消息`,
params: {
query: '语义检索查询,用自然语言描述你想要找的内容类型',
top_k: '返回结果数量,默认 10(建议 5-20)',
candidate_limit: '候选会话数量,默认 50(越大越慢但可能更准确)',
year: '筛选指定年份的会话',
month: '筛选指定月份的会话(1-12',
day: '筛选指定日期的会话(1-31',
start_time: '开始时间,格式 "YYYY-MM-DD HH:mm"',
end_time: '结束时间,格式 "YYYY-MM-DD HH:mm"',
},
},
},
// ===== AI Agent 系统提示词 =====
agent: {
answerWithoutTools: '请根据已获取的信息给出回答,不要再调用工具。',
toolError: '错误: {{error}}',
currentDateIs: '当前日期是',
chatContext: {
private: '对话',
group: '群聊',
},
ownerNote: `当前用户身份:
- 用户在{{chatContext}}中的身份是「{{displayName}}」(platformId: {{platformId}}
- 当用户提到"我"、"我的"时,指的就是「{{displayName}}」
- 查询"我"的发言时,使用 sender_id 参数筛选该成员
`,
memberNotePrivate: `成员查询策略:
- 私聊只有两个人,可以直接获取成员列表
- 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息
`,
memberNoteGroup: `成员查询策略:
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表
- 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言
`,
timeParamsIntro: '时间参数:按用户提到的精度组合 year/month/day/hour',
timeParamExample1: '"10月" → year: {{year}}, month: 10',
timeParamExample2: '"10月1号" → year: {{year}}, month: 10, day: 1',
timeParamExample3: '"10月1号下午3点" → year: {{year}}, month: 10, day: 1, hour: 15',
defaultYearNote: '未指定年份默认{{year}}年,若该月份未到则用{{prevYear}}年',
responseInstruction: '根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。',
responseRulesTitle: '回答要求:',
fallbackRoleDefinition: {
group: `你是一个专业但风格轻松的群聊记录分析助手。
你的任务是帮助用户理解和分析他们的群聊记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。`,
private: `你是一个专业但风格轻松的私聊记录分析助手。
你的任务是帮助用户理解和分析他们的私聊记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。`,
},
fallbackResponseRules: `1. 基于工具返回的数据回答,不要编造信息
2. 如果数据不足以回答问题,请说明
3. 回答要简洁明了,使用 Markdown 格式
4. 可以适度加入 B 站/网络热梗、表情/颜文字(强度适中)
5. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达`,
},
},
// ===== P3: LLM 配置 =====
llm: {
notConfigured: 'LLM 服务未配置,请先在设置中配置 API Key',
maxConfigs: '最多只能添加 {{count}} 个配置',
configNotFound: '配置不存在',
noActiveConfig: '没有激活的配置',
},
// ===== P4: 摘要生成 =====
summary: {
sessionNotFound: '会话不存在或数据库打开失败',
tooFewMessages: '消息数量少于{{count}}条,无需生成摘要',
tooFewValidMessages: '有效消息数量少于{{count}}条,无需生成摘要',
sessionNotExist: '会话不存在',
messagesTooFew: '消息太少',
validMessagesTooFew: '有效消息太少',
systemPromptDirect: '你是一个对话摘要专家,擅长用简洁的语言总结对话内容。',
systemPromptMerge: '你是一个对话摘要专家,擅长将多个摘要合并成一个连贯的总结。',
},
}
+4
View File
@@ -7,6 +7,7 @@ import { initAnalytics, trackDailyActive } from './analytics'
import { initProxy } from './network/proxy'
import { needsLegacyMigration, migrateFromLegacyDir, ensureAppDirs, cleanupPendingDeleteDir } from './paths'
import { migrateAllDatabases, checkMigrationNeeded } from './database/core'
import { initLocale } from './i18n'
class MainProcess {
mainWindow: BrowserWindow | null
@@ -58,6 +59,9 @@ class MainProcess {
// 确保应用目录存在
ensureAppDirs()
// 初始化主进程国际化(在 ensureAppDirs 之后,确保 settings 目录存在)
await initLocale()
// 执行数据库 schema 迁移(确保所有数据库在 Worker 查询前已是最新 schema
this.migrateDatabasesIfNeeded()
+2 -1
View File
@@ -8,6 +8,7 @@ import * as rag from '../ai/rag'
import { aiLogger } from '../ai/logger'
import { getLogsDir } from '../paths'
import { Agent, type AgentStreamChunk, type PromptConfig } from '../ai/agent'
import { t } from '../i18n'
import type { ToolContext } from '../ai/tools/types'
import type { IpcContext } from './types'
@@ -374,7 +375,7 @@ export function registerAIHandlers({ win }: IpcContext): void {
if (activeConfig) {
return llm.deleteConfig(activeConfig.id)
}
return { success: false, error: '没有激活的配置' }
return { success: false, error: t('llm.noActiveConfig') }
}
return llm.deleteConfig(id)
} catch (error) {
+5 -4
View File
@@ -10,6 +10,7 @@ import { detectFormat, diagnoseFormat, scanMultiChatFile, type ParseProgress } f
import type { IpcContext } from './types'
import { CURRENT_SCHEMA_VERSION, getPendingMigrationInfos, type MigrationInfo } from '../database/migrations'
import { exportSessionToTempFile, cleanupTempExportFiles } from '../merger'
import { t } from '../i18n'
/**
* 注册聊天记录相关 IPC 处理器
@@ -60,14 +61,14 @@ export function registerChatHandlers(ctx: IpcContext): void {
ipcMain.handle('chat:selectFile', async () => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: '选择聊天记录文件',
title: t('dialog.selectChatFile'),
defaultPath: app.getPath('documents'),
properties: ['openFile'],
filters: [
{ name: '聊天记录', extensions: ['json', 'jsonl', 'txt'] },
{ name: '所有文件', extensions: ['*'] },
{ name: t('dialog.chatRecords'), extensions: ['json', 'jsonl', 'txt'] },
{ name: t('dialog.allFiles'), extensions: ['*'] },
],
buttonLabel: '导入',
buttonLabel: t('dialog.import'),
})
if (canceled || filePaths.length === 0) {
+4 -3
View File
@@ -6,6 +6,7 @@ import { ipcMain, app, dialog, clipboard, shell } from 'electron'
import * as fs from 'fs/promises'
import type { IpcContext } from './types'
import { simulateUpdateDialog, manualCheckForUpdates } from '../update'
import { t } from '../i18n'
/**
* 注册窗口和文件系统操作 IPC 处理器
@@ -153,17 +154,17 @@ export function registerWindowHandlers(ctx: IpcContext): void {
ipcMain.handle('selectDir', async (_, defaultPath = '') => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: '选择目录',
title: t('dialog.selectDirectory'),
defaultPath: defaultPath || app.getPath('documents'),
properties: ['openDirectory', 'createDirectory'],
buttonLabel: '选择文件夹',
buttonLabel: t('dialog.selectFolder'),
})
if (!canceled) {
return filePaths[0]
}
return null
} catch (err) {
console.error('选择文件夹时发生错误:', err)
console.error(t('dialog.selectFolderError'), err)
throw err
}
})
+13 -12
View File
@@ -4,6 +4,7 @@ import { platform } from '@electron-toolkit/utils'
import { logger } from './logger'
import { getActiveProxyUrl } from './network/proxy'
import { closeWorkerAsync } from './worker/workerManager'
import { t } from './i18n'
// R2 镜像源 URL(速度更快,作为主要更新源)
const R2_MIRROR_URL = 'https://chatlab.1app.top/releases/download'
@@ -143,10 +144,10 @@ const checkUpdate = (win) => {
dialog
.showMessageBox({
title: '发现新版本 v' + info.version,
message: '发现新版本 v' + info.version,
detail: '是否立即下载并安装新版本?',
buttons: ['立即下载', '取消'],
title: t('update.newVersionTitle', { version: info.version }),
message: t('update.newVersionMessage', { version: info.version }),
detail: t('update.newVersionDetail'),
buttons: [t('update.downloadNow'), t('update.cancel')],
defaultId: 0,
cancelId: 1,
type: 'question',
@@ -178,9 +179,9 @@ const checkUpdate = (win) => {
autoUpdater.on('update-downloaded', () => {
dialog
.showMessageBox({
title: '下载完成',
message: '新版本已准备就绪,是否现在安装?',
buttons: ['安装', platform.isMacOS ? '之后提醒' : '稍后(应用退出后自动安装)'],
title: t('update.downloadComplete'),
message: t('update.readyToInstall'),
buttons: [t('update.install'), platform.isMacOS ? t('update.remindLater') : t('update.installOnQuit')],
defaultId: 1,
cancelId: 2,
type: 'question',
@@ -219,7 +220,7 @@ const checkUpdate = (win) => {
} else {
win.webContents.send('show-message', {
type: 'success',
message: '已是最新版本',
message: t('update.upToDate'),
})
}
})
@@ -279,10 +280,10 @@ const manualCheckForUpdates = () => {
*/
const simulateUpdateDialog = (win) => {
dialog.showMessageBox({
title: '发现新版本 v9.9.9',
message: '发现新版本 v9.9.9',
detail: '是否立即下载并安装新版本?',
buttons: ['立即下载', '取消'],
title: t('update.newVersionTitle', { version: '9.9.9' }),
message: t('update.newVersionMessage', { version: '9.9.9' }),
detail: t('update.newVersionDetail'),
buttons: [t('update.downloadNow'), t('update.cancel')],
defaultId: 0,
cancelId: 1,
type: 'question',