mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-03 03:31:18 +08:00
feat: 主进程配置国际化
This commit is contained in:
@@ -9,6 +9,7 @@ import { getAllToolDefinitions, executeToolCalls } from './tools'
|
||||
import type { ToolContext, OwnerInfo } from './tools/types'
|
||||
import { aiLogger } from './logger'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { t as i18nT } from '../i18n'
|
||||
|
||||
// 思考类标签列表(可按需扩展)
|
||||
const THINK_TAGS = ['think', 'analysis', 'reasoning', 'reflection', 'thought', 'thinking']
|
||||
@@ -311,78 +312,11 @@ export interface PromptConfig {
|
||||
responseRules: string
|
||||
}
|
||||
|
||||
// 国际化内容
|
||||
const i18nContent = {
|
||||
'zh-CN': {
|
||||
currentDateIs: '当前日期是',
|
||||
chatTypeDesc: { private: '私聊记录', group: '群聊记录' },
|
||||
chatContext: { private: '对话', group: '群聊' },
|
||||
ownerNote: (displayName: string, platformId: string, chatContext: string) => `当前用户身份:
|
||||
- 用户在${chatContext}中的身份是「${displayName}」(platformId: ${platformId})
|
||||
- 当用户提到"我"、"我的"时,指的就是「${displayName}」
|
||||
- 查询"我"的发言时,使用 sender_id 参数筛选该成员
|
||||
`,
|
||||
memberNotePrivate: `成员查询策略:
|
||||
- 私聊只有两个人,可以直接获取成员列表
|
||||
- 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息
|
||||
`,
|
||||
memberNoteGroup: `成员查询策略:
|
||||
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表
|
||||
- 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
|
||||
- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称
|
||||
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言
|
||||
`,
|
||||
timeParamsIntro: '时间参数:按用户提到的精度组合 year/month/day/hour',
|
||||
timeParamExample1: (year: number) => `"10月" → year: ${year}, month: 10`,
|
||||
timeParamExample2: (year: number) => `"10月1号" → year: ${year}, month: 10, day: 1`,
|
||||
timeParamExample3: (year: number) => `"10月1号下午3点" → year: ${year}, month: 10, day: 1, hour: 15`,
|
||||
defaultYearNote: (year: number, prevYear: number) => `未指定年份默认${year}年,若该月份未到则用${prevYear}年`,
|
||||
responseInstruction: '根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。',
|
||||
responseRulesTitle: '回答要求:',
|
||||
// Fallback 角色定义:适中幽默,允许 B 站/网络热梗与表情
|
||||
fallbackRoleDefinition: (chatType: string) => `你是一个专业但风格轻松的${chatType}记录分析助手。
|
||||
你的任务是帮助用户理解和分析他们的${chatType}记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。`,
|
||||
// Fallback 回答要求:强调严谨优先,适度玩梗
|
||||
fallbackResponseRules: `1. 基于工具返回的数据回答,不要编造信息
|
||||
2. 如果数据不足以回答问题,请说明
|
||||
3. 回答要简洁明了,使用 Markdown 格式
|
||||
4. 可以适度加入 B 站/网络热梗、表情/颜文字(强度适中)
|
||||
5. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达`,
|
||||
},
|
||||
'en-US': {
|
||||
currentDateIs: 'Current date is',
|
||||
chatTypeDesc: { private: 'private chat records', group: 'group chat records' },
|
||||
chatContext: { private: 'conversation', group: 'group chat' },
|
||||
ownerNote: (displayName: string, platformId: string, chatContext: string) => `Current user identity:
|
||||
- The user's identity in this ${chatContext} is "${displayName}" (platformId: ${platformId})
|
||||
- When the user refers to "I" or "my", it refers to "${displayName}"
|
||||
- When querying "my" messages, use the sender_id parameter to filter for this member
|
||||
`,
|
||||
memberNotePrivate: `Member query strategy:
|
||||
- Private chats only have two participants, so the member list can be directly obtained
|
||||
- When the user refers to "the other party" or "he/she", get the other participant's information via get_group_members
|
||||
`,
|
||||
memberNoteGroup: `Member query strategy:
|
||||
- When the user refers to specific group members (e.g., "what did John say", "Mary's messages"), first call get_group_members to get the member list
|
||||
- Group members have three names: accountName (original nickname), groupNickname (group nickname), aliases (user-defined aliases)
|
||||
- The search parameter of get_group_members can be used for fuzzy searching these three names
|
||||
- Once a member is found, use their id field as the sender_id parameter for search_messages to retrieve their messages
|
||||
`,
|
||||
timeParamsIntro: 'Time parameters: combine year/month/day/hour based on user mention',
|
||||
timeParamExample1: (year: number) => `"October" → year: ${year}, month: 10`,
|
||||
timeParamExample2: (year: number) => `"October 1st" → year: ${year}, month: 10, day: 1`,
|
||||
timeParamExample3: (year: number) => `"October 1st 3 PM" → year: ${year}, month: 10, day: 1, hour: 15`,
|
||||
defaultYearNote: (year: number, prevYear: number) =>
|
||||
`If year is not specified, defaults to ${year}. If the month has not yet occurred, ${prevYear} is used.`,
|
||||
responseInstruction:
|
||||
"Based on the user's question, select appropriate tools to retrieve data, then provide an answer based on the data.",
|
||||
responseRulesTitle: 'Response requirements:',
|
||||
fallbackRoleDefinition: (chatType: string) => `You are a professional ${chatType} analysis assistant.
|
||||
Your task is to help users understand and analyze their ${chatType} data.`,
|
||||
fallbackResponseRules: `1. Answer based on data returned by tools, do not fabricate information
|
||||
2. If data is insufficient to answer, please state so
|
||||
3. Keep answers concise and clear, use Markdown format`,
|
||||
},
|
||||
// ==================== 国际化辅助(使用 i18next) ====================
|
||||
|
||||
/** 获取 Agent 翻译,根据传入的 locale 参数 */
|
||||
function agentT(key: string, locale: string, options?: Record<string, unknown>): string {
|
||||
return i18nT(key, { lng: locale, ...options })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,9 +334,8 @@ function getLockedPromptSection(
|
||||
ownerInfo?: OwnerInfo,
|
||||
locale: string = 'zh-CN'
|
||||
): string {
|
||||
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
|
||||
const now = new Date()
|
||||
const dateLocale = locale === 'zh-CN' ? 'zh-CN' : 'en-US'
|
||||
const dateLocale = locale.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||
const currentDate = now.toLocaleDateString(dateLocale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -411,27 +344,35 @@ function getLockedPromptSection(
|
||||
})
|
||||
|
||||
const isPrivate = chatType === 'private'
|
||||
const chatContext = content.chatContext[chatType]
|
||||
const chatContext = agentT(`ai.agent.chatContext.${chatType}`, locale)
|
||||
|
||||
// Owner 说明(当用户设置了"我是谁"时)
|
||||
const ownerNote = ownerInfo ? content.ownerNote(ownerInfo.displayName, ownerInfo.platformId, chatContext) : ''
|
||||
const ownerNote = ownerInfo
|
||||
? agentT('ai.agent.ownerNote', locale, {
|
||||
displayName: ownerInfo.displayName,
|
||||
platformId: ownerInfo.platformId,
|
||||
chatContext,
|
||||
})
|
||||
: ''
|
||||
|
||||
// 成员说明(私聊只有2人)
|
||||
const memberNote = isPrivate ? content.memberNotePrivate : content.memberNoteGroup
|
||||
const memberNote = isPrivate
|
||||
? agentT('ai.agent.memberNotePrivate', locale)
|
||||
: agentT('ai.agent.memberNoteGroup', locale)
|
||||
|
||||
const year = now.getFullYear()
|
||||
const prevYear = year - 1
|
||||
|
||||
return `${content.currentDateIs} ${currentDate}。
|
||||
return `${agentT('ai.agent.currentDateIs', locale)} ${currentDate}。
|
||||
${ownerNote}
|
||||
${memberNote}
|
||||
${content.timeParamsIntro}
|
||||
- ${content.timeParamExample1(year)}
|
||||
- ${content.timeParamExample2(year)}
|
||||
- ${content.timeParamExample3(year)}
|
||||
${content.defaultYearNote(year, prevYear)}
|
||||
${agentT('ai.agent.timeParamsIntro', locale)}
|
||||
- ${agentT('ai.agent.timeParamExample1', locale, { year })}
|
||||
- ${agentT('ai.agent.timeParamExample2', locale, { year })}
|
||||
- ${agentT('ai.agent.timeParamExample3', locale, { year })}
|
||||
${agentT('ai.agent.defaultYearNote', locale, { year, prevYear })}
|
||||
|
||||
${content.responseInstruction}`
|
||||
${agentT('ai.agent.responseInstruction', locale)}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,10 +380,7 @@ ${content.responseInstruction}`
|
||||
* 仅在前端未传递 promptConfig 时使用
|
||||
*/
|
||||
function getFallbackRoleDefinition(chatType: 'group' | 'private', locale: string = 'zh-CN'): string {
|
||||
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
|
||||
const chatTypeDesc =
|
||||
chatType === 'private' ? (locale === 'zh-CN' ? '私聊' : 'private chat') : locale === 'zh-CN' ? '群聊' : 'group chat'
|
||||
return content.fallbackRoleDefinition(chatTypeDesc)
|
||||
return agentT(`ai.agent.fallbackRoleDefinition.${chatType}`, locale)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,8 +388,7 @@ function getFallbackRoleDefinition(chatType: 'group' | 'private', locale: string
|
||||
* 仅在前端未传递 promptConfig 时使用
|
||||
*/
|
||||
function getFallbackResponseRules(locale: string = 'zh-CN'): string {
|
||||
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
|
||||
return content.fallbackResponseRules
|
||||
return agentT('ai.agent.fallbackResponseRules', locale)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -471,8 +408,6 @@ function buildSystemPrompt(
|
||||
ownerInfo?: OwnerInfo,
|
||||
locale: string = 'zh-CN'
|
||||
): string {
|
||||
const content = i18nContent[locale as keyof typeof i18nContent] || i18nContent['zh-CN']
|
||||
|
||||
// 使用用户配置或 fallback
|
||||
const roleDefinition = promptConfig?.roleDefinition || getFallbackRoleDefinition(chatType, locale)
|
||||
const responseRules = promptConfig?.responseRules || getFallbackResponseRules(locale)
|
||||
@@ -485,7 +420,7 @@ function buildSystemPrompt(
|
||||
|
||||
${lockedSection}
|
||||
|
||||
${content.responseRulesTitle}
|
||||
${agentT('ai.agent.responseRulesTitle', locale)}
|
||||
${responseRules}`
|
||||
}
|
||||
|
||||
@@ -634,7 +569,7 @@ export class Agent {
|
||||
aiLogger.warn('Agent', '达到最大工具调用轮数', { maxRounds: this.config.maxToolRounds })
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: '请根据已获取的信息给出回答,不要再调用工具。',
|
||||
content: agentT('ai.agent.answerWithoutTools', this.locale),
|
||||
})
|
||||
|
||||
const finalResponse = await chat(this.messages, this.config.llmOptions)
|
||||
@@ -877,7 +812,7 @@ export class Agent {
|
||||
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: '请根据已获取的信息给出回答,不要再调用工具。',
|
||||
content: agentT('ai.agent.answerWithoutTools', this.locale),
|
||||
})
|
||||
|
||||
// 最后一轮不带 tools(传入 abortSignal)
|
||||
@@ -993,7 +928,9 @@ export class Agent {
|
||||
// 添加工具结果消息
|
||||
this.messages.push({
|
||||
role: 'tool',
|
||||
content: result.success ? JSON.stringify(result.result) : `错误: ${result.error}`,
|
||||
content: result.success
|
||||
? JSON.stringify(result.result)
|
||||
: agentT('ai.agent.toolError', this.locale, { error: result.error }),
|
||||
tool_call_id: tc.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { GeminiService, GEMINI_INFO } from './gemini'
|
||||
import { OpenAICompatibleService, OPENAI_COMPATIBLE_INFO } from './openai-compatible'
|
||||
import { aiLogger, extractErrorInfo, extractErrorStack } from '../logger'
|
||||
import { encryptApiKey, decryptApiKey, isEncrypted } from './crypto'
|
||||
import { t } from '../../i18n'
|
||||
|
||||
// 导出类型
|
||||
export * from './types'
|
||||
@@ -299,7 +300,7 @@ export function addConfig(config: Omit<AIServiceConfig, 'id' | 'createdAt' | 'up
|
||||
const store = loadConfigStore()
|
||||
|
||||
if (store.configs.length >= MAX_CONFIG_COUNT) {
|
||||
return { success: false, error: `最多只能添加 ${MAX_CONFIG_COUNT} 个配置` }
|
||||
return { success: false, error: t('llm.maxConfigs', { count: MAX_CONFIG_COUNT }) }
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
@@ -332,7 +333,7 @@ export function updateConfig(
|
||||
const index = store.configs.findIndex((c) => c.id === id)
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, error: '配置不存在' }
|
||||
return { success: false, error: t('llm.configNotFound') }
|
||||
}
|
||||
|
||||
store.configs[index] = {
|
||||
@@ -353,7 +354,7 @@ export function deleteConfig(id: string): { success: boolean; error?: string } {
|
||||
const index = store.configs.findIndex((c) => c.id === id)
|
||||
|
||||
if (index === -1) {
|
||||
return { success: false, error: '配置不存在' }
|
||||
return { success: false, error: t('llm.configNotFound') }
|
||||
}
|
||||
|
||||
store.configs.splice(index, 1)
|
||||
@@ -375,7 +376,7 @@ export function setActiveConfig(id: string): { success: boolean; error?: string
|
||||
const config = store.configs.find((c) => c.id === id)
|
||||
|
||||
if (!config) {
|
||||
return { success: false, error: '配置不存在' }
|
||||
return { success: false, error: t('llm.configNotFound') }
|
||||
}
|
||||
|
||||
store.activeConfigId = id
|
||||
@@ -542,7 +543,7 @@ export async function chat(
|
||||
const service = getCurrentLLMService()
|
||||
if (!service) {
|
||||
aiLogger.error('LLM', '服务未配置')
|
||||
throw new Error('LLM 服务未配置,请先在设置中配置 API Key')
|
||||
throw new Error(t('llm.notConfigured'))
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -597,7 +598,7 @@ export async function* chatStream(messages: ChatMessage[], options?: ChatOptions
|
||||
const service = getCurrentLLMService()
|
||||
if (!service) {
|
||||
aiLogger.error('LLM', '服务未配置(流式)')
|
||||
throw new Error('LLM 服务未配置,请先在设置中配置 API Key')
|
||||
throw new Error(t('llm.notConfigured'))
|
||||
}
|
||||
|
||||
let chunkCount = 0
|
||||
|
||||
@@ -11,6 +11,7 @@ import Database from 'better-sqlite3'
|
||||
import { chat } from '../llm'
|
||||
import { getDbPath, openDatabase } from '../../database/core'
|
||||
import { aiLogger } from '../logger'
|
||||
import { t } from '../../i18n'
|
||||
|
||||
/** 最小消息数阈值(少于此数量不生成摘要) */
|
||||
const MIN_MESSAGE_COUNT = 3
|
||||
@@ -122,6 +123,7 @@ function getSummaryLengthLimit(messageCount: number): number {
|
||||
|
||||
/**
|
||||
* 判断消息是否有意义(用于过滤)
|
||||
* 支持中英文内容过滤
|
||||
*/
|
||||
function isValidMessage(content: string): boolean {
|
||||
const trimmed = content.trim()
|
||||
@@ -129,19 +131,45 @@ function isValidMessage(content: string): boolean {
|
||||
// 过滤空内容
|
||||
if (!trimmed) return false
|
||||
|
||||
// 过滤单字/双字无意义回复
|
||||
// 过滤单字/双字无意义回复(中文)
|
||||
if (trimmed.length <= 2) {
|
||||
// 允许一些有意义的短词
|
||||
const meaningfulShort = ['好的', '不是', '是的', '可以', '不行', '好吧', '明白', '知道', '同意']
|
||||
if (!meaningfulShort.includes(trimmed)) return false
|
||||
const meaningfulShortZh = ['好的', '不是', '是的', '可以', '不行', '好吧', '明白', '知道', '同意']
|
||||
if (!meaningfulShortZh.includes(trimmed)) return false
|
||||
}
|
||||
|
||||
// 过滤短无意义回复(英文,不区分大小写)
|
||||
const lowerTrimmed = trimmed.toLowerCase()
|
||||
const meaninglessShortEn = [
|
||||
'ok',
|
||||
'k',
|
||||
'yes',
|
||||
'no',
|
||||
'ya',
|
||||
'yep',
|
||||
'nope',
|
||||
'lol',
|
||||
'haha',
|
||||
'hehe',
|
||||
'hmm',
|
||||
'ah',
|
||||
'oh',
|
||||
'wow',
|
||||
'thx',
|
||||
'ty',
|
||||
'np',
|
||||
'gg',
|
||||
'brb',
|
||||
'idk',
|
||||
]
|
||||
if (meaninglessShortEn.includes(lowerTrimmed)) return false
|
||||
|
||||
// 过滤纯表情消息
|
||||
const emojiOnlyPattern = /^[\p{Emoji}\s[\]()()]+$/u
|
||||
if (emojiOnlyPattern.test(trimmed)) return false
|
||||
|
||||
// 过滤占位符文本
|
||||
// 过滤占位符文本(中文 + 英文)
|
||||
const placeholders = [
|
||||
// 中文占位符(QQ/微信导出格式)
|
||||
'[图片]',
|
||||
'[语音]',
|
||||
'[视频]',
|
||||
@@ -153,12 +181,38 @@ function isValidMessage(content: string): boolean {
|
||||
'[红包]',
|
||||
'[转账]',
|
||||
'[撤回消息]',
|
||||
// 英文占位符
|
||||
'[image]',
|
||||
'[voice]',
|
||||
'[video]',
|
||||
'[file]',
|
||||
'[sticker]',
|
||||
'[animated sticker]',
|
||||
'[location]',
|
||||
'[contact]',
|
||||
'[red packet]',
|
||||
'[transfer]',
|
||||
'[recalled message]',
|
||||
'[photo]',
|
||||
'[audio]',
|
||||
'[gif]',
|
||||
]
|
||||
if (placeholders.some((p) => trimmed === p)) return false
|
||||
if (placeholders.some((p) => lowerTrimmed === p.toLowerCase())) return false
|
||||
|
||||
// 过滤系统消息(入群、退群等)
|
||||
const systemPatterns = [/^.*邀请.*加入了群聊$/, /^.*退出了群聊$/, /^.*撤回了一条消息$/, /^你撤回了一条消息$/]
|
||||
if (systemPatterns.some((p) => p.test(trimmed))) return false
|
||||
// 过滤系统消息(中文:入群、退群等)
|
||||
const systemPatternsZh = [/^.*邀请.*加入了群聊$/, /^.*退出了群聊$/, /^.*撤回了一条消息$/, /^你撤回了一条消息$/]
|
||||
if (systemPatternsZh.some((p) => p.test(trimmed))) return false
|
||||
|
||||
// 过滤系统消息(英文)
|
||||
const systemPatternsEn = [
|
||||
/^.*invited.*to the group$/i,
|
||||
/^.*left the group$/i,
|
||||
/^.*recalled a message$/i,
|
||||
/^you recalled a message$/i,
|
||||
/^.*joined the group$/i,
|
||||
/^.*has been removed$/i,
|
||||
]
|
||||
if (systemPatternsEn.some((p) => p.test(trimmed))) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -282,17 +336,14 @@ export async function generateSessionSummary(
|
||||
// 2. 获取会话消息
|
||||
const sessionData = getSessionMessagesForSummary(dbSessionId, chatSessionId)
|
||||
if (!sessionData) {
|
||||
return { success: false, error: '会话不存在或数据库打开失败' }
|
||||
return { success: false, error: t('summary.sessionNotFound') }
|
||||
}
|
||||
|
||||
// 3. 检查消息数量
|
||||
if (sessionData.messageCount < MIN_MESSAGE_COUNT) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
locale === 'zh-CN'
|
||||
? `消息数量少于${MIN_MESSAGE_COUNT}条,无需生成摘要`
|
||||
: `Message count less than ${MIN_MESSAGE_COUNT}, no need to generate summary`,
|
||||
error: t('summary.tooFewMessages', { count: MIN_MESSAGE_COUNT }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,10 +352,7 @@ export async function generateSessionSummary(
|
||||
if (validMessages.length < MIN_MESSAGE_COUNT) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
locale === 'zh-CN'
|
||||
? `有效消息数量少于${MIN_MESSAGE_COUNT}条,无需生成摘要`
|
||||
: `Valid message count less than ${MIN_MESSAGE_COUNT}, no need to generate summary`,
|
||||
error: t('summary.tooFewValidMessages', { count: MIN_MESSAGE_COUNT }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +410,7 @@ export async function generateSessionSummary(
|
||||
async function generateDirectSummary(content: string, lengthLimit: number, locale: string): Promise<string> {
|
||||
const response = await chat(
|
||||
[
|
||||
{ role: 'system', content: '你是一个对话摘要专家,擅长用简洁的语言总结对话内容。' },
|
||||
{ role: 'system', content: t('summary.systemPromptDirect') },
|
||||
{ role: 'user', content: buildSummaryPrompt(content, lengthLimit, locale) },
|
||||
],
|
||||
{
|
||||
@@ -391,7 +439,7 @@ async function generateMapReduceSummary(
|
||||
const segmentContent = formatMessages(segments[i])
|
||||
const response = await chat(
|
||||
[
|
||||
{ role: 'system', content: '你是一个对话摘要专家,擅长用简洁的语言总结对话内容。' },
|
||||
{ role: 'system', content: t('summary.systemPromptDirect') },
|
||||
{ role: 'user', content: buildSubSummaryPrompt(segmentContent, locale) },
|
||||
],
|
||||
{
|
||||
@@ -409,7 +457,7 @@ async function generateMapReduceSummary(
|
||||
|
||||
const mergeResponse = await chat(
|
||||
[
|
||||
{ role: 'system', content: '你是一个对话摘要专家,擅长将多个摘要合并成一个连贯的总结。' },
|
||||
{ role: 'system', content: t('summary.systemPromptMerge') },
|
||||
{ role: 'user', content: buildMergePrompt(subSummaries, lengthLimit, locale) },
|
||||
],
|
||||
{
|
||||
@@ -447,7 +495,7 @@ export async function generateSessionSummaries(
|
||||
|
||||
if (result.success) {
|
||||
success++
|
||||
} else if (result.error?.includes('少于') || result.error?.includes('less than')) {
|
||||
} else if (result.error?.includes('少于') || result.error?.includes('less than') || result.error?.includes('few')) {
|
||||
skipped++
|
||||
} else {
|
||||
failed++
|
||||
@@ -479,20 +527,20 @@ export function checkSessionsCanGenerateSummary(
|
||||
const sessionData = getSessionMessagesForSummary(dbSessionId, chatSessionId)
|
||||
|
||||
if (!sessionData) {
|
||||
results.set(chatSessionId, { canGenerate: false, reason: '会话不存在' })
|
||||
results.set(chatSessionId, { canGenerate: false, reason: t('summary.sessionNotExist') })
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查原始消息数量
|
||||
if (sessionData.messageCount < MIN_MESSAGE_COUNT) {
|
||||
results.set(chatSessionId, { canGenerate: false, reason: '消息太少' })
|
||||
results.set(chatSessionId, { canGenerate: false, reason: t('summary.messagesTooFew') })
|
||||
continue
|
||||
}
|
||||
|
||||
// 预处理:过滤无意义消息
|
||||
const validMessages = preprocessMessages(sessionData.messages)
|
||||
if (validMessages.length < MIN_MESSAGE_COUNT) {
|
||||
results.set(chatSessionId, { canGenerate: false, reason: '有效消息太少' })
|
||||
results.set(chatSessionId, { canGenerate: false, reason: t('summary.validMessagesTooFew') })
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type { ToolDefinition, ToolCall } from '../llm/types'
|
||||
import type { ToolRegistry, RegisteredTool, ToolContext, ToolExecutionResult, ToolExecutor } from './types'
|
||||
import { isEmbeddingEnabled } from '../rag'
|
||||
import { t as i18nT } from '../../i18n'
|
||||
|
||||
// 导出类型
|
||||
export * from './types'
|
||||
@@ -44,23 +45,65 @@ export async function ensureToolsInitialized(): Promise<void> {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译工具定义的 description 和参数 description
|
||||
* 使用 i18next 查找翻译,如果未找到则保留原始文本(中文)
|
||||
*
|
||||
* i18n 键命名规则:
|
||||
* - 工具描述:ai.tools.{toolName}.desc
|
||||
* - 参数描述:ai.tools.{toolName}.params.{paramName}
|
||||
*/
|
||||
function translateToolDefinition(tool: ToolDefinition): ToolDefinition {
|
||||
const name = tool.function.name
|
||||
const descKey = `ai.tools.${name}.desc`
|
||||
const translatedDesc = i18nT(descKey)
|
||||
|
||||
// 深拷贝并翻译参数描述
|
||||
const translatedProperties: typeof tool.function.parameters.properties = {}
|
||||
for (const [paramName, param] of Object.entries(tool.function.parameters.properties)) {
|
||||
const paramKey = `ai.tools.${name}.params.${paramName}`
|
||||
const translatedParamDesc = i18nT(paramKey)
|
||||
translatedProperties[paramName] = {
|
||||
...param,
|
||||
// 如果 i18next 返回的是 key 本身,说明没有找到翻译,保留原始文本
|
||||
description: translatedParamDesc !== paramKey ? translatedParamDesc : param.description,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: tool.type,
|
||||
function: {
|
||||
name: tool.function.name,
|
||||
// 如果 i18next 返回的是 key 本身,说明没有找到翻译,保留原始文本
|
||||
description: translatedDesc !== descKey ? translatedDesc : tool.function.description,
|
||||
parameters: {
|
||||
type: tool.function.parameters.type,
|
||||
properties: translatedProperties,
|
||||
required: tool.function.parameters.required,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的工具定义
|
||||
* 根据配置动态过滤工具(如:语义搜索工具仅在启用 Embedding 时可用)
|
||||
* 根据当前 locale 动态翻译工具描述(解决"响应式"陷阱:每次调用时实时翻译)
|
||||
* @returns 工具定义数组(用于传给 LLM)
|
||||
*/
|
||||
export async function getAllToolDefinitions(): Promise<ToolDefinition[]> {
|
||||
await ensureToolsInitialized()
|
||||
|
||||
const allTools = Array.from(toolRegistry.values()).map((t) => t.definition)
|
||||
const allTools = Array.from(toolRegistry.values()).map((reg) => reg.definition)
|
||||
|
||||
// 根据 Embedding 配置决定是否包含语义搜索工具
|
||||
const embeddingEnabled = isEmbeddingEnabled()
|
||||
if (!embeddingEnabled) {
|
||||
return allTools.filter((t) => t.function.name !== 'semantic_search_messages')
|
||||
}
|
||||
const filteredTools = embeddingEnabled
|
||||
? allTools
|
||||
: allTools.filter((tool) => tool.function.name !== 'semantic_search_messages')
|
||||
|
||||
return allTools
|
||||
// 所有 locale 统一走翻译层,确保 locale 文件同构
|
||||
return filteredTools.map(translateToolDefinition)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +130,7 @@ export async function executeToolCall(toolCall: ToolCall, context: ToolContext):
|
||||
return {
|
||||
toolName,
|
||||
success: false,
|
||||
error: `工具 "${toolName}" 未注册`,
|
||||
error: i18nT('tools.notRegistered', { toolName }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user