feat: 完成最终国际化

This commit is contained in:
digua
2026-01-07 21:40:03 +08:00
committed by digua
parent 8403125278
commit 7bb735533b
80 changed files with 2819 additions and 919 deletions
+142 -55
View File
@@ -136,14 +136,113 @@ 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 参数来获取该成员的发言
`,
toolsIntro: (chatTypeDesc: string) => `你可以使用以下工具来获取${chatTypeDesc}数据:`,
toolDescriptions: [
'search_messages - 根据关键词搜索聊天记录,支持时间筛选和发送者筛选',
'get_recent_messages - 获取指定时间段的聊天消息',
'get_member_stats - 获取成员活跃度统计',
'get_time_stats - 获取时间分布统计',
'get_group_members - 获取成员列表,包括 id、QQ号、账号名称、昵称、别名和消息统计',
'get_member_name_history - 获取成员的昵称变更历史,需要先通过 get_group_members 获取成员 ID',
'get_conversation_between - 获取两个成员之间的对话记录,需要先通过 get_group_members 获取两人的成员 ID',
'get_message_context - 根据消息 ID 获取前后的上下文消息,支持批量查询,消息 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: '回答要求:',
fallbackRoleDefinition: (chatType: string) => `你是一个专业的${chatType}记录分析助手。
你的任务是帮助用户理解和分析他们的${chatType}记录数据。`,
fallbackResponseRules: `1. 基于工具返回的数据回答,不要编造信息
2. 如果数据不足以回答问题,请说明
3. 回答要简洁明了,使用 Markdown 格式`,
},
'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
`,
toolsIntro: (chatTypeDesc: string) => `You can use the following tools to get ${chatTypeDesc} data:`,
toolDescriptions: [
'search_messages - Search chat records by keywords, supports time and sender filtering',
'get_recent_messages - Get chat messages for a specified time period',
'get_member_stats - Get member activity statistics',
'get_time_stats - Get time distribution statistics',
'get_group_members - Get member list, including ID, QQ number, account name, nickname, aliases, and message statistics',
'get_member_name_history - Get member nickname change history, requires member ID from get_group_members first',
'get_conversation_between - Get conversation records between two members, requires member IDs from get_group_members first',
'get_message_context - Get context messages before and after a message ID, supports batch queries, message ID can be obtained from other search tool results',
],
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`,
},
}
/**
* 获取系统锁定部分的提示词(工具说明、时间处理等)
* @param chatType 聊天类型 ('group' | 'private')
* @param ownerInfo Owner 信息(当前用户在对话中的身份)
* @param locale 语言设置
*/
function getLockedPromptSection(chatType: 'group' | 'private', ownerInfo?: OwnerInfo): string {
function getLockedPromptSection(
chatType: 'group' | 'private',
ownerInfo?: OwnerInfo,
locale: string = 'zh-CN'
): string {
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
const now = new Date()
const currentDate = now.toLocaleDateString('zh-CN', {
const dateLocale = locale === 'zh-CN' ? 'zh-CN' : 'en-US'
const currentDate = now.toLocaleDateString(dateLocale, {
year: 'numeric',
month: 'long',
day: 'numeric',
@@ -151,71 +250,52 @@ function getLockedPromptSection(chatType: 'group' | 'private', ownerInfo?: Owner
})
const isPrivate = chatType === 'private'
const chatTypeDesc = isPrivate ? '私聊记录' : '群聊记录'
const chatTypeDesc = content.chatTypeDesc[chatType]
const chatContext = content.chatContext[chatType]
// Owner 说明(当用户设置了"我是谁"时)
const ownerNote = ownerInfo
? `当前用户身份:
- 用户在${isPrivate ? '对话' : '群聊'}中的身份是「${ownerInfo.displayName}」(platformId: ${ownerInfo.platformId}
- 当用户提到"我"、"我的"时,指的就是「${ownerInfo.displayName}
- 查询"我"的发言时,使用 sender_id 参数筛选该成员
`
: ''
const ownerNote = ownerInfo ? content.ownerNote(ownerInfo.displayName, ownerInfo.platformId, chatContext) : ''
// 成员说明(私聊只有2人)
const memberNote = isPrivate
? `成员查询策略:
- 私聊只有两个人,可以直接获取成员列表
- 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息
`
: `成员查询策略:
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表
- 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言
`
const memberNote = isPrivate ? content.memberNotePrivate : content.memberNoteGroup
return `当前日期是 ${currentDate}
const toolsList = content.toolDescriptions.map((desc, i) => `${i + 1}. ${desc}`).join('\n')
const year = now.getFullYear()
const prevYear = year - 1
return `${content.currentDateIs} ${currentDate}
${ownerNote}
你可以使用以下工具来获取${chatTypeDesc}数据:
${content.toolsIntro(chatTypeDesc)}
1. search_messages - 根据关键词搜索聊天记录,支持时间筛选和发送者筛选
2. get_recent_messages - 获取指定时间段的聊天消息
3. get_member_stats - 获取成员活跃度统计
4. get_time_stats - 获取时间分布统计
5. get_group_members - 获取成员列表,包括 id、QQ号、账号名称、昵称、别名和消息统计
6. get_member_name_history - 获取成员的昵称变更历史,需要先通过 get_group_members 获取成员 ID
7. get_conversation_between - 获取两个成员之间的对话记录,需要先通过 get_group_members 获取两人的成员 ID
8. get_message_context - 根据消息 ID 获取前后的上下文消息,支持批量查询,消息 ID 可从其他搜索工具的返回结果中获取
${toolsList}
${memberNote}
时间参数:按用户提到的精度组合 year/month/day/hour
- "10月" → year: ${now.getFullYear()}, month: 10
- "10月1号" → year: ${now.getFullYear()}, month: 10, day: 1
- "10月1号下午3点" → year: ${now.getFullYear()}, month: 10, day: 1, hour: 15
未指定年份默认${now.getFullYear()}年,若该月份未到则用${now.getFullYear() - 1}
${content.timeParamsIntro}
- ${content.timeParamExample1(year)}
- ${content.timeParamExample2(year)}
- ${content.timeParamExample3(year)}
${content.defaultYearNote(year, prevYear)}
根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。`
${content.responseInstruction}`
}
/**
* 获取 Fallback 角色定义(主要配置来自前端 src/config/prompts.ts
* 仅在前端未传递 promptConfig 时使用
*/
function getFallbackRoleDefinition(chatType: 'group' | 'private'): string {
const chatTypeDesc = chatType === 'private' ? '私聊' : '群聊'
return `你是一个专业的${chatTypeDesc}记录分析助手。
你的任务是帮助用户理解和分析他们的${chatTypeDesc}记录数据。`
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)
}
/**
* 获取 Fallback 回答要求(主要配置来自前端 src/config/prompts.ts
* 仅在前端未传递 promptConfig 时使用
*/
function getFallbackResponseRules(): string {
return `1. 基于工具返回的数据回答,不要编造信息
2. 如果数据不足以回答问题,请说明
3. 回答要简洁明了,使用 Markdown 格式`
function getFallbackResponseRules(locale: string = 'zh-CN'): string {
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
return content.fallbackResponseRules
}
/**
@@ -227,25 +307,29 @@ function getFallbackResponseRules(): string {
* @param chatType 聊天类型 ('group' | 'private')
* @param promptConfig 用户自定义提示词配置(来自前端激活的预设)
* @param ownerInfo Owner 信息(当前用户在对话中的身份)
* @param locale 语言设置
*/
function buildSystemPrompt(
chatType: 'group' | 'private' = 'group',
promptConfig?: PromptConfig,
ownerInfo?: OwnerInfo
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)
const responseRules = promptConfig?.responseRules || getFallbackResponseRules()
const roleDefinition = promptConfig?.roleDefinition || getFallbackRoleDefinition(chatType, locale)
const responseRules = promptConfig?.responseRules || getFallbackResponseRules(locale)
// 获取锁定的系统部分(包含动态日期、工具说明和 Owner 信息)
const lockedSection = getLockedPromptSection(chatType, ownerInfo)
const lockedSection = getLockedPromptSection(chatType, ownerInfo, locale)
// 组合完整提示词
return `${roleDefinition}
${lockedSection}
回答要求:
${content.responseRulesTitle}
${responseRules}`
}
@@ -263,6 +347,7 @@ export class Agent {
private historyMessages: ChatMessage[] = []
private chatType: 'group' | 'private' = 'group'
private promptConfig?: PromptConfig
private locale: string = 'zh-CN'
/** 累计 Token 使用量 */
private totalUsage: TokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
@@ -271,13 +356,15 @@ export class Agent {
config: AgentConfig = {},
historyMessages: ChatMessage[] = [],
chatType: 'group' | 'private' = 'group',
promptConfig?: PromptConfig
promptConfig?: PromptConfig,
locale: string = 'zh-CN'
) {
this.context = context
this.abortSignal = config.abortSignal
this.historyMessages = historyMessages
this.chatType = chatType
this.promptConfig = promptConfig
this.locale = locale
this.config = {
maxToolRounds: config.maxToolRounds ?? 5,
llmOptions: config.llmOptions ?? { temperature: 0.7, maxTokens: 2048 },
@@ -315,7 +402,7 @@ export class Agent {
}
// 初始化消息(包含历史记录)
const systemPrompt = buildSystemPrompt(this.chatType, this.promptConfig, this.context.ownerInfo)
const systemPrompt = buildSystemPrompt(this.chatType, this.promptConfig, this.context.ownerInfo, this.locale)
this.messages = [
{ role: 'system', content: systemPrompt },
...this.historyMessages, // 插入历史对话
@@ -420,7 +507,7 @@ export class Agent {
}
// 初始化消息(包含历史记录)
const systemPrompt = buildSystemPrompt(this.chatType, this.promptConfig, this.context.ownerInfo)
const systemPrompt = buildSystemPrompt(this.chatType, this.promptConfig, this.context.ownerInfo, this.locale)
this.messages = [
{ role: 'system', content: systemPrompt },
...this.historyMessages, // 插入历史对话
@@ -681,8 +768,8 @@ export class Agent {
tool_calls: toolCalls,
})
// 执行工具
const results = await executeToolCalls(toolCalls, this.context)
// 执行工具(传递 locale 用于工具返回结果的国际化)
const results = await executeToolCalls(toolCalls, { ...this.context, locale: this.locale })
// 添加 tool 消息
for (let i = 0; i < toolCalls.length; i++) {
+140 -87
View File
@@ -8,6 +8,50 @@ import type { ToolDefinition } from '../llm/types'
import type { ToolContext } from './types'
import * as workerManager from '../../worker/workerManager'
// ==================== 国际化辅助函数 ====================
/**
* 判断是否使用中文
* 中文环境返回 true,其他语言(包括英文)返回 false
*/
function isChineseLocale(locale?: string): boolean {
return locale === 'zh-CN'
}
/**
* 工具返回结果的国际化文本
*/
const i18nTexts = {
allTime: { zh: '全部时间', en: 'All time' },
noContent: { zh: '[无内容]', en: '[No content]' },
memberNotFound: { zh: '未找到该成员', en: 'Member not found' },
untilNow: { zh: '至今', en: 'Present' },
noChangeRecord: { zh: '无变更记录', en: 'No change record' },
noConversation: { zh: '未找到这两人之间的对话', en: 'No conversation found between these two members' },
noMessageContext: { zh: '未找到指定的消息或上下文', en: 'Message or context not found' },
messages: { zh: '条', en: '' },
alias: { zh: '别名', en: 'Alias' },
weekdays: {
zh: ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'],
en: ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
dailySummary: {
zh: (days: number, total: number, avg: number) => `最近${days}天共${total}条,日均${avg}`,
en: (days: number, total: number, avg: number) => `Last ${days} days: ${total} messages, avg ${avg}/day`,
},
}
/**
* 获取国际化文本
*/
function t(key: keyof typeof i18nTexts, locale?: string): string | string[] {
const text = i18nTexts[key]
if (typeof text === 'object' && 'zh' in text && 'en' in text) {
return isChineseLocale(locale) ? text.zh : text.en
}
return ''
}
// ==================== 时间参数辅助函数 ====================
/**
@@ -19,7 +63,7 @@ interface ExtendedTimeParams {
day?: number
hour?: number
start_time?: string // 格式: "YYYY-MM-DD HH:mm"
end_time?: string // 格式: "YYYY-MM-DD HH:mm"
end_time?: string // 格式: "YYYY-MM-DD HH:mm"
}
/**
@@ -99,11 +143,15 @@ function parseExtendedTimeParams(
/**
* 格式化时间范围用于返回结果
*/
function formatTimeRange(timeFilter?: { startTs: number; endTs: number }): string | { start: string; end: string } {
if (!timeFilter) return '全部时间'
function formatTimeRange(
timeFilter?: { startTs: number; endTs: number },
locale?: string
): string | { start: string; end: string } {
if (!timeFilter) return t('allTime', locale) as string
const localeStr = isChineseLocale(locale) ? 'zh-CN' : 'en-US'
return {
start: new Date(timeFilter.startTs * 1000).toLocaleString('zh-CN'),
end: new Date(timeFilter.endTs * 1000).toLocaleString('zh-CN'),
start: new Date(timeFilter.startTs * 1000).toLocaleString(localeStr),
end: new Date(timeFilter.endTs * 1000).toLocaleString(localeStr),
}
}
@@ -115,14 +163,18 @@ const MAX_MESSAGE_CONTENT_LENGTH = 200
* 输出格式: "2025/3/3 07:25:04 张三: 消息内容"
* 超长内容会被截断
*/
function formatMessageCompact(msg: {
id?: number
senderName: string
content: string | null
timestamp: number
}): string {
const time = new Date(msg.timestamp * 1000).toLocaleString('zh-CN')
let content = msg.content || '[无内容]'
function formatMessageCompact(
msg: {
id?: number
senderName: string
content: string | null
timestamp: number
},
locale?: string
): string {
const localeStr = isChineseLocale(locale) ? 'zh-CN' : 'en-US'
const time = new Date(msg.timestamp * 1000).toLocaleString(localeStr)
let content = msg.content || (t('noContent', locale) as string)
// 截断超长消息内容
if (content.length > MAX_MESSAGE_CONTENT_LENGTH) {
@@ -142,7 +194,8 @@ const searchMessagesTool: ToolDefinition = {
type: 'function',
function: {
name: 'search_messages',
description: '根据关键词搜索群聊记录。适用于用户想要查找特定话题、关键词相关的聊天内容。可以指定时间范围和发送者来筛选消息。支持精确到分钟级别的时间查询。',
description:
'根据关键词搜索群聊记录。适用于用户想要查找特定话题、关键词相关的聊天内容。可以指定时间范围和发送者来筛选消息。支持精确到分钟级别的时间查询。',
parameters: {
type: 'object',
properties: {
@@ -177,11 +230,13 @@ const searchMessagesTool: ToolDefinition = {
},
start_time: {
type: 'string',
description: '开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
description:
'开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
},
end_time: {
type: 'string',
description: '结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
description:
'结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
},
},
required: ['keywords'],
@@ -203,7 +258,7 @@ async function searchMessagesExecutor(
},
context: ToolContext
): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit, locale } = context
// 用户配置优先:如果用户设置了 maxMessagesLimit,使用它;否则使用 LLM 指定的值或默认值 100,上限 5000
const limit = Math.min(maxMessagesLimit || params.limit || 100, 5000)
@@ -223,8 +278,8 @@ async function searchMessagesExecutor(
return {
total: result.total,
returned: result.messages.length,
timeRange: formatTimeRange(effectiveTimeFilter),
messages: result.messages.map((m) => formatMessageCompact(m)),
timeRange: formatTimeRange(effectiveTimeFilter, locale),
messages: result.messages.map((m) => formatMessageCompact(m, locale)),
}
}
@@ -236,7 +291,8 @@ const getRecentMessagesTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_recent_messages',
description: '获取指定时间段内的群聊消息。适用于回答"最近大家聊了什么"、"X月群里聊了什么"等概览性问题。支持精确到分钟级别的时间查询。',
description:
'获取指定时间段内的群聊消息。适用于回答"最近大家聊了什么"、"X月群里聊了什么"等概览性问题。支持精确到分钟级别的时间查询。',
parameters: {
type: 'object',
properties: {
@@ -262,11 +318,13 @@ const getRecentMessagesTool: ToolDefinition = {
},
start_time: {
type: 'string',
description: '开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
description:
'开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
},
end_time: {
type: 'string',
description: '结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
description:
'结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
},
},
},
@@ -285,7 +343,7 @@ async function getRecentMessagesExecutor(
},
context: ToolContext
): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit, locale } = context
// 用户配置优先:如果用户设置了 maxMessagesLimit,使用它;否则使用 LLM 指定的值或默认值 100(节省 token)
const limit = maxMessagesLimit || params.limit || 100
@@ -297,8 +355,8 @@ async function getRecentMessagesExecutor(
return {
total: result.total,
returned: result.messages.length,
timeRange: formatTimeRange(effectiveTimeFilter),
messages: result.messages.map((m) => formatMessageCompact(m)),
timeRange: formatTimeRange(effectiveTimeFilter, locale),
messages: result.messages.map((m) => formatMessageCompact(m, locale)),
}
}
@@ -322,11 +380,8 @@ const getMemberStatsTool: ToolDefinition = {
},
}
async function getMemberStatsExecutor(
params: { top_n?: number },
context: ToolContext
): Promise<unknown> {
const { sessionId, timeFilter } = context
async function getMemberStatsExecutor(params: { top_n?: number }, context: ToolContext): Promise<unknown> {
const { sessionId, timeFilter, locale } = context
const topN = params.top_n || 10
const result = await workerManager.getMemberActivity(sessionId, timeFilter)
@@ -335,11 +390,10 @@ async function getMemberStatsExecutor(
const topMembers = result.slice(0, topN)
// 格式化为简洁文本:排名. 名字 消息数(百分比)
const msgSuffix = isChineseLocale(locale) ? '条' : ''
return {
totalMembers: result.length,
topMembers: topMembers.map((m, index) =>
`${index + 1}. ${m.name} ${m.messageCount}条(${m.percentage}%)`
),
topMembers: topMembers.map((m, index) => `${index + 1}. ${m.name} ${m.messageCount}${msgSuffix}(${m.percentage}%)`),
}
}
@@ -369,29 +423,26 @@ async function getTimeStatsExecutor(
params: { type: 'hourly' | 'weekday' | 'daily' },
context: ToolContext
): Promise<unknown> {
const { sessionId, timeFilter } = context
const { sessionId, timeFilter, locale } = context
const msgSuffix = isChineseLocale(locale) ? '条' : ''
switch (params.type) {
case 'hourly': {
const result = await workerManager.getHourlyActivity(sessionId, timeFilter)
const peak = result.reduce((max, curr) =>
curr.messageCount > max.messageCount ? curr : max
)
const peak = result.reduce((max, curr) => (curr.messageCount > max.messageCount ? curr : max))
// 格式化为简洁文本:时间 消息数
return {
peakHour: `${peak.hour}:00 (${peak.messageCount})`,
distribution: result.map((h) => `${h.hour}:00 ${h.messageCount}`),
peakHour: `${peak.hour}:00 (${peak.messageCount}${msgSuffix})`,
distribution: result.map((h) => `${h.hour}:00 ${h.messageCount}${msgSuffix}`),
}
}
case 'weekday': {
const weekdayNames = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']
const weekdayNames = t('weekdays', locale) as string[]
const result = await workerManager.getWeekdayActivity(sessionId, timeFilter)
const peak = result.reduce((max, curr) =>
curr.messageCount > max.messageCount ? curr : max
)
const peak = result.reduce((max, curr) => (curr.messageCount > max.messageCount ? curr : max))
return {
peakDay: `${weekdayNames[peak.weekday]} (${peak.messageCount})`,
distribution: result.map((w) => `${weekdayNames[w.weekday]} ${w.messageCount}`),
peakDay: `${weekdayNames[peak.weekday]} (${peak.messageCount}${msgSuffix})`,
distribution: result.map((w) => `${weekdayNames[w.weekday]} ${w.messageCount}${msgSuffix}`),
}
}
case 'daily': {
@@ -400,9 +451,10 @@ async function getTimeStatsExecutor(
const recent = result.slice(-30)
const total = recent.reduce((sum, d) => sum + d.messageCount, 0)
const avg = Math.round(total / recent.length)
const summaryFn = i18nTexts.dailySummary[isChineseLocale(locale) ? 'zh' : 'en']
return {
summary: `最近${recent.length}天共${total}条,日均${avg}`,
trend: recent.map((d) => `${d.date} ${d.messageCount}`),
summary: summaryFn(recent.length, total, avg),
trend: recent.map((d) => `${d.date} ${d.messageCount}${msgSuffix}`),
}
}
}
@@ -416,7 +468,8 @@ const getGroupMembersTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_group_members',
description: '获取群成员列表,包括成员的基本信息、别名和消息统计。适用于查询"群里有哪些人"、"某人的别名是什么"、"谁的QQ号是xxx"等问题。',
description:
'获取群成员列表,包括成员的基本信息、别名和消息统计。适用于查询"群里有哪些人"、"某人的别名是什么"、"谁的QQ号是xxx"等问题。',
parameters: {
type: 'object',
properties: {
@@ -437,7 +490,7 @@ async function getGroupMembersExecutor(
params: { search?: string; limit?: number },
context: ToolContext
): Promise<unknown> {
const { sessionId } = context
const { sessionId, locale } = context
const members = await workerManager.getMembers(sessionId)
@@ -464,13 +517,15 @@ async function getGroupMembersExecutor(
}
// 格式化为简洁文本:id|QQ号|显示名(群昵称)|消息数
const msgSuffix = isChineseLocale(locale) ? '条' : ''
const aliasLabel = t('alias', locale) as string
return {
totalMembers: members.length,
returnedMembers: filteredMembers.length,
members: filteredMembers.map((m) => {
const displayName = m.groupNickname || m.accountName || m.platformId
const aliasStr = m.aliases.length > 0 ? `|别名:${m.aliases.join(',')}` : ''
return `${m.id}|${m.platformId}|${displayName}|${m.messageCount}${aliasStr}`
const aliasStr = m.aliases.length > 0 ? `|${aliasLabel}:${m.aliases.join(',')}` : ''
return `${m.id}|${m.platformId}|${displayName}|${m.messageCount}${msgSuffix}${aliasStr}`
}),
}
}
@@ -483,7 +538,8 @@ const getMemberNameHistoryTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_member_name_history',
description: '获取成员的昵称变更历史记录。适用于回答"某人以前叫什么名字"、"某人的昵称变化"、"某人曾用名"等问题。需要先通过 get_group_members 工具获取成员 ID。',
description:
'获取成员的昵称变更历史记录。适用于回答"某人以前叫什么名字"、"某人的昵称变化"、"某人曾用名"等问题。需要先通过 get_group_members 工具获取成员 ID。',
parameters: {
type: 'object',
properties: {
@@ -497,11 +553,8 @@ const getMemberNameHistoryTool: ToolDefinition = {
},
}
async function getMemberNameHistoryExecutor(
params: { member_id: number },
context: ToolContext
): Promise<unknown> {
const { sessionId } = context
async function getMemberNameHistoryExecutor(params: { member_id: number }, context: ToolContext): Promise<unknown> {
const { sessionId, locale } = context
// 先获取成员基本信息
const members = await workerManager.getMembers(sessionId)
@@ -509,7 +562,7 @@ async function getMemberNameHistoryExecutor(
if (!member) {
return {
error: '未找到该成员',
error: t('memberNotFound', locale) as string,
member_id: params.member_id,
}
}
@@ -518,27 +571,27 @@ async function getMemberNameHistoryExecutor(
const history = await workerManager.getMemberNameHistory(sessionId, params.member_id)
// 格式化历史记录为简洁文本
const localeStr = isChineseLocale(locale) ? 'zh-CN' : 'en-US'
const untilNow = t('untilNow', locale) as string
const formatHistory = (h: { name: string; startTs: number; endTs: number | null }) => {
const start = new Date(h.startTs * 1000).toLocaleDateString('zh-CN')
const end = h.endTs ? new Date(h.endTs * 1000).toLocaleDateString('zh-CN') : '至今'
const start = new Date(h.startTs * 1000).toLocaleDateString(localeStr)
const end = h.endTs ? new Date(h.endTs * 1000).toLocaleDateString(localeStr) : untilNow
return `${h.name} (${start} ~ ${end})`
}
const accountNames = history
.filter((h: { nameType: string }) => h.nameType === 'account_name')
.map(formatHistory)
const accountNames = history.filter((h: { nameType: string }) => h.nameType === 'account_name').map(formatHistory)
const groupNicknames = history
.filter((h: { nameType: string }) => h.nameType === 'group_nickname')
.map(formatHistory)
const groupNicknames = history.filter((h: { nameType: string }) => h.nameType === 'group_nickname').map(formatHistory)
const displayName = member.groupNickname || member.accountName || member.platformId
const aliasStr = member.aliases.length > 0 ? `|别名:${member.aliases.join(',')}` : ''
const aliasLabel = t('alias', locale) as string
const aliasStr = member.aliases.length > 0 ? `|${aliasLabel}:${member.aliases.join(',')}` : ''
const noChangeRecord = t('noChangeRecord', locale) as string
return {
member: `${member.id}|${member.platformId}|${displayName}${aliasStr}`,
accountNameHistory: accountNames.length > 0 ? accountNames : '无变更记录',
groupNicknameHistory: groupNicknames.length > 0 ? groupNicknames : '无变更记录',
accountNameHistory: accountNames.length > 0 ? accountNames : noChangeRecord,
groupNicknameHistory: groupNicknames.length > 0 ? groupNicknames : noChangeRecord,
}
}
@@ -549,7 +602,8 @@ const getConversationBetweenTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_conversation_between',
description: '获取两个群成员之间的对话记录。适用于回答"A和B之间聊了什么"、"查看两人的对话"等问题。需要先通过 get_group_members 获取成员 ID。支持精确到分钟级别的时间查询。',
description:
'获取两个群成员之间的对话记录。适用于回答"A和B之间聊了什么"、"查看两人的对话"等问题。需要先通过 get_group_members 获取成员 ID。支持精确到分钟级别的时间查询。',
parameters: {
type: 'object',
properties: {
@@ -583,11 +637,13 @@ const getConversationBetweenTool: ToolDefinition = {
},
start_time: {
type: 'string',
description: '开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
description:
'开始时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 14:00"。指定后会覆盖 year/month/day/hour 参数',
},
end_time: {
type: 'string',
description: '结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
description:
'结束时间,格式 "YYYY-MM-DD HH:mm",如 "2024-03-15 18:30"。指定后会覆盖 year/month/day/hour 参数',
},
},
required: ['member_id_1', 'member_id_2'],
@@ -609,7 +665,7 @@ async function getConversationBetweenExecutor(
},
context: ToolContext
): Promise<unknown> {
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit } = context
const { sessionId, timeFilter: contextTimeFilter, maxMessagesLimit, locale } = context
// 用户配置优先:如果用户设置了 maxMessagesLimit,使用它;否则使用 LLM 指定的值或默认值 100(节省 token)
const limit = maxMessagesLimit || params.limit || 100
@@ -626,7 +682,7 @@ async function getConversationBetweenExecutor(
if (result.messages.length === 0) {
return {
error: '未找到这两人之间的对话',
error: t('noConversation', locale) as string,
member1Id: params.member_id_1,
member2Id: params.member_id_2,
}
@@ -637,8 +693,8 @@ async function getConversationBetweenExecutor(
returned: result.messages.length,
member1: result.member1Name,
member2: result.member2Name,
timeRange: formatTimeRange(effectiveTimeFilter),
conversation: result.messages.map((m) => formatMessageCompact(m)),
timeRange: formatTimeRange(effectiveTimeFilter, locale),
conversation: result.messages.map((m) => formatMessageCompact(m, locale)),
}
}
@@ -650,13 +706,15 @@ const getMessageContextTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_message_context',
description: '根据消息 ID 获取前后的上下文消息。适用于需要查看某条消息前后聊天内容的场景,比如"这条消息的前后在聊什么"、"查看某条消息的上下文"等。支持单个或批量消息 ID。',
description:
'根据消息 ID 获取前后的上下文消息。适用于需要查看某条消息前后聊天内容的场景,比如"这条消息的前后在聊什么"、"查看某条消息的上下文"等。支持单个或批量消息 ID。',
parameters: {
type: 'object',
properties: {
message_ids: {
type: 'array',
description: '要查询上下文的消息 ID 列表,可以是单个 ID 或多个 ID。消息 ID 可以从 search_messages 等工具的返回结果中获取',
description:
'要查询上下文的消息 ID 列表,可以是单个 ID 或多个 ID。消息 ID 可以从 search_messages 等工具的返回结果中获取',
items: { type: 'number' },
},
context_size: {
@@ -673,18 +731,14 @@ async function getMessageContextExecutor(
params: { message_ids: number[]; context_size?: number },
context: ToolContext
): Promise<unknown> {
const { sessionId } = context
const { sessionId, locale } = context
const contextSize = params.context_size || 20
const messages = await workerManager.getMessageContext(
sessionId,
params.message_ids,
contextSize
)
const messages = await workerManager.getMessageContext(sessionId, params.message_ids, contextSize)
if (messages.length === 0) {
return {
error: '未找到指定的消息或上下文',
error: t('noMessageContext', locale) as string,
messageIds: params.message_ids,
}
}
@@ -693,7 +747,7 @@ async function getMessageContextExecutor(
totalMessages: messages.length,
contextSize: contextSize,
requestedMessageIds: params.message_ids,
messages: messages.map((m) => formatMessageCompact(m)),
messages: messages.map((m) => formatMessageCompact(m, locale)),
}
}
@@ -707,4 +761,3 @@ registerTool(getGroupMembersTool, getGroupMembersExecutor)
registerTool(getMemberNameHistoryTool, getMemberNameHistoryExecutor)
registerTool(getConversationBetweenTool, getConversationBetweenExecutor)
registerTool(getMessageContextTool, getMessageContextExecutor)
+2
View File
@@ -29,6 +29,8 @@ export interface ToolContext {
maxMessagesLimit?: number
/** Owner 信息(当前用户在对话中的身份) */
ownerInfo?: OwnerInfo
/** 语言环境(用于工具返回结果的国际化) */
locale?: string
}
/**
+3 -3
View File
@@ -81,7 +81,7 @@ export function registerChatHandlers(ctx: IpcContext): void {
const format = formatFeature?.name || null
console.log('[IpcMain] Detected format:', format)
if (!format) {
return { error: '无法识别的文件格式' }
return { error: 'error.unrecognized_format' }
}
return { filePath, format }
@@ -98,11 +98,11 @@ export function registerChatHandlers(ctx: IpcContext): void {
console.log('[IpcMain] chat:import called with:', filePath)
try {
// 发送进度:开始检测格式
// Send progress: detecting format (message not used by frontend, stage-based translation)
win.webContents.send('chat:importProgress', {
stage: 'detecting',
progress: 5,
message: '正在检测文件格式...',
message: '', // Frontend translates based on stage
})
// 使用流式导入(在 Worker 线程中执行)
@@ -130,7 +130,7 @@ async function* parseChatLabJsonl(options: ParseOptions): AsyncGenerator<ParseEv
let messagesProcessed = 0
// 发送初始进度
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '开始解析 JSONL...')
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '')
yield { type: 'progress', data: initialProgress }
onProgress?.(initialProgress)
@@ -257,7 +257,7 @@ async function* parseChatLabJsonl(options: ParseOptions): AsyncGenerator<ParseEv
}
// 完成
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '解析完成')
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '')
yield { type: 'progress', data: doneProgress }
onProgress?.(doneProgress)
+2 -2
View File
@@ -83,7 +83,7 @@ async function* parseChatLab(options: ParseOptions): AsyncGenerator<ParseEvent,
let messagesProcessed = 0
// 发送初始进度
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '开始解析...')
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '')
yield { type: 'progress', data: initialProgress }
onProgress?.(initialProgress)
@@ -245,7 +245,7 @@ async function* parseChatLab(options: ParseOptions): AsyncGenerator<ParseEvent,
}
// 完成
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '解析完成')
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '')
yield { type: 'progress', data: doneProgress }
onProgress?.(doneProgress)
+1 -1
View File
@@ -20,7 +20,7 @@ export const formats: FormatModule[] = [
chatlab, // 优先级 1 - ChatLab JSON
chatlabJsonl, // 优先级 2 - ChatLab JSONL(流式格式,支持超大文件)
shuakamiQqExporter, // 优先级 10 - shuakami/qq-chat-exporter
yccccccyEchotrace, // 优先级 15 - ycccccccy/echotrace 微信导出
yccccccyEchotrace, // 优先级 15 - ycccccccy/echotrace
whatsappNativeTxt, // 优先级 25 - WhatsApp 官方导出 TXT
qqNativeTxt, // 优先级 30 - QQ 官方导出 TXT
]
@@ -151,7 +151,7 @@ async function* parseTxt(options: ParseOptions): AsyncGenerator<ParseEvent, void
let skippedLines = 0 // 跳过的无效行计数
// 发送初始进度
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '开始解析...')
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '')
yield { type: 'progress', data: initialProgress }
onProgress?.(initialProgress)
@@ -311,7 +311,7 @@ async function* parseTxt(options: ParseOptions): AsyncGenerator<ParseEvent, void
}
// 完成
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '解析完成')
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '')
yield { type: 'progress', data: doneProgress }
onProgress?.(doneProgress)
@@ -235,7 +235,7 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
let skippedMessages = 0 // 跳过的无效消息计数
// 发送初始进度
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '开始解析...')
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '')
yield { type: 'progress', data: initialProgress }
onProgress?.(initialProgress)
@@ -395,7 +395,7 @@ async function* parseV4(options: ParseOptions): AsyncGenerator<ParseEvent, void,
}
// 完成
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '解析完成')
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '')
yield { type: 'progress', data: doneProgress }
onProgress?.(doneProgress)
@@ -213,7 +213,7 @@ async function preprocessQQJson(inputPath: string, onProgress?: (progress: Parse
const outputFilename = `slim_${Date.now()}_${path.basename(inputPath)}`
const outputPath = path.join(tempDir, outputFilename)
onProgress?.(createProgress('parsing', 0, totalBytes, 0, '预处理:读取文件头...'))
onProgress?.(createProgress('parsing', 0, totalBytes, 0, ''))
// 先从原文件读取 avatars(因为它在文件末尾,消息处理时可能无法访问)
const avatarsStr = readAvatarsFromFile(inputPath)
@@ -268,7 +268,7 @@ async function preprocessQQJson(inputPath: string, onProgress?: (progress: Parse
// 解析失败时忽略
}
onProgress?.(createProgress('parsing', 0, totalBytes, 0, '预处理:开始精简消息...'))
onProgress?.(createProgress('parsing', 0, totalBytes, 0, ''))
const readStream = fs.createReadStream(inputPath, { encoding: 'utf-8' })
const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf-8' })
@@ -326,7 +326,7 @@ async function preprocessQQJson(inputPath: string, onProgress?: (progress: Parse
writeStream.end()
writeStream.on('finish', () => {
onProgress?.(createProgress('done', totalBytes, totalBytes, messagesProcessed, '预处理完成'))
onProgress?.(createProgress('done', totalBytes, totalBytes, messagesProcessed, ''))
resolve(outputPath)
})
})
@@ -336,7 +336,7 @@ async function preprocessQQJson(inputPath: string, onProgress?: (progress: Parse
if (fs.existsSync(outputPath)) {
fs.unlinkSync(outputPath)
}
onProgress?.(createProgress('error', bytesRead, totalBytes, messagesProcessed, `预处理错误: ${err.message}`))
onProgress?.(createProgress('error', bytesRead, totalBytes, messagesProcessed, err.message))
reject(err)
})
})
@@ -181,7 +181,7 @@ async function* parseWhatsApp(options: ParseOptions): AsyncGenerator<ParseEvent,
let skippedLines = 0
// 发送初始进度
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '开始解析...')
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '')
yield { type: 'progress', data: initialProgress }
onProgress?.(initialProgress)
@@ -335,7 +335,7 @@ async function* parseWhatsApp(options: ParseOptions): AsyncGenerator<ParseEvent,
}
// 完成
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '解析完成')
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '')
yield { type: 'progress', data: doneProgress }
onProgress?.(doneProgress)
@@ -1,13 +1,13 @@
/**
* ycccccccy/echotrace 微信导出格式解析器
* ycccccccy/echotrace 导出格式解析器
* 适配项目: https://github.com/ycccccccy/echotrace
*
* 特征:
* - 顶层包含 session 和 messages 字段
* - session.wxid: 微信ID(群聊以 @chatroom 结尾)
* - session.wxid: ID(群聊以 @chatroom 结尾)
* - session.type: "群聊" 或 "私聊"
* - messages[].type: 中文消息类型字符串
* - messages[].senderUsername: 发送者微信ID
* - messages[].senderUsername: 发送者ID
* - messages[].senderDisplayName: 发送者显示名
*
* 注意:localType 字段不可信,不使用
@@ -47,7 +47,7 @@ function extractNameFromFilePath(filePath: string): string {
export const feature: FormatFeature = {
id: 'ycccccccy-echotrace',
name: 'ycccccccy/echotrace 微信导出',
name: 'ycccccccy/echotrace 导出',
platform: KNOWN_PLATFORMS.WECHAT,
priority: 15,
extensions: ['.json'],
@@ -78,7 +78,7 @@ interface EchotraceMessage {
localType: number // 不可信,不使用
content: string
isSend: number | null // 0=接收, 1=发送, null=系统
senderUsername: string // 发送者微信ID
senderUsername: string // 发送者ID
senderDisplayName: string // 发送者显示名
senderAvatarKey: string // 头像查找 key(通常与 senderUsername 相同)
source: string
@@ -154,12 +154,12 @@ async function* parseEchotrace(options: ParseOptions): AsyncGenerator<ParseEvent
let messagesProcessed = 0
// 发送初始进度
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '开始解析...')
const initialProgress = createProgress('parsing', 0, totalBytes, 0, '')
yield { type: 'progress', data: initialProgress }
onProgress?.(initialProgress)
// 记录解析开始
onLog?.('info', `开始解析 Echotrace 微信导出文件,大小: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`)
onLog?.('info', `开始解析 Echotrace 导出文件,大小: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`)
// 读取文件头获取 session 信息
const headContent = readFileHeadBytes(filePath, 2000)
@@ -507,7 +507,7 @@ async function* parseEchotrace(options: ParseOptions): AsyncGenerator<ParseEvent
}
// 完成
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '解析完成')
const doneProgress = createProgress('done', totalBytes, totalBytes, messagesProcessed, '')
yield { type: 'progress', data: doneProgress }
onProgress?.(doneProgress)
+10 -10
View File
@@ -207,7 +207,7 @@ export async function streamImport(filePath: string, requestId: string): Promise
// 检测格式
const formatFeature = detectFormat(filePath)
if (!formatFeature) {
return { success: false, error: '无法识别文件格式' }
return { success: false, error: 'error.unrecognized_format' }
}
// 初始化性能日志(实时写入文件)
@@ -234,14 +234,14 @@ export async function streamImport(filePath: string, requestId: string): Promise
totalBytes: 0,
messagesProcessed: 0,
percentage: 0,
message: '预处理:精简大文件中...',
message: '', // Frontend translates based on stage
})
try {
tempFilePath = await preprocessor.preprocess(filePath, (progress) => {
sendProgress(requestId, {
...progress,
message: progress.message || '预处理中...',
message: '', // Frontend translates based on stage
})
})
actualFilePath = tempFilePath
@@ -340,14 +340,14 @@ export async function streamImport(filePath: string, requestId: string): Promise
lastCheckpointCount = totalMessageCount
}
// 发送写入进度
// Send write progress (frontend shows messagesProcessed count)
sendProgress(requestId, {
stage: 'importing',
bytesRead: 0,
totalBytes: 0,
messagesProcessed: totalMessageCount,
percentage: 100,
message: `正在写入数据库... 已处理 ${totalMessageCount.toLocaleString()}`,
message: '', // Frontend translates based on stage and shows messagesProcessed
})
}
beginTransaction()
@@ -545,7 +545,7 @@ export async function streamImport(filePath: string, requestId: string): Promise
totalBytes: 0,
messagesProcessed: totalMessageCount,
percentage: 100,
message: '正在写入昵称历史...',
message: '', // Frontend translates based on stage
})
logPerf('开始写入昵称历史', totalMessageCount)
@@ -635,7 +635,7 @@ export async function streamImport(filePath: string, requestId: string): Promise
totalBytes: 0,
messagesProcessed: totalMessageCount,
percentage: 100,
message: '正在创建索引...',
message: '', // Frontend translates based on stage
})
logPerf('开始创建索引', totalMessageCount)
createIndexes(db)
@@ -648,7 +648,7 @@ export async function streamImport(filePath: string, requestId: string): Promise
totalBytes: 0,
messagesProcessed: totalMessageCount,
percentage: 100,
message: '正在优化数据库...',
message: '', // Frontend translates based on stage
})
doCheckpoint()
logPerf('WAL checkpoint 完成', totalMessageCount)
@@ -669,7 +669,7 @@ export async function streamImport(filePath: string, requestId: string): Promise
return {
success: false,
error: '未解析到任何消息,请检查文件格式是否正确',
error: 'error.no_messages',
}
}
@@ -740,7 +740,7 @@ export async function streamParseFileInfo(filePath: string, requestId: string):
totalBytes: fileSize,
messagesProcessed: 0,
percentage: 0,
message: '正在读取文件...',
message: '', // Frontend translates based on stage
})
// 创建临时数据库
+2 -1
View File
@@ -372,7 +372,8 @@ interface AgentApi {
onChunk?: (chunk: AgentStreamChunk) => void,
historyMessages?: Array<{ role: 'user' | 'assistant'; content: string }>,
chatType?: 'group' | 'private',
promptConfig?: PromptConfig
promptConfig?: PromptConfig,
locale?: string
) => { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> }
abort: (requestId: string) => Promise<{ success: boolean; error?: string }>
}
+16 -6
View File
@@ -865,6 +865,7 @@ const agentApi = {
* @param historyMessages 对话历史(可选,用于上下文关联)
* @param chatType 聊天类型('group' | 'private'
* @param promptConfig 用户自定义提示词配置(可选)
* @param locale 语言设置(可选,默认 'zh-CN'
* @returns 返回 { requestId, promise }requestId 可用于中止请求
*/
runStream: (
@@ -873,7 +874,8 @@ const agentApi = {
onChunk?: (chunk: AgentStreamChunk) => void,
historyMessages?: Array<{ role: 'user' | 'assistant'; content: string }>,
chatType?: 'group' | 'private',
promptConfig?: PromptConfig
promptConfig?: PromptConfig,
locale?: string
): { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> } => {
const requestId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
console.log(
@@ -901,21 +903,29 @@ const agentApi = {
}
// 监听完成事件
const completeHandler = (_event: Electron.IpcRendererEvent, data: { requestId: string; result: AgentResult }) => {
const completeHandler = (
_event: Electron.IpcRendererEvent,
data: { requestId: string; result: AgentResult & { error?: string } }
) => {
if (data.requestId === requestId) {
console.log('[preload] Agent 完成,requestId:', requestId)
console.log('[preload] Agent 完成,requestId:', requestId, 'hasError:', !!data.result?.error)
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
ipcRenderer.removeListener('agent:complete', completeHandler)
resolve({ success: true, result: data.result })
// 如果 result 中包含 error,返回失败状态
if (data.result?.error) {
resolve({ success: false, error: data.result.error })
} else {
resolve({ success: true, result: data.result })
}
}
}
ipcRenderer.on('agent:streamChunk', chunkHandler)
ipcRenderer.on('agent:complete', completeHandler)
// 发起请求(传递历史消息、聊天类型提示词配置)
// 发起请求(传递历史消息、聊天类型提示词配置和语言设置
ipcRenderer
.invoke('agent:runStream', requestId, userMessage, context, historyMessages, chatType, promptConfig)
.invoke('agent:runStream', requestId, userMessage, context, historyMessages, chatType, promptConfig, locale)
.then((result) => {
console.log('[preload] Agent invoke 返回:', result)
if (!result.success) {