mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-18 12:28:56 +08:00
feat: 完成最终国际化
This commit is contained in:
+142
-55
@@ -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++) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface ToolContext {
|
||||
maxMessagesLimit?: number
|
||||
/** Owner 信息(当前用户在对话中的身份) */
|
||||
ownerInfo?: OwnerInfo
|
||||
/** 语言环境(用于工具返回结果的国际化) */
|
||||
locale?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
// 创建临时数据库
|
||||
|
||||
Vendored
+2
-1
@@ -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 }>
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user