mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-17 11:59:01 +08:00
feat: 主进程配置国际化
This commit is contained in:
+33
-96
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
@@ -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.',
|
||||
},
|
||||
}
|
||||
@@ -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: '你是一个对话摘要专家,擅长将多个摘要合并成一个连贯的总结。',
|
||||
},
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user