添加了 TTS 相关服务并更新了设置

This commit is contained in:
1600822305 2025-04-18 04:06:39 +08:00
parent 877beeab43
commit 53708a973f
69 changed files with 36250 additions and 1411 deletions

View File

@ -7,6 +7,8 @@
const http = require('http') const http = require('http')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const net = require('net')
const crypto = require('crypto')
// 输出环境信息 // 输出环境信息
console.log('ASR Server (Embedded) starting...') console.log('ASR Server (Embedded) starting...')
@ -105,11 +107,271 @@ process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason) console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
}) })
// WebSocket客户端管理
const clients = {
browser: null,
electron: null
}
// 处理WebSocket连接
server.on('upgrade', (request, socket, head) => {
try {
console.log('[WebSocket] Connection upgrade request received')
// 解析WebSocket密钥
const key = request.headers['sec-websocket-key']
const acceptKey = crypto.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
.digest('base64')
// 发送WebSocket握手响应
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
'\r\n'
)
console.log('[WebSocket] Handshake successful')
// 处理WebSocket数据
handleWebSocketConnection(socket)
} catch (error) {
console.error('[WebSocket] Error handling upgrade:', error)
socket.destroy()
}
})
// 处理WebSocket连接
function handleWebSocketConnection(socket) {
let buffer = Buffer.alloc(0)
let role = null
socket.on('data', (data) => {
try {
buffer = Buffer.concat([buffer, data])
// 处理数据帧
while (buffer.length > 2) {
// 检查是否有完整的帧
const firstByte = buffer[0]
const secondByte = buffer[1]
const isFinalFrame = Boolean((firstByte >>> 7) & 0x1)
const [opCode, maskFlag, payloadLength] = [
firstByte & 0xF, (secondByte >>> 7) & 0x1, secondByte & 0x7F
]
// 处理不同的负载长度
let payloadStartIndex = 2
let payloadLen = payloadLength
if (payloadLength === 126) {
payloadLen = buffer.readUInt16BE(2)
payloadStartIndex = 4
} else if (payloadLength === 127) {
// 处理大于16位的长度
payloadLen = Number(buffer.readBigUInt64BE(2))
payloadStartIndex = 10
}
// 处理掩码
let maskingKey
if (maskFlag) {
maskingKey = buffer.slice(payloadStartIndex, payloadStartIndex + 4)
payloadStartIndex += 4
}
// 检查是否有足够的数据
const frameEnd = payloadStartIndex + payloadLen
if (buffer.length < frameEnd) {
// 需要更多数据
break
}
// 提取负载
let payload = buffer.slice(payloadStartIndex, frameEnd)
// 如果有掩码,解码负载
if (maskFlag && maskingKey) {
for (let i = 0; i < payload.length; i++) {
payload[i] = payload[i] ^ maskingKey[i % 4]
}
}
// 处理不同的操作码
if (opCode === 0x8) {
// 关闭帧
console.log('[WebSocket] Received close frame')
socket.end()
return
} else if (opCode === 0x9) {
// Ping
sendPong(socket)
} else if (opCode === 0x1 || opCode === 0x2) {
// 文本或二进制数据
const message = opCode === 0x1 ? payload.toString('utf8') : payload
handleMessage(socket, message, role)
}
// 移除已处理的帧
buffer = buffer.slice(frameEnd)
}
} catch (error) {
console.error('[WebSocket] Error processing data:', error)
}
})
socket.on('close', () => {
console.log(`[WebSocket] Connection closed${role ? ` (${role})` : ''}`)
if (role === 'browser') {
clients.browser = null
} else if (role === 'electron') {
clients.electron = null
}
})
socket.on('error', (error) => {
console.error(`[WebSocket] Socket error${role ? ` (${role})` : ''}:`, error)
})
}
// 发送WebSocket数据
function sendWebSocketFrame(socket, data, opCode = 0x1) {
try {
const payload = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data))
const payloadLength = payload.length
let header
if (payloadLength < 126) {
header = Buffer.from([0x80 | opCode, payloadLength])
} else if (payloadLength < 65536) {
header = Buffer.alloc(4)
header[0] = 0x80 | opCode
header[1] = 126
header.writeUInt16BE(payloadLength, 2)
} else {
header = Buffer.alloc(10)
header[0] = 0x80 | opCode
header[1] = 127
header.writeBigUInt64BE(BigInt(payloadLength), 2)
}
socket.write(Buffer.concat([header, payload]))
} catch (error) {
console.error('[WebSocket] Error sending data:', error)
}
}
// 发送Pong响应
function sendPong(socket) {
const pongFrame = Buffer.from([0x8A, 0x00])
socket.write(pongFrame)
}
// 处理消息
function handleMessage(socket, message, currentRole) {
try {
if (typeof message === 'string') {
const data = JSON.parse(message)
// 处理身份识别
if (data.type === 'identify') {
const role = data.role
if (role === 'browser' || role === 'electron') {
console.log(`[WebSocket] Client identified as: ${role}`)
// 存储客户端连接
clients[role] = socket
// 设置当前连接的角色
socket._role = role
return
}
}
// 获取当前连接的角色
const role = currentRole || socket._role
// 转发消息
if (role === 'browser') {
// 浏览器发送的消息转发给Electron
if (clients.electron) {
console.log(`[WebSocket] Browser -> Electron: ${JSON.stringify(data)}`)
sendWebSocketFrame(clients.electron, message)
} else {
console.log('[WebSocket] Cannot forward message: Electron client not connected')
}
} else if (role === 'electron') {
// Electron发送的消息转发给浏览器
if (clients.browser) {
console.log(`[WebSocket] Electron -> Browser: ${JSON.stringify(data)}`)
sendWebSocketFrame(clients.browser, message)
} else {
console.log('[WebSocket] Cannot forward message: Browser client not connected')
}
} else {
console.log(`[WebSocket] Received message from unknown role: ${message}`)
}
}
} catch (error) {
console.error('[WebSocket] Error handling message:', error, message)
}
}
// 检查端口是否被占用
function isPortAvailable(port) {
return new Promise((resolve) => {
const testServer = require('net').createServer()
testServer.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`[Server] Port ${port} is in use, trying another port...`)
resolve(false)
} else {
console.error(`[Server] Error checking port ${port}:`, err)
resolve(false)
}
})
testServer.once('listening', () => {
testServer.close()
resolve(true)
})
testServer.listen(port)
})
}
// 找到可用的端口
async function findAvailablePort(startPort) {
let port = startPort
const maxPort = startPort + 10 // 尝试最多10个端口
while (port < maxPort) {
if (await isPortAvailable(port)) {
return port
}
port++
}
throw new Error(`Could not find an available port between ${startPort} and ${maxPort-1}`)
}
// 尝试启动服务器 // 尝试启动服务器
try { (async () => {
const port = 34515 try {
// 默认端口
const defaultPort = 34515
// 找到可用的端口
const port = await findAvailablePort(defaultPort)
// 将端口号写入文件,便于主进程读取
const portFilePath = path.join(__dirname, 'port.txt')
fs.writeFileSync(portFilePath, port.toString(), 'utf8')
console.log(`[Server] Port ${port} is available, saved to ${portFilePath}`)
// 启动服务器
server.listen(port, () => { server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`) console.log(`[Server] Server running at http://localhost:${port}`)
// 写入成功标记
fs.writeFileSync(path.join(__dirname, 'server-ready.txt'), 'ready', 'utf8')
}) })
// 处理服务器错误 // 处理服务器错误
@ -117,7 +379,91 @@ try {
console.error(`[Server] Failed to start server:`, error) console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start process.exit(1) // Exit if server fails to start
}) })
} catch (error) {
// 保持进程运行
// 使用定时器保持进程运行
const keepAliveInterval = setInterval(() => {
console.log('[Server] Keep alive ping...')
}, 10000) // 每10秒发送一次日志保持进程运行
// 添加信号处理程序
process.on('SIGINT', () => {
console.log('[Server] Received SIGINT signal, shutting down...')
clearInterval(keepAliveInterval)
server.close()
process.exit(0)
})
process.on('SIGTERM', () => {
console.log('[Server] Received SIGTERM signal, shutting down...')
clearInterval(keepAliveInterval)
server.close()
process.exit(0)
})
// 处理进程退出
process.on('exit', () => {
console.log('[Server] Process is exiting, cleaning up resources...')
try {
// 清除定时器
if (keepAliveInterval) {
clearInterval(keepAliveInterval)
}
// 关闭服务器
if (server) {
try {
server.close()
} catch (err) {
console.error('[Server] Error closing server:', err)
}
}
// 删除端口文件
if (fs.existsSync(portFilePath)) {
fs.unlinkSync(portFilePath)
console.log('[Server] Removed port file:', portFilePath)
}
// 删除就绪标记
const readyFilePath = path.join(__dirname, 'server-ready.txt')
if (fs.existsSync(readyFilePath)) {
fs.unlinkSync(readyFilePath)
console.log('[Server] Removed ready file:', readyFilePath)
}
console.log('[Server] Cleanup completed')
} catch (e) {
console.error('[Server] Error cleaning up files:', e)
}
})
// 添加未捕获异常处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 尝试清理资源
try {
if (keepAliveInterval) {
clearInterval(keepAliveInterval)
}
if (fs.existsSync(portFilePath)) {
fs.unlinkSync(portFilePath)
}
const readyFilePath = path.join(__dirname, 'server-ready.txt')
if (fs.existsSync(readyFilePath)) {
fs.unlinkSync(readyFilePath)
}
} catch (e) {
console.error('[Server] Error cleaning up after uncaught exception:', e)
}
// 给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
} catch (error) {
console.error('[Server] Critical error starting server:', error) console.error('[Server] Critical error starting server:', error)
process.exit(1) process.exit(1)
} }
})()

69
asr-server/test.js Normal file
View File

@ -0,0 +1,69 @@
/**
* ASR服务器测试脚本
* 用于测试ASR服务器是否正常工作
*/
const WebSocket = require('ws');
const http = require('http');
// 测试HTTP服务器
console.log('测试HTTP服务器...');
http.get('http://localhost:34515', (res) => {
console.log(`HTTP状态码: ${res.statusCode}`);
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('HTTP响应接收完成');
console.log(`响应长度: ${data.length} 字节`);
console.log('HTTP测试完成');
// 测试WebSocket
testWebSocket();
});
}).on('error', (err) => {
console.error('HTTP测试失败:', err.message);
});
// 测试WebSocket
function testWebSocket() {
console.log('\n测试WebSocket...');
const ws = new WebSocket('ws://localhost:34515');
ws.on('open', () => {
console.log('WebSocket连接已打开');
// 发送身份识别消息
ws.send(JSON.stringify({
type: 'identify',
role: 'electron'
}));
// 发送测试消息
setTimeout(() => {
console.log('发送测试消息...');
ws.send(JSON.stringify({
type: 'test',
message: '这是一条测试消息'
}));
}, 1000);
// 关闭连接
setTimeout(() => {
console.log('关闭WebSocket连接...');
ws.close();
console.log('测试完成');
}, 2000);
});
ws.on('message', (data) => {
console.log(`收到WebSocket消息: ${data}`);
});
ws.on('error', (error) => {
console.error('WebSocket测试失败:', error.message);
});
}

64
check_json.js Normal file
View File

@ -0,0 +1,64 @@
const fs = require('fs');
const path = require('path');
// 读取agents.json文件
const filePath = path.join('resources', 'data', 'agents.json');
fs.readFile(filePath, (err, data) => {
if (err) {
console.error('读取文件失败:', err);
return;
}
// 输出文件的前20个字节的十六进制表示
console.log('文件前20个字节:');
for (let i = 0; i < Math.min(20, data.length); i++) {
console.log(`字节 ${i}: 0x${data[i].toString(16)} (${String.fromCharCode(data[i])})`);
}
// 尝试不同的方式解析JSON
console.log('\n尝试不同的方式解析JSON:');
// 1. 直接解析
try {
const json1 = JSON.parse(data);
console.log('方法1成功: 直接解析');
} catch (e) {
console.error('方法1失败:', e.message);
}
// 2. 转换为字符串后解析
try {
const json2 = JSON.parse(data.toString());
console.log('方法2成功: 转换为字符串后解析');
} catch (e) {
console.error('方法2失败:', e.message);
}
// 3. 移除BOM后解析
try {
const str = data.toString();
const noBomStr = str.charCodeAt(0) === 0xFEFF ? str.slice(1) : str;
const json3 = JSON.parse(noBomStr);
console.log('方法3成功: 移除BOM后解析');
} catch (e) {
console.error('方法3失败:', e.message);
}
// 4. 移除前3个字符后解析
try {
const str = data.toString().slice(3);
const json4 = JSON.parse(str);
console.log('方法4成功: 移除前3个字符后解析');
} catch (e) {
console.error('方法4失败:', e.message);
}
// 5. 移除所有非ASCII字符后解析
try {
const str = data.toString().replace(/[^\x20-\x7E]/g, '');
const json5 = JSON.parse(str);
console.log('方法5成功: 移除所有非ASCII字符后解析');
} catch (e) {
console.error('方法5失败:', e.message);
}
});

1073
checkpoint.py Normal file

File diff suppressed because it is too large Load Diff

20
checkpoint_config.json Normal file
View File

@ -0,0 +1,20 @@
{
"project_path": "J:\\Cherry\\cherry-studioTTS",
"backup_path": "J:\\Cherry\\Backups",
"excludes": [
"node_modules",
"dist",
"out",
".git",
"release",
"__pycache__",
"*.log",
"*.lock",
"*.exe",
"*.dll",
"*.zip",
"*.tar.gz",
"checkpoint_config.json",
"checkpoint_info.txt"
]
}

343
const extractedData = {.md Normal file
View File

@ -0,0 +1,343 @@
const extractedData = {
/**
* Descriptions of tools available within the system.
* These were likely found in class definitions extending a base 'Tool' or 'Vr' class.
*/
toolDescriptions: [
{
name: 'shell', // Inferred from zi.shell constant usage
description: `Execute a shell command.
- You can use this tool to interact with the user's local version control system. Do not use the
retrieval tool for that purpose.
- If there is a more specific tool available that can perform the function, use that tool instead of
this one.
The OS is ${process.platform}. The shell is '${/*this._shellName - determined dynamically*/ ''}'.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'webFetch', // Inferred from zi.webFetch constant usage
description: `Fetches data from a webpage and converts it into Markdown.
1. The tool takes in a URL and returns the content of the page in Markdown format;
2. If the return is not valid Markdown, it means the tool cannot successfully parse this page.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'readFile', // Inferred from Hn.readFile constant usage
description: "Read a file."
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'saveFile', // Inferred from Hn.saveFile constant usage
description: `Save a new file. Use this tool to write new files with the attached content. It CANNOT modify existing files. Do NOT use this tool to edit an existing file by overwriting it entirely. Use the str-replace-editor tool to edit existing files instead.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'editFile', // Inferred from Hn.editFile constant usage
description: `
Edit a file. Accepts a file path and a description of the edit.
This tool can edit whole files.
The description should be detailed and precise, and include all required information to perform the edit.
It can include both natural language and code. It can include multiple code snippets to described different
edits in the file. It can include descriptions of how to perform these edits precisely.
All the contents that should go in a file should be placed in a markdown code block, like this:
<begin-example>
Add a function called foo.
\`\`\`
def foo():
...
\`\`\`
</end-example>
This includes all contents, even if it's not code.
Be precise or I will take away your toys.
Prefer to use this tool when editing parts of a file.
`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'strReplaceEditor', // Inferred from zi.strReplaceEditor constant usage
description: `Custom editing tool for viewing, creating and editing files
* \`path\` is a file path relative to the workspace root
* command \`view\` displays the result of applying \`cat -n\`.
* If a \`command\` generates a long output, it will be truncated and marked with \`<response clipped>\`
* \`insert\` and \`str_replace\` commands output a snippet of the edited section for each entry. This snippet reflects the final state of the file after all edits and IDE auto-formatting have been applied.
Notes for using the \`str_replace\` command:
* Use the \`str_replace_entries\` parameter with an array of objects
* Each object should have \`old_str\`, \`new_str\`, \`old_str_start_line_number\` and \`old_str_end_line_number\` properties
* The \`old_str_start_line_number\` and \`old_str_end_line_number\` parameters are 1-based line numbers
* Both \`old_str_start_line_number\` and \`old_str_end_line_number\` are INCLUSIVE
* The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespace!
* Empty \`old_str\` is allowed only when the file is empty or contains only whitespaces
* It is important to specify \`old_str_start_line_number\` and \`old_str_end_line_number\` to disambiguate between multiple occurrences of \`old_str\` in the file
* Make sure that \`old_str_start_line_number\` and \`old_str_end_line_number\` do not overlap with other entries in \`str_replace_entries\`
Notes for using the \`insert\` command:
* Use the \`insert_line_entries\` parameter with an array of objects
* Each object should have \`insert_line\` and \`new_str\` properties
* The \`insert_line\` parameter specifies the line number after which to insert the new string
* The \`insert_line\` parameter is 1-based line number
* To insert at the very beginning of the file, use \`insert_line: 0\`
Notes for using the \`view\` command:
* Strongly prefer to use larger ranges of at least 1000 lines when scanning through files. One call with large range is much more efficient than many calls with small ranges
IMPORTANT:
* This is the only tool you should use for editing files.
* If it fails try your best to fix inputs and retry.
* DO NOT fall back to removing the whole file and recreating it from scratch.
* DO NOT use sed or any other command line tools for editing files.
* Try to fit as many edits in one tool call as possible
* Use view command to read the file before editing it.
`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'removeFiles', // Inferred from Hn.removeFiles constant usage
description: `Remove files. ONLY use this tool to delete files in the user's workspace. This is the only safe tool to delete files in a way that the user can undo the change. Do NOT use the shell or launch-process tools to remove files.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'remember', // Inferred from zi.remember constant usage
description: `Call this tool when user asks you:
- to remember something
- to create memory/memories
Use this tool only with information that can be useful in the long-term.
Do not use this tool for temporary information.
`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'launchProcess', // Inferred from Hn.launchProcess constant usage
description: `Launch a new process with a shell command. A process can be waiting (\`wait=true\`) or non-waiting (\`wait=false\`, which is default).
If \`wait=true\`, launches the process in an interactive terminal, and waits for the process to complete up to
\`wait_seconds\` seconds (default: 60). If the process ends
during this period, the tool call returns. If the timeout expires, the process will continue running in the
background but the tool call will return. You can then interact with the process using the other process tools.
Note: Only one waiting process can be running at a time. If you try to launch a process with \`wait=true\`
while another is running, the tool will return an error.
If \`wait=false\`, launches a background process in a separate terminal. This returns immediately, while the
process keeps running in the background.
Notes:
- Use \`wait=true\` processes when the command is expected to be short, or when you can't
proceed with your task until the process is complete. Use \`wait=false\` for processes that are
expected to run in the background, such as starting a server you'll need to interact with, or a
long-running process that does not need to complete before proceeding with the task.
- If this tool returns while the process is still running, you can continue to interact with the process
using the other available tools. You can wait for the process, read from it, write to it, kill it, etc.
- You can use this tool to interact with the user's local version control system. Do not use the
retrieval tool for that purpose.
- If there is a more specific tool available that can perform the function, use that tool instead of
this one.
The OS is ${process.platform}.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'killProcess', // Inferred from Hn.killProcess constant usage
description: "Kill a process by its process ID."
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'readProcess', // Inferred from Hn.readProcess constant usage
description: `Read output from a running process.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'writeProcess', // Inferred from Hn.writeProcess constant usage
description: `Write input to a process's stdin.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'listProcesses', // Inferred from Hn.listProcesses constant usage
description: "List all known processes and their states."
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'waitProcess', // Inferred from Hn.waitProcess constant usage
description: "Wait for a process to complete or timeout."
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'codebaseRetrieval', // Inferred from zi.codebaseRetrieval constant usage
description: `This tool is Augment's context engine, the world's best codebase context engine. It:
1. Takes in a natural language description of the code you are looking for;
2. Uses a proprietary retrieval/embedding model suite that produces the highest-quality recall of relevant code snippets from across the codebase;
3. Maintains a real-time index of the codebase, so the results are always up-to-date and reflects the current state of the codebase on the disk;
4. Can retrieve across different programming languages;
5. Only reflects the current state of the codebase on the disk, and has no information on version control or code history.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
},
{
name: 'openBrowser', // Inferred from Hn.openBrowser constant usage
description: `Open a URL in the default browser.
1. The tool takes in a URL and opens it in the default browser.
2. The tool does not return any content. It is intended for the user to visually inspect and interact with the page. You will not have access to it.
3. You should not use \`open-browser\` on a URL that you have called the tool on before in the conversation history, because the page is already open in the user's browser and the user can see it and refresh it themselves. Each time you call \`open-browser\`, it will jump the user to the browser window, which is highly annoying to the user.`
// inputSchemaJson is omitted for brevity, but was present in the original code.
}
],
/**
* Prompt templates and snippets used for various tasks like onboarding,
* codebase orientation, memory injection/compression, etc.
* Placeholders like {variable} indicate dynamic content insertion.
*/
promptTemplates: {
onboarding: {
introduction: `
Instructions for you:
- Repeat the following markdown without the codeblock
\`\`\`
# \u{1F44B} Meet Auggie
I'm your AI coding assistant. I excel at understanding large, complex codebases but I am happy to chip in on codebases of all sizes.
\`\`\`
`,
gitConfigured: `
Instructions for you:
- Repeat the following markdown without the codeblock
\`\`\`
## Who are you?
Mind if I fetch some information about you from your git configuration:
\`\`\`
- Run \`git config user.name\` and \`git config user.email\`
- Repeat the following text in the codeblock
\`\`\`
Hey, {name}, since I am an LLM and I don't have a real memory (sad) I'll be using \`\u{1F4E6} Augment Memories\`
\`\`\`
`,
memories: `
Instructions for you:
- Repeat the following markdown without the codeblock
\`\`\`
## How I work
* **Augment Memories:** Project-specific memories
* New folder = clean slate
* I learn from my mistakes when you correct me
* You can ask me to remember things (e.g. "commit to memory...")
* **Native Integrations:** Configure integrations like GitHub + Linear with 1-click over in [Settings](command:vscode-augment.showSettingsPanel)
\`\`\`
`
},
orientation: {
localization: "{languageTree} prompt for {programmingLanguage}", // Inferred template name and structure
detectLanguages: "Detect languages prompt using {fileExtensionsList}", // Inferred template name and structure
compression: "Compression prompt using {assembledKnowledge}", // Inferred template name and structure
buildTest: "Build/test query template for {language} using {rootFolderContent} and {locationList}" // Inferred template name and structure
},
memories: {
injection: "Inject new memory '{newMemory}' into current memories:\n{currentMemories}", // Inferred structure
complexInjection: "Inject complex new memory '{newMemory}' into current memories:\n{currentMemories}", // Inferred structure
compression: "Compress memories:\n{memories}\nTarget size: {compressionTarget}", // Inferred structure
recentMemoriesSubprompt: "Consider these recent memories:\n{recentMemories}", // Inferred structure
classifyAndDistill: "Classify and distill message: {message}", // Inferred structure
distill: "Distill message: {message}" // Inferred structure
},
contextualSnippets: {
folderContext: `- The user is working from the directory \`\${relPath}\`.
- When the user mentions a file name or when viewing output from shell commands, it is likely relative to \`\${relPath}\`.
- When creating, deleting, viewing or editing files, first try prepending \`\${relPath}\` to the path.
- When running shell commands, do not prepend \`\${relPath}\` to the path.
` // Found as variable `qgt`
},
memoriesFileHeader: [
// Multiple variations exist, likely chosen based on usage count
String.raw`
__ __ _
| \/ | (_)
| \ / | ___ _ __ ___ ___ _ __ _ ___ ___
| |\/| |/ _ \ '_ ' _ \ / _ \| '__| |/ _ \/ __|
| | | | __/ | | | | | (_) | | | | __/\__ \
|_| |_|\___|_| |_| |_|\___/|_| |_|\___||___/
.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.
( Memories help me remember useful details for future interactions. )
) (
( During Agent sessions, I'll try to create useful Memories automatically. )
)Memories can be about your codebase, technologies or your personal preferences.(
( )
)Your Memories belong to you and are stored locally at the bottom of this file; (
( in the future, we may give you an option to share your memories with others. )
) (
( NOTE: Memories will be compressed when this file grows too large. )
)For personal Memories: consider putting them in User Guidelines (via '@' menu) (
( For repository-level Memories: consider using '.augment-guidelines' file )
)Neither will be compressed. (
( )
)Happy Coding! (
( )
"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"+.+"
()
O
o
{AUGGIE_LOGO}
↓↓↓ MEMORIES START HERE ↓↓↓
`,
String.raw`
__ __ _
| \/ | (_)
| \ / | ___ _ __ ___ ___ _ __ _ ___ ___
| |\/| |/ _ \ '_ ' _ \ / _ \| '__| |/ _ \/ __|
| | | | __/ | | | | | (_) | | | | __/\__ \
|_| |_|\___|_| |_| |_|\___/|_| |_|\___||___/
__________________________________________________________________________________
/\ \
\_| NOTE: Memories will be compressed when this file grows too large. |
| For personal Memories: consider putting them in User Guidelines (via '@' menu) |
| For repository-level Memories: consider using '.augment-guidelines' file |
| Neither will be compressed. |
| _____________________________________________________________________________|_
\_/_______________________________________________________________________________/
↓↓↓ MEMORIES START HERE ↓↓↓
`
],
commitMessage: {
// The actual prompt template for commit messages is likely constructed dynamically
// within the `CommitMessagePromptPreparer` class, combining diffs and commit history.
// Representing the core idea here.
generate: `Generate a commit message based on the following changes:
<diff>
{diff}
</diff>
Consider these recent relevant commits by the same author:
<relevant_commits>
{relevant_commit_messages}
</relevant_commits>
Consider these example commits from the repository:
<example_commits>
{example_commit_messages}
</example_commits>
Changed file stats:
{changedFileStats}`
}
}
};
// Example usage (optional):
// console.log(JSON.stringify(extractedData, null, 2));

14
create_empty_agents.js Normal file
View File

@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');
// 创建一个空的agents.json文件
const emptyAgents = [];
const filePath = path.join('resources', 'data', 'agents.json');
// 备份原始文件
fs.copyFileSync(filePath, filePath + '.bak');
console.log('已备份原始文件到 ' + filePath + '.bak');
// 写入新文件
fs.writeFileSync(filePath, JSON.stringify(emptyAgents, null, 2), 'utf8');
console.log('已创建新的agents.json文件内容为空数组');

View File

@ -31,8 +31,8 @@ files:
- '!**/{.DS_Store,Thumbs.db}' - '!**/{.DS_Store,Thumbs.db}'
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}' - '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer' - '!node_modules/rollup-plugin-visualizer'
- '!node_modules/js-tiktoken' # - '!node_modules/js-tiktoken'
- '!node_modules/@tavily/core/node_modules/js-tiktoken' # - '!node_modules/@tavily/core/node_modules/js-tiktoken'
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
asarUnpack: # Removed ASR server rules from 'files' section asarUnpack: # Removed ASR server rules from 'files' section
@ -54,11 +54,9 @@ win:
- target: nsis - target: nsis
arch: arch:
- x64 - x64
- arm64
- target: portable - target: portable
arch: arch:
- x64 - x64
- arm64
nsis: nsis:
artifactName: ${productName}-${version}-${arch}-setup.${ext} artifactName: ${productName}-${version}-${arch}-setup.${ext}
shortcutName: ${productName} shortcutName: ${productName}

BIN
memory.db Normal file

Binary file not shown.

View File

@ -66,6 +66,7 @@
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@google/generative-ai": "^0.24.0", "@google/generative-ai": "^0.24.0",
"@langchain/community": "^0.3.36", "@langchain/community": "^0.3.36",
"@langchain/core": "^0.3.44",
"@mozilla/readability": "^0.6.0", "@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15", "@notionhq/client": "^2.2.15",
"@strongtz/win32-arm64-msvc": "^0.4.7", "@strongtz/win32-arm64-msvc": "^0.4.7",
@ -88,6 +89,7 @@
"fetch-socks": "^1.3.2", "fetch-socks": "^1.3.2",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"got-scraping": "^4.1.1", "got-scraping": "^4.1.1",
"js-tiktoken": "^1.0.19",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
@ -99,7 +101,8 @@
"turndown-plugin-gfm": "^1.0.2", "turndown-plugin-gfm": "^1.0.2",
"undici": "^7.4.0", "undici": "^7.4.0",
"webdav": "^5.8.0", "webdav": "^5.8.0",
"zipread": "^1.3.3" "zipread": "^1.3.3",
"zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@agentic/exa": "^7.3.3", "@agentic/exa": "^7.3.3",
@ -115,7 +118,7 @@
"@emotion/is-prop-valid": "^1.3.1", "@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1", "@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",
"@google/genai": "^0.4.0", "@google/genai": "^0.8.0",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.9.0", "@modelcontextprotocol/sdk": "^1.9.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,94 +1,486 @@
import { ChildProcess, spawn } from 'node:child_process' import http from 'node:http'
import net from 'node:net'
import crypto from 'node:crypto'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron' import { app, ipcMain } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import { getResourcePath } from '../utils'
/** /**
* ASR服务器服务ASR服务器进程 * ASR服务器服务ASR服务器进程
*/ */
class ASRServerService { export class ASRServerService {
private asrServerProcess: ChildProcess | null = null // HTML内容
private INDEX_HTML_CONTENT: string = '';
// 服务器相关属性
private httpServer: http.Server | null = null;
private wsClients: { browser: any | null; electron: any | null } = { browser: null, electron: null };
private serverPort: number = 34515; // 默认端口
private isServerRunning: boolean = false;
/**
*
*/
constructor() {
this.loadIndexHtml();
}
/**
* index.html文件
*/
private loadIndexHtml(): void {
try {
// 在开发环境和生产环境中使用不同的路径
let htmlPath = '';
if (app.isPackaged) {
// 生产环境
const resourcePath = getResourcePath();
htmlPath = path.join(resourcePath, 'app', 'asr-server', 'index.html');
} else {
// 开发环境
htmlPath = path.join(app.getAppPath(), 'asr-server', 'index.html');
}
log.info(`加载index.html文件: ${htmlPath}`);
if (fs.existsSync(htmlPath)) {
this.INDEX_HTML_CONTENT = fs.readFileSync(htmlPath, 'utf8');
log.info(`成功加载index.html文件`);
} else {
log.error(`index.html文件不存在: ${htmlPath}`);
// 使用默认的HTML内容
this.INDEX_HTML_CONTENT = `<!DOCTYPE html>
<html>
<head>
<title>ASR Server Error</title>
</head>
<body>
<h1>Error: index.html file not found</h1>
<p>Please make sure the ASR server files are properly installed.</p>
</body>
</html>`;
}
} catch (error) {
log.error(`加载index.html文件时出错:`, error);
// 使用默认的HTML内容
this.INDEX_HTML_CONTENT = `<!DOCTYPE html>
<html>
<head>
<title>ASR Server Error</title>
</head>
<body>
<h1>Error loading index.html</h1>
<p>An error occurred while loading the ASR server files.</p>
</body>
</html>`;
}
}
/** /**
* IPC处理程序 * IPC处理程序
*/ */
public registerIpcHandlers(): void { public registerIpcHandlers(): void {
// 启动ASR服务器 // 启动ASR服务器
ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this)) ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this));
// 停止ASR服务器 // 停止ASR服务器
ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this)) ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this));
} }
/** /**
* ASR服务器 *
* @returns Promise<{success: boolean, pid?: number, error?: string}> * @param port
* @returns truefalse
*/ */
private async startServer(): Promise<{ success: boolean; pid?: number; error?: string }> { private isPortAvailable(port: number): Promise<boolean> {
try { return new Promise((resolve) => {
if (this.asrServerProcess) { const testServer = net.createServer();
return { success: true, pid: this.asrServerProcess.pid } testServer.once('error', (err: any) => {
} if (err.code === 'EADDRINUSE') {
log.info(`端口 ${port} 已被占用,尝试其他端口...`);
// 获取服务器文件路径 resolve(false);
log.info('App path:', app.getAppPath())
// 在开发环境和生产环境中使用不同的路径
let serverPath = ''
const isPackaged = app.isPackaged
if (isPackaged) {
// 生产环境 (打包后) - 使用 extraResources 复制的路径
// 注意: 'app' 是 extraResources 配置中 'to' 字段的一部分
serverPath = path.join(process.resourcesPath, 'app', 'asr-server', 'server.js')
log.info('生产环境ASR 服务器路径:', serverPath)
} else { } else {
// 开发环境 - 指向项目根目录的 asr-server log.error(`检查端口 ${port} 时出错:`, err);
serverPath = path.join(app.getAppPath(), 'asr-server', 'server.js') resolve(false);
log.info('开发环境ASR 服务器路径:', serverPath) }
});
testServer.once('listening', () => {
testServer.close();
resolve(true);
});
testServer.listen(port);
});
} }
// 注意:删除了 isExeFile 检查逻辑, 假设总是用 node 启动 /**
// Removed unused variable 'isExeFile' *
log.info('ASR服务器路径:', serverPath) * @param startPort
* @returns
*/
private async findAvailablePort(startPort: number): Promise<number> {
let port = startPort;
const maxPort = startPort + 10; // 尝试最多10个端口
// 检查文件是否存在 while (port < maxPort) {
if (!fs.existsSync(serverPath)) { if (await this.isPortAvailable(port)) {
return { success: false, error: '服务器文件不存在' } return port;
}
port++;
} }
// 启动服务器进程 throw new Error(`${startPort}${maxPort-1} 之间找不到可用的端口`);
// 始终使用 node 启动 server.js }
log.info(`尝试使用 node 启动: ${serverPath}`)
this.asrServerProcess = spawn('node', [serverPath], {
stdio: 'pipe', // 'pipe' 用于捕获输出, 如果需要调试可以临时改为 'inherit'
detached: false // false 通常足够
})
// 处理服务器输出 /**
this.asrServerProcess.stdout?.on('data', (data) => { * HTTP请求
log.info(`[ASR Server] ${data.toString()}`) * @param req HTTP请求
}) * @param res HTTP响应
*/
private handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
// 只处理根路径请求返回index.html
if (req.url === '/' || req.url === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(this.INDEX_HTML_CONTENT);
log.info(`返回index.html到客户端`);
} else {
// 其他路径返回404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
log.info(`请求的路径不存在: ${req.url}`);
}
}
this.asrServerProcess.stderr?.on('data', (data) => {
log.error(`[ASR Server Error] ${data.toString()}`)
})
// 处理服务器退出 /**
this.asrServerProcess.on('close', (code) => { * ASR服务器
log.info(`[ASR Server] 进程退出,退出码: ${code}`) * @returns Promise<{success: boolean, pid?: number, port?: number, error?: string}>
this.asrServerProcess = null */
}) private async startServer(): Promise<{ success: boolean; pid?: number; port?: number; error?: string }> {
try {
// 如果服务器已经运行,直接返回成功
if (this.isServerRunning && this.httpServer) {
return { success: true, port: this.serverPort };
}
// 等待一段时间确保服务器启动 // 尝试找到可用的端口
await new Promise((resolve) => setTimeout(resolve, 1000)) try {
this.serverPort = await this.findAvailablePort(this.serverPort);
return { success: true, pid: this.asrServerProcess.pid }
} catch (error) { } catch (error) {
log.error('启动ASR服务器失败:', error) log.error('找不到可用的端口:', error);
return { success: false, error: (error as Error).message } return { success: false, error: '找不到可用的端口' };
}
log.info(`使用端口: ${this.serverPort}`);
// 创建HTTP服务器
this.httpServer = http.createServer(this.handleHttpRequest.bind(this));
// 启动HTTP服务器
try {
await new Promise<void>((resolve, reject) => {
if (!this.httpServer) {
reject(new Error('HTTP服务器创建失败'));
return;
}
this.httpServer.on('error', (err) => {
log.error(`HTTP服务器错误:`, err);
reject(err);
});
this.httpServer.listen(this.serverPort, () => {
log.info(`HTTP服务器已启动监听端口: ${this.serverPort}`);
resolve();
});
});
// 设置WebSocket处理
this.setupWebSocketServer();
// 标记服务器已启动
this.isServerRunning = true;
log.info(`ASR服务器启动成功端口: ${this.serverPort}`);
return { success: true, port: this.serverPort };
} catch (error) {
log.error('启动HTTP服务器失败:', error);
// 关闭HTTP服务器
if (this.httpServer) {
this.httpServer.close();
this.httpServer = null;
}
return { success: false, error: `启动HTTP服务器失败: ${(error as Error).message}` };
}
} catch (error) {
log.error('启动ASR服务器失败:', error);
return { success: false, error: (error as Error).message };
}
}
/**
* WebSocket服务器
*/
private setupWebSocketServer(): void {
if (!this.httpServer) {
log.error('HTTP服务器不存在无法设置WebSocket');
return;
}
// 处理WebSocket连接升级
this.httpServer.on('upgrade', (request, socket, _head) => {
try {
log.info('[WebSocket] 收到连接升级请求');
// 解析WebSocket密钥
const key = request.headers['sec-websocket-key'] as string;
const acceptKey = crypto
.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
.digest('base64');
// 发送WebSocket握手响应
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
'\r\n'
);
log.info('[WebSocket] 握手成功');
// 处理WebSocket数据
this.handleWebSocketConnection(socket);
} catch (error) {
log.error('[WebSocket] 处理升级错误:', error);
socket.destroy();
}
});
}
/**
* WebSocket连接
* @param socket
*/
private handleWebSocketConnection(socket: any): void {
let buffer = Buffer.alloc(0);
let role: 'browser' | 'electron' | null = null;
socket.on('data', (data: Buffer) => {
try {
buffer = Buffer.concat([buffer, data]);
// 处理数据帧
while (buffer.length > 2) {
// 检查是否有完整的帧
const firstByte = buffer[0];
const secondByte = buffer[1];
// const isFinalFrame = Boolean((firstByte >>> 7) & 0x1); // 暂时不使用
const [opCode, maskFlag, payloadLength] = [
firstByte & 0xF, (secondByte >>> 7) & 0x1, secondByte & 0x7F
];
// 处理不同的负载长度
let payloadStartIndex = 2;
let payloadLen = payloadLength;
if (payloadLength === 126) {
payloadLen = buffer.readUInt16BE(2);
payloadStartIndex = 4;
} else if (payloadLength === 127) {
// 处理大于16位的长度
payloadLen = Number(buffer.readBigUInt64BE(2));
payloadStartIndex = 10;
}
// 处理掩码
let maskingKey: Buffer | undefined;
if (maskFlag) {
maskingKey = buffer.slice(payloadStartIndex, payloadStartIndex + 4);
payloadStartIndex += 4;
}
// 检查是否有足够的数据
const frameEnd = payloadStartIndex + payloadLen;
if (buffer.length < frameEnd) {
// 需要更多数据
break;
}
// 提取负载
let payload = buffer.slice(payloadStartIndex, frameEnd);
// 如果有掩码,解码负载
if (maskFlag && maskingKey) {
for (let i = 0; i < payload.length; i++) {
payload[i] = payload[i] ^ maskingKey[i % 4];
}
}
// 处理不同的操作码
if (opCode === 0x8) {
// 关闭帧
log.info('[WebSocket] 收到关闭帧');
socket.end();
return;
} else if (opCode === 0x9) {
// Ping
this.sendPong(socket);
} else if (opCode === 0x1 || opCode === 0x2) {
// 文本或二进制数据
const message = opCode === 0x1 ? payload.toString('utf8') : payload;
this.handleMessage(socket, message, role);
}
// 移除已处理的帧
buffer = buffer.slice(frameEnd);
}
} catch (error) {
log.error('[WebSocket] 处理数据错误:', error);
}
});
socket.on('close', () => {
const socketRole = (socket as any)._role || role;
log.info(`[WebSocket] 连接关闭${socketRole ? ` (${socketRole})` : ''}`);
if (socketRole === 'browser') {
this.wsClients.browser = null;
// 如果浏览器断开连接通知Electron客户端
if (this.wsClients.electron) {
this.sendWebSocketFrame(this.wsClients.electron, JSON.stringify({
type: 'status',
message: 'Browser disconnected'
}));
log.info('[WebSocket] 已向Electron发送Browser disconnected状态');
}
} else if (socketRole === 'electron') {
this.wsClients.electron = null;
}
});
socket.on('error', (error: Error) => {
log.error(`[WebSocket] 套接字错误${role ? ` (${role})` : ''}:`, error);
});
}
/**
* WebSocket数据
* @param socket
* @param data
* @param opCode
*/
private sendWebSocketFrame(socket: any, data: string | object, opCode = 0x1): void {
try {
const payload = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data));
const payloadLength = payload.length;
let header: Buffer;
if (payloadLength < 126) {
header = Buffer.from([0x80 | opCode, payloadLength]);
} else if (payloadLength < 65536) {
header = Buffer.alloc(4);
header[0] = 0x80 | opCode;
header[1] = 126;
header.writeUInt16BE(payloadLength, 2);
} else {
header = Buffer.alloc(10);
header[0] = 0x80 | opCode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(payloadLength), 2);
}
socket.write(Buffer.concat([header, payload]));
} catch (error) {
log.error('[WebSocket] 发送数据错误:', error);
}
}
/**
* Pong响应
* @param socket
*/
private sendPong(socket: any): void {
const pongFrame = Buffer.from([0x8A, 0x00]);
socket.write(pongFrame);
}
/**
*
* @param socket
* @param message
* @param currentRole
*/
private handleMessage(socket: any, message: string | Buffer, currentRole: string | null): void {
try {
if (typeof message === 'string') {
const data = JSON.parse(message);
// 处理身份识别
if (data.type === 'identify') {
const role = data.role;
if (role === 'browser' || role === 'electron') {
log.info(`[WebSocket] 客户端识别为: ${role}`);
// 存储客户端连接
this.wsClients[role] = socket;
// 设置当前连接的角色
(socket as any)._role = role;
// 如果是浏览器连接通知Electron客户端
if (role === 'browser' && this.wsClients.electron) {
// 发送browser_ready消息
this.sendWebSocketFrame(this.wsClients.electron, JSON.stringify({
type: 'status',
message: 'browser_ready'
}));
log.info('[WebSocket] 已向Electron发送browser_ready状态');
// 发送Browser connected消息
this.sendWebSocketFrame(this.wsClients.electron, JSON.stringify({
type: 'status',
message: 'Browser connected'
}));
log.info('[WebSocket] 已向Electron发送Browser connected状态');
}
return;
}
}
// 获取当前连接的角色
const role = currentRole || (socket as any)._role;
// 转发消息
if (role === 'browser') {
// 浏览器发送的消息转发给Electron
if (this.wsClients.electron) {
log.info(`[WebSocket] Browser -> Electron: ${JSON.stringify(data)}`);
this.sendWebSocketFrame(this.wsClients.electron, message);
} else {
log.info('[WebSocket] 无法转发消息: Electron客户端未连接');
}
} else if (role === 'electron') {
// Electron发送的消息转发给浏览器
if (this.wsClients.browser) {
log.info(`[WebSocket] Electron -> Browser: ${JSON.stringify(data)}`);
this.sendWebSocketFrame(this.wsClients.browser, message);
} else {
log.info('[WebSocket] 无法转发消息: 浏览器客户端未连接');
}
} else {
log.info(`[WebSocket] 收到来自未知角色的消息: ${message}`);
}
}
} catch (error) {
log.error('[WebSocket] 处理消息错误:', error, message);
} }
} }
@ -100,32 +492,29 @@ class ASRServerService {
*/ */
private async stopServer( private async stopServer(
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
pid?: number _pid?: number
): Promise<{ success: boolean; error?: string }> { ): Promise<{ success: boolean; error?: string }> {
try { try {
if (!this.asrServerProcess) { // 关闭HTTP服务器
return { success: true } if (this.httpServer) {
this.httpServer.close();
this.httpServer = null;
} }
// 检查PID是否匹配 // 重置客户端连接
if (pid && this.asrServerProcess.pid !== pid) { this.wsClients = { browser: null, electron: null };
log.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${this.asrServerProcess.pid}) 不匹配`)
}
// 杀死进程 // 重置服务器状态
this.asrServerProcess.kill() this.isServerRunning = false;
// 等待一段时间确保进程已经退出 log.info('ASR服务器已停止');
await new Promise((resolve) => setTimeout(resolve, 500)) return { success: true };
this.asrServerProcess = null
return { success: true }
} catch (error) { } catch (error) {
log.error('停止ASR服务器失败:', error) log.error('停止ASR服务器失败:', error);
return { success: false, error: (error as Error).message } return { success: false, error: (error as Error).message };
} }
} }
} }
// 导出单例实 // 创建并导出单例
export const asrServerService = new ASRServerService() export const asrServerService = new ASRServerService();

View File

@ -46,7 +46,7 @@ export class GeminiService {
if (response.files) { if (response.files) {
return response.files return response.files
.filter((file) => file.state === FileState.ACTIVE) .filter((file) => file.state === FileState.ACTIVE)
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size) .find((i) => i.displayName === file.origin_name && parseInt(i.sizeBytes) === file.size)
} }
return undefined return undefined
} }

View File

@ -39,6 +39,9 @@ export class MemoryFileService {
} }
], ],
shortMemories: [], shortMemories: [],
assistantMemories: [],
assistantMemoryActive: true,
assistantMemoryAnalyzeModel: 'gpt-3.5-turbo',
analyzeModel: 'gpt-3.5-turbo', analyzeModel: 'gpt-3.5-turbo',
shortMemoryAnalyzeModel: 'gpt-3.5-turbo', shortMemoryAnalyzeModel: 'gpt-3.5-turbo',
historicalContextAnalyzeModel: 'gpt-3.5-turbo', historicalContextAnalyzeModel: 'gpt-3.5-turbo',
@ -77,6 +80,9 @@ export class MemoryFileService {
const defaultData = { const defaultData = {
memoryLists: [], memoryLists: [],
shortMemories: [], shortMemories: [],
assistantMemories: [],
assistantMemoryActive: true,
assistantMemoryAnalyzeModel: '',
analyzeModel: '', analyzeModel: '',
shortMemoryAnalyzeModel: '', shortMemoryAnalyzeModel: '',
historicalContextAnalyzeModel: '', historicalContextAnalyzeModel: '',

View File

@ -6,6 +6,7 @@ import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar' import Sidebar from './components/app/Sidebar'
import DeepClaudeProvider from './components/DeepClaudeProvider'
import MemoryProvider from './components/MemoryProvider' import MemoryProvider from './components/MemoryProvider'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
@ -31,6 +32,7 @@ function App(): React.ReactElement {
<SyntaxHighlighterProvider> <SyntaxHighlighterProvider>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<MemoryProvider> <MemoryProvider>
<DeepClaudeProvider />
<TopViewContainer> <TopViewContainer>
<HashRouter> <HashRouter>
<NavigationHandler /> <NavigationHandler />

View File

@ -0,0 +1,222 @@
import { addAssistantMemoryItem } from '@renderer/services/MemoryService'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { deleteAssistantMemory } from '@renderer/store/memory'
import { Button, Empty, Input, List, Modal, Tooltip, Typography } from 'antd'
import { DeleteOutlined } from '@ant-design/icons'
import { useCallback, useState } from 'react'
import { Provider } from 'react-redux'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const { Text } = Typography
const StyledModal = styled(Modal)`
.ant-modal-content {
background-color: ${(props) => props.theme.popupBackground};
color: ${(props) => props.theme.textColor};
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.ant-modal-header {
background-color: transparent;
border-bottom: none;
}
.ant-modal-title {
color: ${(props) => props.theme.textColor};
}
.ant-modal-close {
color: ${(props) => props.theme.textColor};
}
.ant-modal-body {
padding: 16px 24px;
}
.ant-modal-footer {
border-top: none;
}
`
const MemoryInput = styled(Input.TextArea)`
margin-bottom: 16px;
border-radius: 6px;
background-color: ${(props) => props.theme.inputBackground};
color: ${(props) => props.theme.textColor};
border-color: ${(props) => props.theme.borderColor};
&:focus,
&:hover {
border-color: ${(props) => props.theme.primaryColor};
}
`
const MemoryList = styled(List)`
max-height: 300px;
overflow-y: auto;
margin-bottom: 16px;
border-radius: 6px;
border: 1px solid ${(props) => props.theme.borderColor};
background-color: ${(props) => props.theme.cardBackground};
.ant-list-item {
padding: 12px 16px;
border-bottom: 1px solid ${(props) => props.theme.borderColor};
}
.ant-list-item:last-child {
border-bottom: none;
}
.ant-list-item-meta-title {
color: ${(props) => props.theme.textColor};
}
.ant-list-item-meta-description {
color: ${(props) => props.theme.secondaryTextColor};
}
`
const EmptyContainer = styled(Empty)`
padding: 24px;
.ant-empty-description {
color: ${(props) => props.theme.secondaryTextColor};
}
`
interface AssistantMemoryPopupProps {
open: boolean
onClose: () => void
assistantId: string
}
const AssistantMemoryPopup = ({ open, onClose, assistantId }: AssistantMemoryPopupProps) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
// 获取助手记忆状态
const assistantMemoryActive = useAppSelector((state) => state.memory?.assistantMemoryActive || false)
const assistantMemories = useAppSelector((state) => {
const allAssistantMemories = state.memory?.assistantMemories || []
// 只显示当前助手的记忆
return assistantId ? allAssistantMemories.filter((memory) => memory.assistantId === assistantId) : []
})
// 添加助手记忆的状态
const [newMemoryContent, setNewMemoryContent] = useState('')
// 添加新的助手记忆
const handleAddMemory = useCallback(() => {
if (newMemoryContent.trim() && assistantId) {
addAssistantMemoryItem(newMemoryContent.trim(), assistantId)
setNewMemoryContent('') // 清空输入框
}
}, [newMemoryContent, assistantId])
// 删除助手记忆
const handleDeleteMemory = useCallback(
(id: string) => {
dispatch(deleteAssistantMemory(id))
},
[dispatch]
)
return (
<StyledModal
title={t('settings.memory.assistantMemory') || '助手记忆'}
open={open}
onCancel={onClose}
footer={null}
width={500}>
<div>
<Text type="secondary">
{t('settings.memory.assistantMemoryDescription') ||
'助手记忆是与特定助手关联的记忆,可以帮助助手记住重要信息。'}
</Text>
<div style={{ marginTop: 16 }}>
<MemoryInput
value={newMemoryContent}
onChange={(e) => setNewMemoryContent(e.target.value)}
placeholder={t('settings.memory.addAssistantMemoryPlaceholder') || '添加助手记忆...'}
autoSize={{ minRows: 2, maxRows: 4 }}
disabled={!assistantMemoryActive}
/>
<Button
type="primary"
onClick={handleAddMemory}
disabled={!assistantMemoryActive || !newMemoryContent.trim()}>
{t('settings.memory.addAssistantMemory') || '添加助手记忆'}
</Button>
</div>
<div style={{ marginTop: 16 }}>
{assistantMemories.length > 0 ? (
<MemoryList
itemLayout="horizontal"
dataSource={assistantMemories}
renderItem={(memory: any) => (
<List.Item
actions={[
<Tooltip title={t('settings.memory.delete') || '删除'} key="delete">
<Button
icon={<DeleteOutlined />}
onClick={() => handleDeleteMemory(memory.id as string)}
type="text"
danger
/>
</Tooltip>
]}>
<List.Item.Meta
title={<div style={{ wordBreak: 'break-word' }}>{memory.content as string}</div>}
description={new Date(memory.createdAt as string).toLocaleString()}
/>
</List.Item>
)}
/>
) : (
<EmptyContainer
description={
!assistantMemoryActive
? t('settings.memory.assistantMemoryDisabled') || '助手记忆功能已禁用'
: t('settings.memory.noAssistantMemories') || '无助手记忆'
}
/>
)}
</div>
</div>
</StyledModal>
)
}
// 静态方法,用于显示弹窗
AssistantMemoryPopup.show = (props: Omit<AssistantMemoryPopupProps, 'open' | 'onClose'>) => {
const div = document.createElement('div')
document.body.appendChild(div)
const close = () => {
Modal.destroyAll()
if (div && div.parentNode) {
div.parentNode.removeChild(div)
}
}
Modal.confirm({
content: (
<Provider store={store}>
<AssistantMemoryPopup open={true} onClose={close} {...props} />
</Provider>
),
icon: null,
footer: null,
width: 500,
closable: true,
centered: true,
maskClosable: true,
className: 'assistant-memory-popup'
})
}
export default AssistantMemoryPopup

View File

@ -0,0 +1,78 @@
import { useEffect } from 'react'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { addProvider, removeProvider } from '@renderer/store/llm'
import { Provider } from '@renderer/types'
import { createAllDeepClaudeProviders, checkModelCombinationsInLocalStorage } from '@renderer/utils/createDeepClaudeProvider'
/**
* DeepClaudeProvider组件
* DeepClaude提供商
*/
const DeepClaudeProvider = () => {
const dispatch = useAppDispatch()
const providers = useAppSelector(state => state.llm.providers)
// 监听localStorage中的modelCombinations变化
useEffect(() => {
// 初始化时加载DeepClaude提供商
loadDeepClaudeProviders()
// 监听localStorage变化
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'modelCombinations') {
loadDeepClaudeProviders()
}
}
// 添加事件监听器
window.addEventListener('storage', handleStorageChange)
// 清理函数
return () => {
window.removeEventListener('storage', handleStorageChange)
}
}, [])
// 加载DeepClaude提供商
const loadDeepClaudeProviders = () => {
console.log('[DeepClaudeProvider] 开始加载DeepClaude提供商')
// 检查localStorage中的模型组合数据
checkModelCombinationsInLocalStorage()
// 移除所有现有的DeepClaude提供商
const existingDeepClaudeProviders = providers.filter(p => p.type === 'deepclaude')
console.log('[DeepClaudeProvider] 移除现有DeepClaude提供商数量:', existingDeepClaudeProviders.length)
existingDeepClaudeProviders.forEach(provider => {
dispatch(removeProvider(provider))
})
// 创建并添加新的DeepClaude提供商
const deepClaudeProviders = createAllDeepClaudeProviders()
console.log('[DeepClaudeProvider] 创建的DeepClaude提供商数量:', deepClaudeProviders.length)
// 列出所有提供商,便于调试
console.log('[DeepClaudeProvider] 当前所有提供商:',
providers.map(p => ({ id: p.id, name: p.name, type: p.type })))
// 添加DeepClaude提供商
deepClaudeProviders.forEach(provider => {
console.log('[DeepClaudeProvider] 添加DeepClaude提供商:', provider.id, provider.name, provider.type,
provider.models.length > 0 ? `包含${provider.models.length}个模型` : '无模型')
dispatch(addProvider(provider))
})
// 再次列出所有提供商,确认添加成功
setTimeout(() => {
const currentProviders = store.getState().llm.providers
console.log('[DeepClaudeProvider] 添加后的所有提供商:',
currentProviders.map((p: Provider) => ({ id: p.id, name: p.name, type: p.type })))
console.log('[DeepClaudeProvider] DeepClaude提供商加载完成')
}, 100)
}
// 这是一个纯逻辑组件,不需要渲染任何内容
return null
}
export default DeepClaudeProvider

View File

@ -0,0 +1,250 @@
import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons'
import { Box } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { addAssistantMemoryItem } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { deleteAssistantMemory } from '@renderer/store/memory'
import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd'
import _ from 'lodash'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { createSelector } from 'reselect'
import styled from 'styled-components'
const ButtonGroup = styled.div`
display: flex;
gap: 8px;
margin-top: 8px;
`
const MemoryContent = styled.div`
word-break: break-word;
`
interface ShowParams {
assistantId: string
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ assistantId, resolve }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const [open, setOpen] = useState(true)
// 创建记忆选择器 - 使用createSelector进行记忆化
const selectAssistantMemoriesByAssistantId = useMemo(
() =>
createSelector(
[(state) => state.memory?.assistantMemories || [], (_state, assistantId) => assistantId],
(assistantMemories, assistantId) => {
return assistantId ? assistantMemories.filter((memory) => memory.assistantId === assistantId) : []
}
),
[]
)
// 获取助手记忆状态
const assistantMemoryActive = useAppSelector((state) => state.memory?.assistantMemoryActive || false)
// 定义助手记忆类型
interface AssistantMemory {
id: string
content: string
assistantId: string
createdAt: string
}
const assistantMemories = useAppSelector((state) => selectAssistantMemoriesByAssistantId(state, assistantId)) as AssistantMemory[]
// 获取分析统计数据
const totalAnalyses = useAppSelector((state) => state.memory?.analysisStats?.totalAnalyses || 0)
const successfulAnalyses = useAppSelector((state) => state.memory?.analysisStats?.successfulAnalyses || 0)
const successRate = totalAnalyses ? (successfulAnalyses / totalAnalyses) * 100 : 0
const avgAnalysisTime = useAppSelector((state) => state.memory?.analysisStats?.averageAnalysisTime || 0)
// 添加助手记忆的状态
const [newMemoryContent, setNewMemoryContent] = useState('')
// 添加新的助手记忆 - 使用防抖减少频繁更新
const handleAddMemory = useCallback(
_.debounce(() => {
if (newMemoryContent.trim() && assistantId) {
addAssistantMemoryItem(newMemoryContent.trim(), assistantId)
setNewMemoryContent('') // 清空输入框
}
}, 300),
[newMemoryContent, assistantId]
)
// 删除助手记忆 - 直接删除无需确认,使用节流避免频繁删除操作
const handleDeleteMemory = useCallback(
_.throttle(async (id: string) => {
// 先从当前状态中获取要删除的记忆之外的所有记忆
const state = store.getState().memory
const filteredAssistantMemories = state.assistantMemories.filter((memory) => memory.id !== id)
// 执行删除操作
dispatch(deleteAssistantMemory(id))
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
try {
// 加载当前文件数据
const currentData = await window.api.memory.loadData()
// 替换 assistantMemories 数组,保留其他重要设置
const newData = {
...currentData,
assistantMemories: filteredAssistantMemories,
assistantMemoryActive: currentData.assistantMemoryActive,
assistantMemoryAnalyzeModel: currentData.assistantMemoryAnalyzeModel
}
// 使用 true 参数强制覆盖文件
const result = await window.api.memory.saveData(newData, true)
if (result) {
console.log(`[AssistantMemoryPopup] Successfully deleted assistant memory with ID ${id}`)
message.success(t('settings.memory.deleteSuccess') || '删除成功')
} else {
console.error(`[AssistantMemoryPopup] Failed to delete assistant memory with ID ${id}`)
message.error(t('settings.memory.deleteError') || '删除失败')
}
} catch (error) {
console.error('[AssistantMemoryPopup] Failed to delete assistant memory:', error)
message.error(t('settings.memory.deleteError') || '删除失败')
}
}, 500),
[dispatch, t]
)
const onClose = () => {
setOpen(false)
}
const afterClose = () => {
resolve({})
}
AssistantMemoryPopup.hide = onClose
return (
<Modal
title={t('settings.memory.assistantMemory') || '助手记忆'}
open={open}
onCancel={onClose}
afterClose={afterClose}
footer={null}
width={500}
centered>
<Box mb={16}>
<Input.TextArea
value={newMemoryContent}
onChange={(e) => setNewMemoryContent(e.target.value)}
placeholder={t('settings.memory.addAssistantMemoryPlaceholder') || '添加助手记忆...'}
autoSize={{ minRows: 2, maxRows: 4 }}
disabled={!assistantMemoryActive || !assistantId}
/>
<ButtonGroup>
<Button
type="primary"
onClick={() => handleAddMemory()}
disabled={!assistantMemoryActive || !newMemoryContent.trim() || !assistantId}>
{t('settings.memory.addAssistantMemory') || '添加助手记忆'}
</Button>
</ButtonGroup>
</Box>
{/* 性能监控统计信息 */}
<Box mb={16}>
<Card
size="small"
title={t('settings.memory.performanceStats') || '系统性能统计'}
extra={<InfoCircleOutlined />}>
<Row gutter={16}>
<Col span={8}>
<Statistic
title={t('settings.memory.totalAnalyses') || '总分析次数'}
value={totalAnalyses}
precision={0}
/>
</Col>
<Col span={8}>
<Statistic
title={t('settings.memory.successRate') || '成功率'}
value={successRate}
precision={1}
suffix="%"
/>
</Col>
<Col span={8}>
<Statistic
title={t('settings.memory.avgAnalysisTime') || '平均分析时间'}
value={avgAnalysisTime}
precision={0}
suffix="ms"
/>
</Col>
</Row>
</Card>
</Box>
<MemoriesList>
{assistantMemories.length > 0 ? (
<List
itemLayout="horizontal"
dataSource={assistantMemories}
renderItem={(memory) => (
<List.Item
actions={[
<Tooltip title={t('settings.memory.delete')} key="delete">
<Button
icon={<DeleteOutlined />}
onClick={() => handleDeleteMemory(memory.id)}
type="text"
danger
/>
</Tooltip>
]}>
<List.Item.Meta
title={<MemoryContent>{memory.content}</MemoryContent>}
description={new Date(memory.createdAt).toLocaleString()}
/>
</List.Item>
)}
/>
) : (
<Empty description={!assistantId ? t('settings.memory.noCurrentAssistant') || '无当前助手' : t('settings.memory.noAssistantMemories') || '无助手记忆'} />
)}
</MemoriesList>
</Modal>
)
}
const MemoriesList = styled.div`
max-height: 300px;
overflow-y: auto;
`
const TopViewKey = 'AssistantMemoryPopup'
export default class AssistantMemoryPopup {
static hide: () => void = () => {}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -0,0 +1,257 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { TopView } from '@renderer/components/TopView'
import {
isEmbeddingModel,
isFunctionCallingModel,
isReasoningModel,
isVisionModel,
isWebSearchModel
} from '@renderer/config/models'
import { useProvider } from '@renderer/hooks/useProvider'
import { Model, ModelType } from '@renderer/types'
import { getDefaultGroupName } from '@renderer/utils'
import { Button, Checkbox, Divider, Flex, Form, Input, message, Modal } from 'antd'
import { CheckboxProps } from 'antd/lib/checkbox'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface ModelEditPopupProps {
model: Model
resolve: (updatedModel?: Model) => void
}
const PopupContainer: FC<ModelEditPopupProps> = ({ model, resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm()
const { t } = useTranslation()
const [showModelTypes, setShowModelTypes] = useState(false)
const { updateModel } = useProvider(model.provider)
const onFinish = (values: any) => {
const updatedModel = {
...model,
id: values.id || model.id,
name: values.name || model.name,
group: values.group || model.group
}
updateModel(updatedModel)
setShowModelTypes(false)
setOpen(false)
resolve(updatedModel)
}
const handleClose = () => {
setShowModelTypes(false)
setOpen(false)
resolve()
}
const onUpdateModel = (updatedModel: Model) => {
updateModel(updatedModel)
// 只更新模型数据,不关闭弹窗,不返回结果
}
return (
<Modal
title={t('models.edit')}
open={open}
onCancel={handleClose}
footer={null}
maskClosable={false}
centered
width={600} // 增加宽度
styles={{
content: {
padding: '20px', // 增加内边距
borderRadius: 15 // 增加圆角
}
}}
afterOpenChange={(visible) => {
if (visible) {
form.getFieldInstance('id')?.focus()
} else {
setShowModelTypes(false)
}
}}>
<Form
form={form}
labelCol={{ flex: '120px' }} // 增加标签宽度
labelAlign="left"
colon={false}
style={{ marginTop: 15 }}
size="large" // 使表单控件更大
initialValues={{
id: model.id,
name: model.name,
group: model.group
}}
onFinish={onFinish}>
<Form.Item
name="id"
label={t('settings.models.add.model_id')}
tooltip={t('settings.models.add.model_id.tooltip')}
rules={[{ required: true }]}>
<Flex justify="space-between" gap={5}>
<Input
placeholder={t('settings.models.add.model_id.placeholder')}
spellCheck={false}
maxLength={200}
disabled={true}
value={model.id}
onChange={(e) => {
const value = e.target.value
form.setFieldValue('name', value)
form.setFieldValue('group', getDefaultGroupName(value))
}}
/>
<Button
type="text"
icon={<CopyIcon />}
onClick={() => {
navigator.clipboard.writeText(model.id)
message.success(t('message.copy.success'))
}}
/>
</Flex>
</Form.Item>
<Form.Item
name="name"
label={t('settings.models.add.model_name')}
tooltip={t('settings.models.add.model_name.tooltip')}
rules={[{ required: true }]}>
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} maxLength={200} />
</Form.Item>
<Form.Item
name="group"
label={t('settings.models.add.model_group')}
tooltip={t('settings.models.add.model_group.tooltip')}>
<Input placeholder={t('settings.models.add.model_group.placeholder')} spellCheck={false} maxLength={200} />
</Form.Item>
<Form.Item style={{ marginBottom: 20, textAlign: 'center', marginTop: 10 }}>
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
<MoreSettingsRow onClick={() => setShowModelTypes(!showModelTypes)}>
{t('settings.moresetting')}
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
</MoreSettingsRow>
<Button type="primary" htmlType="submit" size="large">
{t('common.save')}
</Button>
</Flex>
</Form.Item>
{showModelTypes && (
<div>
<Divider style={{ margin: '0 0 15px 0' }} />
<TypeTitle>{t('models.type.select')}:</TypeTitle>
{(() => {
const defaultTypes = [
...(isVisionModel(model) ? ['vision'] : []),
...(isEmbeddingModel(model) ? ['embedding'] : []),
...(isReasoningModel(model) ? ['reasoning'] : []),
...(isFunctionCallingModel(model) ? ['function_calling'] : []),
...(isWebSearchModel(model) ? ['web_search'] : [])
] as ModelType[]
// 合并现有选择和默认类型
const selectedTypes = [...new Set([...(model.type || []), ...defaultTypes])]
const showTypeConfirmModal = (type: string) => {
window.modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => {
const updatedModel = { ...model, type: [...selectedTypes, type] as ModelType[] }
onUpdateModel(updatedModel)
},
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
const newType = types.find((type) => !selectedTypes.includes(type as ModelType))
if (newType) {
// 如果有新类型被添加,显示确认对话框
showTypeConfirmModal(newType)
} else {
// 如果没有新类型,只是取消选择了某些类型,直接更新
const updatedModel = { ...model, type: types as ModelType[] }
onUpdateModel(updatedModel)
}
}
return (
<Checkbox.Group
value={selectedTypes}
onChange={handleTypeChange}
style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
<StyledCheckbox value="vision">{t('models.type.vision')}</StyledCheckbox>
<StyledCheckbox value="web_search">{t('models.type.websearch')}</StyledCheckbox>
<StyledCheckbox value="reasoning">{t('models.type.reasoning')}</StyledCheckbox>
<StyledCheckbox value="function_calling">{t('models.type.function_calling')}</StyledCheckbox>
<StyledCheckbox value="embedding">{t('models.type.embedding')}</StyledCheckbox>
<StyledCheckbox value="rerank">{t('models.type.rerank')}</StyledCheckbox>
</Checkbox.Group>
)
})()}
</div>
)}
</Form>
</Modal>
)
}
const MoreSettingsRow = styled.div`
cursor: pointer;
display: flex;
align-items: center;
gap: 8px; // 增加间距
color: var(--color-text-secondary);
font-size: 14px; // 增加字体大小
&:hover {
color: var(--color-text-primary);
}
`
const ExpandIcon = styled.span`
font-size: 12px; // 增加图标大小
display: flex;
align-items: center;
`
const TypeTitle = styled.div`
font-size: 16px; // 增加字体大小
margin-bottom: 15px; // 增加下边距
font-weight: 500;
`
const StyledCheckbox = styled(Checkbox)<CheckboxProps>`
font-size: 14px; // 增加字体大小
padding: 5px 0; // 增加内边距
.ant-checkbox-inner {
width: 18px; // 增加复选框大小
height: 18px; // 增加复选框大小
}
.ant-checkbox + span {
padding-left: 12px; // 增加文字与复选框的间距
}
`
export default class ModelEditPopup {
static hide() {
TopView.hide('ModelEditPopup')
}
static show(model: Model) {
return new Promise<Model | undefined>((resolve) => {
TopView.show(<PopupContainer model={model} resolve={resolve} />, 'ModelEditPopup')
})
}
}

View File

@ -0,0 +1,64 @@
import { SettingOutlined } from '@ant-design/icons'
import { useProvider } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types'
import { Button, Tooltip } from 'antd'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ModelEditPopup from './ModelEditPopup'
interface ModelSettingsButtonProps {
model: Model
size?: number
className?: string
}
const ModelSettingsButton: FC<ModelSettingsButtonProps> = ({ model, size = 16, className }) => {
const { t } = useTranslation()
const { updateModel } = useProvider(model.provider)
const handleClick = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation() // 防止触发父元素的点击事件
const updatedModel = await ModelEditPopup.show(model)
if (updatedModel) {
updateModel(updatedModel)
}
},
[model, updateModel]
)
return (
<Tooltip title={t('models.edit')} placement="top">
<StyledButton
type="text"
icon={<SettingOutlined style={{ fontSize: size }} />}
onClick={handleClick}
className={className}
/>
</Tooltip>
)
}
const StyledButton = styled(Button)`
display: flex;
align-items: center;
justify-content: center;
padding: 6px; // 增加内边距
margin: 0;
height: auto;
width: auto;
min-width: auto;
background: transparent;
border: none;
opacity: 0.5;
transition: opacity 0.2s;
&:hover {
opacity: 1;
background: transparent;
}
`
export default ModelSettingsButton

View File

@ -1,549 +1,485 @@
import { PushpinOutlined } from '@ant-design/icons' import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models' import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types' // Removed unused 'Provider' import
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd' import { Avatar, Divider, Empty, Input, InputRef, Modal, Tooltip } from 'antd'
import { first, sortBy } from 'lodash' import { first, sortBy } from 'lodash'
import { Search } from 'lucide-react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' // Added useMemo here
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import ModelTagsWithLabel from '../ModelTagsWithLabel' import ModelTags from '../ModelTags'
import Scrollbar from '../Scrollbar' import Scrollbar from '../Scrollbar'
import ModelSettingsButton from './ModelSettingsButton'
type MenuItem = Required<MenuProps>['items'][number]
interface Props { interface Props {
model?: Model model?: Model // The currently active model, for highlighting
} }
interface PopupContainerProps extends Props { interface PopupContainerProps extends Props {
resolve: (value: Model | undefined) => void resolve: (value: Model | undefined) => void
} }
const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => { const PINNED_PROVIDER_ID = '__pinned__' // Special ID for pinned section
const PopupContainer: React.FC<PopupContainerProps> = ({ model: activeModel, resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(null) const inputRef = useRef<InputRef>(null)
const { providers } = useProviders() const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([]) const [pinnedModels, setPinnedModels] = useState<string[]>([])
const scrollContainerRef = useRef<HTMLDivElement>(null) const [selectedProviderId, setSelectedProviderId] = useState<string>('all')
const [keyboardSelectedId, setKeyboardSelectedId] = useState<string>('') // 移除未使用的状态
const menuItemRefs = useRef<Record<string, HTMLElement | null>>({})
const setMenuItemRef = useCallback(
(key: string) => (el: HTMLElement | null) => {
if (el) {
menuItemRefs.current[key] = el
}
},
[]
)
// --- Load Pinned Models ---
useEffect(() => { useEffect(() => {
const loadPinnedModels = async () => { const loadPinnedModels = async () => {
const setting = await db.settings.get('pinned:models') const setting = await db.settings.get('pinned:models')
const savedPinnedModels = setting?.value || [] const savedPinnedModels = setting?.value || []
// Filter out invalid pinned models
const allModelIds = providers.flatMap((p) => p.models || []).map((m) => getModelUniqId(m)) const allModelIds = providers.flatMap((p) => p.models || []).map((m) => getModelUniqId(m))
const validPinnedModels = savedPinnedModels.filter((id) => allModelIds.includes(id)) const validPinnedModels = savedPinnedModels.filter((id: string) => allModelIds.includes(id))
// Update storage if there were invalid models
if (validPinnedModels.length !== savedPinnedModels.length) { if (validPinnedModels.length !== savedPinnedModels.length) {
await db.settings.put({ id: 'pinned:models', value: validPinnedModels }) await db.settings.put({ id: 'pinned:models', value: validPinnedModels })
} }
setPinnedModels(sortBy(validPinnedModels)) // Keep pinned models sorted if needed
setPinnedModels(sortBy(validPinnedModels, ['group', 'name'])) // Set initial selected provider
if (activeModel) {
const activeModelId = getModelUniqId(activeModel)
if (validPinnedModels.includes(activeModelId)) {
setSelectedProviderId(PINNED_PROVIDER_ID)
} else {
setSelectedProviderId(activeModel.provider)
}
} else if (validPinnedModels.length > 0) {
setSelectedProviderId(PINNED_PROVIDER_ID)
} else if (providers.length > 0) {
setSelectedProviderId(providers[0].id)
}
} }
loadPinnedModels() loadPinnedModels()
}, [providers]) }, [providers, activeModel]) // Depend on providers and activeModel
const togglePin = async (modelId: string) => { // --- Pin/Unpin Logic ---
const togglePin = useCallback(
async (modelId: string) => {
const newPinnedModels = pinnedModels.includes(modelId) const newPinnedModels = pinnedModels.includes(modelId)
? pinnedModels.filter((id) => id !== modelId) ? pinnedModels.filter((id) => id !== modelId)
: [...pinnedModels, modelId] : [...pinnedModels, modelId]
await db.settings.put({ id: 'pinned:models', value: newPinnedModels }) await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
setPinnedModels(sortBy(newPinnedModels, ['group', 'name'])) setPinnedModels(sortBy(newPinnedModels)) // Keep sorted
// If unpinning the last pinned model and currently viewing pinned, switch provider
if (newPinnedModels.length === 0 && selectedProviderId === PINNED_PROVIDER_ID) {
setSelectedProviderId(providers[0]?.id || 'all')
} }
// If pinning a model while viewing its provider, maybe switch to pinned? (Optional UX decision)
// else if (!pinnedModels.includes(modelId) && selectedProviderId !== PINNED_PROVIDER_ID) {
// setSelectedProviderId(PINNED_PROVIDER_ID);
// }
},
[pinnedModels, selectedProviderId, providers]
)
// 根据输入的文本筛选模型 // 缓存所有模型列表只在providers变化时重新计算
const getFilteredModels = useCallback( const allModels = useMemo(() => {
(provider) => { return providers.flatMap((p) => p.models || [])
let models = provider.models.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m)) .filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
}, [providers])
// --- Filter Models for Right Column ---
const displayedModels = useMemo(() => {
let modelsToShow: Model[] = []
// 如果有搜索文本,在所有模型中搜索
if (searchText.trim()) { if (searchText.trim()) {
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
models = models.filter((m) => { modelsToShow = allModels.filter((m) => {
const fullName = provider.isSystem const provider = providers.find((p) => p.id === m.provider)
? `${m.name} ${provider.name} ${t('provider.' + provider.id)}` const providerName = provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
: `${m.name} ${provider.name}` const fullName = `${m.name} ${providerName}`.toLowerCase()
return keywords.every((keyword) => fullName.includes(keyword))
const lowerFullName = fullName.toLowerCase()
return keywords.every((keyword) => lowerFullName.includes(keyword))
}) })
} else { } else {
// 如果不是搜索状态,过滤掉已固定的模型 // 没有搜索文本时,根据选择的供应商筛选
models = models.filter((m) => !pinnedModels.includes(getModelUniqId(m))) if (selectedProviderId === 'all') {
// 显示所有模型
modelsToShow = allModels
} else if (selectedProviderId === PINNED_PROVIDER_ID) {
// 显示固定的模型
modelsToShow = allModels.filter((m) => pinnedModels.includes(getModelUniqId(m)))
} else if (selectedProviderId) {
// 显示选中供应商的模型
const provider = providers.find((p) => p.id === selectedProviderId)
if (provider && provider.models) {
modelsToShow = provider.models.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
} }
return sortBy(models, ['group', 'name'])
},
[searchText, t, pinnedModels]
)
// 递归处理菜单项为每个项添加ref
const processMenuItems = useCallback(
(items: MenuItem[]) => {
// 内部定义 renderMenuItem 函数
const renderMenuItem = (item: any) => {
return {
...item,
label: <div ref={setMenuItemRef(item.key)}>{item.label}</div>
} }
} }
return items.map((item) => { return sortBy(modelsToShow, ['group', 'name'])
if (item && 'children' in item && item.children) { }, [selectedProviderId, pinnedModels, searchText, allModels, providers, t])
return {
...item,
children: (item.children as MenuItem[]).map(renderMenuItem)
}
}
return item
})
},
[setMenuItemRef]
)
const filteredItems: MenuItem[] = providers // --- Event Handlers ---
.filter((p) => p.models && p.models.length > 0) const handleProviderSelect = useCallback((providerId: string) => {
.map((p) => { setSelectedProviderId(providerId)
const filteredModels = getFilteredModels(p).map((m) => ({ }, [])
key: getModelUniqId(m),
label: ( const handleModelSelect = useCallback((model: Model) => {
<ModelItem> resolve(model)
<ModelNameRow>
<span>{m?.name}</span> <ModelTagsWithLabel model={m} size={11} showLabel={false} />
</ModelNameRow>
<PinIcon
onClick={(e) => {
e.stopPropagation()
togglePin(getModelUniqId(m))
}}
isPinned={pinnedModels.includes(getModelUniqId(m))}>
<PushpinOutlined />
</PinIcon>
</ModelItem>
),
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => {
resolve(m)
setOpen(false) setOpen(false)
} }, [resolve, setOpen])
}))
// Only return the group if it has filtered models const onCancel = useCallback(() => {
return filteredModels.length > 0
? {
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: filteredModels
}
: null
})
.filter(Boolean) as MenuItem[] // Filter out null items
if (pinnedModels.length > 0 && searchText.length === 0) {
const pinnedItems = providers
.flatMap((p) =>
p.models
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
.map((m) => ({
key: getModelUniqId(m),
model: m,
provider: p
}))
)
.map((m) => ({
key: getModelUniqId(m.model) + '_pinned',
label: (
<ModelItem>
<ModelNameRow>
<span>
{m.model?.name} | {m.provider.isSystem ? t(`provider.${m.provider.id}`) : m.provider.name}
</span>{' '}
<ModelTagsWithLabel model={m.model} size={11} showLabel={false} />
</ModelNameRow>
<PinIcon
onClick={(e) => {
e.stopPropagation()
togglePin(getModelUniqId(m.model))
}}
isPinned={true}>
<PushpinOutlined />
</PinIcon>
</ModelItem>
),
icon: (
<Avatar src={getModelLogo(m.model?.id || '')} size={24}>
{first(m.model?.name)}
</Avatar>
),
onClick: () => {
resolve(m.model)
setOpen(false) setOpen(false)
} }, [])
}))
if (pinnedItems.length > 0) { const onClose = useCallback(async () => {
filteredItems.unshift({
key: 'pinned',
label: t('models.pinned'),
type: 'group',
children: pinnedItems
} as MenuItem)
}
}
// 处理菜单项添加ref
const processedItems = processMenuItems(filteredItems)
const onCancel = () => {
setKeyboardSelectedId('')
setOpen(false)
}
const onClose = async () => {
setKeyboardSelectedId('')
resolve(undefined) resolve(undefined)
SelectModelPopup.hide() SelectModelPopup.hide()
} }, [resolve])
// --- Focus Input on Open ---
useEffect(() => { useEffect(() => {
open && setTimeout(() => inputRef.current?.focus(), 0) open && setTimeout(() => inputRef.current?.focus(), 0)
}, [open]) }, [open])
useEffect(() => { // --- Provider List for Left Column ---
if (open && model) { const providerListItems = useMemo(() => {
setTimeout(() => { const items: { id: string; name: string }[] = [
const modelId = getModelUniqId(model) { id: 'all', name: t('models.all') || '全部' } // 添加“全部”选项
if (menuItemRefs.current[modelId]) { ]
menuItemRefs.current[modelId]?.scrollIntoView({ block: 'center', behavior: 'auto' }) if (pinnedModels.length > 0) {
items.push({ id: PINNED_PROVIDER_ID, name: t('models.pinned') })
} }
}, 100) // Small delay to ensure menu is rendered
}
}, [open, model])
// 获取所有可见的模型项
const getVisibleModelItems = useCallback(() => {
const items: { key: string; model: Model }[] = []
// 如果有置顶模型且没有搜索文本,添加置顶模型
if (pinnedModels.length > 0 && searchText.length === 0) {
providers
.flatMap((p) => p.models || [])
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
.forEach((m) => items.push({ key: getModelUniqId(m) + '_pinned', model: m }))
}
// 添加其他过滤后的模型
providers.forEach((p) => { providers.forEach((p) => {
if (p.models) { // Only add provider if it has non-embedding/rerank models
getFilteredModels(p).forEach((m) => { if (p.models?.some((m) => !isEmbeddingModel(m) && !isRerankModel(m))) {
const modelId = getModelUniqId(m) items.push({ id: p.id, name: p.isSystem ? t(`provider.${p.id}`) : p.name })
const isPinned = pinnedModels.includes(modelId)
// 搜索状态下,所有匹配的模型都应该可以被选中,包括固定的模型
// 非搜索状态下,只添加非固定模型(固定模型已在上面添加)
if (searchText.length > 0 || !isPinned) {
items.push({
key: modelId,
model: m
})
} }
}) })
}
})
return items return items
}, [pinnedModels, searchText, providers, getFilteredModels]) }, [providers, pinnedModels, t])
// 添加一个useLayoutEffect来处理滚动
useLayoutEffect(() => {
if (open && keyboardSelectedId && menuItemRefs.current[keyboardSelectedId]) {
// 获取当前选中元素和容器
const selectedElement = menuItemRefs.current[keyboardSelectedId]
const scrollContainer = scrollContainerRef.current
if (!scrollContainer) return
const selectedRect = selectedElement.getBoundingClientRect()
const containerRect = scrollContainer.getBoundingClientRect()
// 计算元素相对于容器的位置
const currentScrollTop = scrollContainer.scrollTop
const elementTop = selectedRect.top - containerRect.top + currentScrollTop
const groupTitleHeight = 30
// 确定滚动位置
if (selectedRect.top < containerRect.top + groupTitleHeight) {
// 元素被组标题遮挡,向上滚动
scrollContainer.scrollTo({
top: elementTop - groupTitleHeight,
behavior: 'smooth'
})
} else if (selectedRect.bottom > containerRect.bottom) {
// 元素在视口下方,向下滚动
scrollContainer.scrollTo({
top: elementTop - containerRect.height + selectedRect.height,
behavior: 'smooth'
})
}
}
}, [open, keyboardSelectedId])
// 处理键盘导航
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const items = getVisibleModelItems()
if (items.length === 0) return
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
const currentIndex = items.findIndex((item) => item.key === keyboardSelectedId)
let nextIndex
if (currentIndex === -1) {
nextIndex = e.key === 'ArrowDown' ? 0 : items.length - 1
} else {
nextIndex =
e.key === 'ArrowDown' ? (currentIndex + 1) % items.length : (currentIndex - 1 + items.length) % items.length
}
const nextItem = items[nextIndex]
setKeyboardSelectedId(nextItem.key)
} else if (e.key === 'Enter') {
e.preventDefault() // 阻止回车的默认行为
if (keyboardSelectedId) {
const selectedItem = items.find((item) => item.key === keyboardSelectedId)
if (selectedItem) {
resolve(selectedItem.model)
setOpen(false)
}
}
}
},
[keyboardSelectedId, getVisibleModelItems, resolve, setOpen]
)
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
// 搜索文本改变时重置键盘选中状态
useEffect(() => {
setKeyboardSelectedId('')
}, [searchText])
const selectedKeys = keyboardSelectedId ? [keyboardSelectedId] : model ? [getModelUniqId(model)] : []
// --- Render ---
return ( return (
<Modal <Modal
centered centered
open={open} open={open}
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
width={600} transitionName=""
transitionName="ant-move-down"
styles={{ styles={{
content: { content: {
borderRadius: 20, borderRadius: 15, // Adjusted border radius
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
paddingBottom: 20,
border: '1px solid var(--color-border)' border: '1px solid var(--color-border)'
},
body: {
padding: 0 // Remove default body padding
} }
}} }}
closeIcon={null} closeIcon={null}
footer={null}> footer={null}
<HStack style={{ padding: '0 12px', marginTop: 5 }}> width={900} // 进一步增加宽度,使界面更宽敞
>
{/* Search Input */}
<SearchContainer onClick={() => inputRef.current?.focus()}>
<SearchInputContainer>
<Input <Input
prefix={ prefix={
<SearchIcon> <SearchIcon>
<Search size={15} /> <SearchOutlined />
</SearchIcon> </SearchIcon>
} }
ref={inputRef} ref={inputRef}
placeholder={t('models.search')} placeholder={t('models.search')}
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setSearchText(value)
// 当搜索时,自动选择"all"供应商,以显示所有匹配的模型
if (value.trim() && selectedProviderId !== 'all') {
setSelectedProviderId('all')
}
}, [selectedProviderId, t])}
// 移除焦点事件处理
allowClear allowClear
autoFocus autoFocus
style={{ paddingLeft: 0 }} style={{
paddingLeft: 0,
height: '32px',
fontSize: '14px'
}}
variant="borderless" variant="borderless"
size="middle" size="middle"
onKeyDown={(e) => {
// 防止上下键移动光标
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault()
}
}}
/>
</HStack>
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
<Container>
{processedItems.length > 0 ? (
<StyledMenu
items={processedItems}
selectedKeys={selectedKeys}
mode="inline"
inlineIndent={6}
onSelect={({ key }) => {
setKeyboardSelectedId(key as string)
}}
/> />
</SearchInputContainer>
</SearchContainer>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5, marginTop: -5 }} />
{/* Two Column Layout */}
<TwoColumnContainer>
{/* Left Column: Providers */}
<ProviderListColumn>
<Scrollbar style={{ height: '60vh', paddingRight: '5px' }}>
{providerListItems.map((provider, index) => (
<React.Fragment key={provider.id}>
<Tooltip title={provider.name} placement="right" mouseEnterDelay={0.5}>
<ProviderListItem
$selected={selectedProviderId === provider.id}
onClick={() => handleProviderSelect(provider.id)}>
<ProviderName>{provider.name}</ProviderName>
{provider.id === PINNED_PROVIDER_ID && <PinnedIcon />}
</ProviderListItem>
</Tooltip>
{/* 在每个供应商之后添加分割线,除了最后一个 */}
{index < providerListItems.length - 1 && <ProviderDivider />}
</React.Fragment>
))}
</Scrollbar>
</ProviderListColumn>
{/* Right Column: Models */}
<ModelListColumn>
<Scrollbar style={{ height: '60vh', paddingRight: '5px' }}>
{displayedModels.length > 0 ? (
displayedModels.map((m) => (
<ModelListItem
key={getModelUniqId(m)}
$selected={activeModel ? getModelUniqId(activeModel) === getModelUniqId(m) : false}
onClick={() => handleModelSelect(m)}>
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
<ModelDetails>
<ModelNameRow>
<Tooltip title={m?.name} mouseEnterDelay={0.5}>
<span className="model-name">{m?.name}</span>
</Tooltip>
{/* Show provider only if not in pinned view or if search is active */}
{(selectedProviderId !== PINNED_PROVIDER_ID || searchText) && (
<Tooltip title={providers.find((p) => p.id === m.provider)?.name ?? m.provider} mouseEnterDelay={0.5}>
<span className="provider-name">
| {providers.find((p) => p.id === m.provider)?.name ?? m.provider}
</span>
</Tooltip>
)}
<ModelTags model={m} />
</ModelNameRow>
</ModelDetails>
<ActionButtons>
<ModelSettingsButton model={m} size={14} className="settings-button" />
<PinButton
$isPinned={pinnedModels.includes(getModelUniqId(m))}
onClick={(e) => {
e.stopPropagation() // Prevent model selection when clicking pin
togglePin(getModelUniqId(m))
}}>
<PushpinOutlined />
</PinButton>
</ActionButtons>
</ModelListItem>
))
) : ( ) : (
<EmptyState> <EmptyState>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('models.no_matches')} />
</EmptyState> </EmptyState>
)} )}
</Container>
</Scrollbar> </Scrollbar>
</ModelListColumn>
</TwoColumnContainer>
</Modal> </Modal>
) )
} }
const Container = styled.div` // --- Styled Components ---
margin-top: 10px;
const SearchContainer = styled(HStack)`
padding: 8px 15px;
cursor: pointer;
` `
const StyledMenu = styled(Menu)` const SearchInputContainer = styled.div`
background-color: transparent; width: 100%;
padding: 5px;
margin-top: -10px;
max-height: calc(60vh - 50px);
.ant-menu-item-group-title {
position: sticky;
top: 0;
z-index: 1;
margin: 0 -5px;
padding: 5px 10px;
padding-left: 18px;
font-size: 12px;
font-weight: 500;
/* Scroll-driven animation for sticky header */
animation: background-change linear both;
animation-timeline: scroll();
animation-range: entry 0% entry 1%;
}
/* Simple animation that changes background color when sticky */
@keyframes background-change {
to {
background-color: var(--color-background);
}
}
.ant-menu-item {
height: 36px;
line-height: 36px;
&.ant-menu-item-selected {
background-color: var(--color-background-mute) !important;
color: var(--color-text-primary) !important;
}
&:not([data-menu-id^='pinned-']) {
.pin-icon {
opacity: 0;
}
&:hover {
.pin-icon {
opacity: 0.3;
}
}
}
.anticon {
min-width: auto;
}
}
`
const ModelItem = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
`
const SearchIcon = styled.div`
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
margin-right: 5px;
color: var(--color-icon);
font-size: 14px; font-size: 14px;
position: relative; flex-shrink: 0;
width: 100%;
&:hover {
background-color: var(--color-background-mute);
}
`
const TwoColumnContainer = styled.div`
display: flex;
height: 60vh; // 增加高度
`
const ProviderListColumn = styled.div`
width: 200px; // 减小宽度到200px
border-right: 0.5px solid var(--color-border);
padding: 15px 10px; // 减小内边距
box-sizing: border-box;
background-color: var(--color-background-soft); // Slight background difference
`
const ProviderListItem = styled.div<{ $selected: boolean }>`
padding: 10px 12px; // 增加上下内边距
cursor: pointer;
border-radius: 8px; // 减小圆角
margin-bottom: 8px; // 增加下边距
font-size: 14px; // 减小字体大小
font-weight: ${(props) => (props.$selected ? '600' : '400')};
background-color: ${(props) => (props.$selected ? 'var(--color-background-mute)' : 'transparent')};
color: ${(props) => (props.$selected ? 'var(--color-text-primary)' : 'var(--color-text)')};
display: flex;
align-items: center;
justify-content: space-between; // To push pin icon to the right for "Pinned"
overflow: hidden; // 防止文本溢出
text-overflow: ellipsis; // 溢出显示省略号
white-space: nowrap; // 不换行
&:hover {
background-color: var(--color-background-mute);
}
`
const ModelListColumn = styled.div`
flex: 1;
padding: 12px; // 减小内边距
box-sizing: border-box;
`
const ModelListItem = styled.div<{ $selected: boolean }>`
display: flex;
align-items: center;
padding: 8px 12px; // 进一步减小内边距
margin-bottom: 6px; // 进一步减小下边距
border-radius: 6px; // 进一步减小圆角
cursor: pointer;
background-color: ${(props) => (props.$selected ? 'var(--color-background-mute)' : 'transparent')};
&:hover {
background-color: var(--color-background-mute);
.pin-button, .settings-button {
opacity: 0.5; // Show buttons on hover
}
}
.pin-button, .settings-button {
opacity: ${(props) => (props.$selected ? 0.5 : 0)}; // Show if selected or hovered
transition: opacity 0.2s;
&:hover {
opacity: 1 !important; // Full opacity on direct hover
}
}
`
const ModelDetails = styled.div`
margin-left: 10px; // 进一步减小左边距
flex: 1;
overflow: hidden; // Prevent long names from breaking layout
` `
const ModelNameRow = styled.div` const ModelNameRow = styled.div`
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 6px; // 进一步减小间距
font-size: 13px; // 进一步减小字体大小
.model-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px; // 进一步减小最大宽度
}
.provider-name {
color: var(--color-text-secondary);
font-size: 11px; // 进一步减小字体大小
white-space: nowrap;
overflow: hidden; // 防止文本溢出
text-overflow: ellipsis; // 溢出显示省略号
max-width: 120px; // 增加最大宽度
}
`
const ActionButtons = styled.div`
display: flex;
align-items: center;
gap: 4px; // 进一步减小间距
margin-left: auto; // Push to the right
`
const PinButton = styled.button<{ $isPinned: boolean }>`
background: none;
border: none;
cursor: pointer;
padding: 4px; // 进一步减小内边距
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'var(--color-icon)')};
transform: ${(props) => (props.$isPinned ? 'rotate(-45deg)' : 'none')};
font-size: 14px; // 进一步减小图标大小
line-height: 1; // Ensure icon aligns well
&:hover {
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'var(--color-text-primary)')};
}
` `
const EmptyState = styled.div` const EmptyState = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 200px; height: 100%;
color: var(--color-text-secondary);
` `
const SearchIcon = styled.div` const ProviderName = styled.span`
width: 32px; overflow: hidden;
height: 32px; text-overflow: ellipsis;
border-radius: 50%; flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
margin-right: 2px;
` `
const PinIcon = styled.span.attrs({ className: 'pin-icon' })<{ isPinned: boolean }>` const PinnedIcon = styled(PushpinOutlined)`
margin-left: auto; margin-left: auto;
padding: 0 8px; flex-shrink: 0;
opacity: ${(props) => (props.isPinned ? 1 : 'inherit')};
transition: opacity 0.2s;
position: absolute;
right: 0;
color: ${(props) => (props.isPinned ? 'var(--color-primary)' : 'inherit')};
transform: ${(props) => (props.isPinned ? 'rotate(-45deg)' : 'none')};
&:hover {
opacity: 1 !important;
color: ${(props) => (props.isPinned ? 'var(--color-primary)' : 'inherit')};
}
` `
const ProviderDivider = styled.div`
height: 1px;
background-color: var(--color-border);
margin: 8px 0;
opacity: 0.5;
`
// --- Export Class ---
export default class SelectModelPopup { export default class SelectModelPopup {
static hide() { static hide() {
TopView.hide('SelectModelPopup') TopView.hide('SelectModelPopup')
} }
static show(params: Props) { static show(params: Props) {
return new Promise<Model | undefined>((resolve) => { return new Promise<Model | undefined>((resolve) => {
// 直接显示新的弹窗不使用setTimeout
TopView.show(<PopupContainer {...params} resolve={resolve} />, 'SelectModelPopup') TopView.show(<PopupContainer {...params} resolve={resolve} />, 'SelectModelPopup')
}) })
} }

View File

@ -51,7 +51,16 @@ const PopupContainer: React.FC<Props> = ({ topicId, resolve }) => {
// 获取短记忆状态 // 获取短记忆状态
const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false) const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false)
const shortMemories = useAppSelector((state) => selectShortMemoriesByTopicId(state, topicId))
// 定义短记忆类型
interface ShortMemory {
id: string
content: string
topicId: string
createdAt: string
}
const shortMemories = useAppSelector((state) => selectShortMemoriesByTopicId(state, topicId)) as ShortMemory[]
// 获取分析统计数据 // 获取分析统计数据
const totalAnalyses = useAppSelector((state) => state.memory?.analysisStats?.totalAnalyses || 0) const totalAnalyses = useAppSelector((state) => state.memory?.analysisStats?.totalAnalyses || 0)

View File

@ -71,7 +71,10 @@ export function useAssistant(id: string) {
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })), updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })), removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
setModel: useCallback( setModel: useCallback(
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })), (model: Model) => {
console.log('[useAssistant] 设置模型:', model.id, model.name, model.provider)
assistant && dispatch(setModel({ assistantId: assistant?.id, model }))
},
[assistant, dispatch] [assistant, dispatch]
), ),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)), updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),

View File

@ -55,47 +55,85 @@ export async function getTopicById(topicId: string) {
return { ...topic, messages } as Topic return { ...topic, messages } as Topic
} }
// 优化自动重命名功能减少API调用和性能影响
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => { export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
// 如果该主题正在重命名中,直接返回
if (renamingTopics.has(topicId)) { if (renamingTopics.has(topicId)) {
return return
} }
// Declare variables outside the try block to make them accessible in finally
let enableTopicNaming: boolean | undefined
let topic: Topic | undefined
let messages: any[] = [] // Assuming messages is an array, adjust type if needed
try { try {
renamingTopics.add(topicId) renamingTopics.add(topicId)
const topic = await getTopicById(topicId) // 获取主题设置并确保其为布尔值
const enableTopicNaming = getStoreSetting('enableTopicNaming') enableTopicNaming = getStoreSetting('enableTopicNaming') === true
if (isEmpty(topic.messages)) { // 从当前状态中获取主题,避免数据库访问
const state = store.getState()
const topics = state.assistants.assistants.map((a) => a.topics).flat()
topic = topics.find((t) => t.id === topicId)
// 如果主题不存在或已手动编辑名称,直接返回
if (!topic || topic.isNameManuallyEdited) {
return return
} }
if (topic.isNameManuallyEdited) { // 获取消息
messages = state.messages.messagesByTopic[topicId] || []
if (isEmpty(messages)) {
return return
} }
// 如果不启用自动命名使用第一条消息的前50个字符作为主题名称
if (!enableTopicNaming) { if (!enableTopicNaming) {
const topicName = topic.messages[0]?.content.substring(0, 50) const topicName = messages[0]?.content?.substring(0, 50)
if (topicName) { // Ensure topic is defined before using it
const data = { ...topic, name: topicName } as Topic if (topicName && topic) {
const data = { ...topic, name: topicName }
// Check if _setActiveTopic exists and is a function before calling
if (typeof _setActiveTopic === 'function') {
_setActiveTopic(data) _setActiveTopic(data)
}
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} }
return return
} }
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) { // 只有当主题名称是默认名称且消息数量足够时才调用API生成摘要
if (topic && topic.name === i18n.t('chat.default.topic.name') && messages.length >= 2) {
// 延迟加载摘要API减少切换会话时的卡顿
setTimeout(async () => {
try {
const { fetchMessagesSummary } = await import('@renderer/services/ApiService') const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant }) const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) { // Ensure topic is defined before using it
if (summaryText && topic) {
const data = { ...topic, name: summaryText } const data = { ...topic, name: summaryText }
// Check if _setActiveTopic exists and is a function before calling
if (typeof _setActiveTopic === 'function') {
_setActiveTopic(data) _setActiveTopic(data)
}
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} }
} } catch (error) {
// 静默处理错误,不影响用户体验
} finally { } finally {
renamingTopics.delete(topicId) renamingTopics.delete(topicId)
} }
}, 1000) // 延迟1秒执行避免切换会话时的卡顿
return
}
} finally {
// 如果没有进入延迟执行的分支,则在这里清除标记
if (!enableTopicNaming || topic?.name !== i18n.t('chat.default.topic.name') || messages.length < 2) {
renamingTopics.delete(topicId)
}
}
} }
// Convert class to object with functions since class only has static methods // Convert class to object with functions since class only has static methods

View File

@ -1192,6 +1192,15 @@
"allCategories": "All", "allCategories": "All",
"uncategorized": "Uncategorized", "uncategorized": "Uncategorized",
"shortMemory": "Short-term Memory", "shortMemory": "Short-term Memory",
"assistantMemory": "Assistant Memory",
"assistantMemorySettings": "Assistant Memory Settings",
"assistantMemoryDescription": "Assistant memory is associated with specific assistants and helps them remember important information.",
"assistantMemoryAnalyzeModel": "Assistant Memory Analysis Model",
"addAssistantMemory": "Add Assistant Memory",
"addAssistantMemoryPlaceholder": "Add assistant memory...",
"noAssistantMemories": "No assistant memories",
"noCurrentAssistant": "No current assistant",
"toggleAssistantMemoryActive": "Toggle Assistant Memory Function",
"loading": "Loading...", "loading": "Loading...",
"longMemory": "Long-term Memory", "longMemory": "Long-term Memory",
"toggleShortMemoryActive": "Toggle Short-term Memory", "toggleShortMemoryActive": "Toggle Short-term Memory",
@ -1331,6 +1340,36 @@
"messages.title": "Message Settings", "messages.title": "Message Settings",
"messages.use_serif_font": "Use serif font", "messages.use_serif_font": "Use serif font",
"model": "Default Model", "model": "Default Model",
"modelCombination": {
"title": "Model Combination",
"add": "Add Model Combination",
"addTitle": "Add Model Combination",
"editTitle": "Edit Model Combination",
"name": "Name",
"namePlaceholder": "Enter model combination name",
"nameRequired": "Please enter model combination name",
"reasonerModel": "Reasoner Model",
"targetModel": "Target Model",
"reasonerModelRequired": "Please select reasoner model",
"targetModelRequired": "Please select target model",
"selectModel": "Select Model",
"notSelected": "Not Selected",
"empty": "No model combinations yet",
"confirmDelete": "Delete Model Combination",
"confirmDeleteContent": "Are you sure you want to delete this model combination?",
"deleteSuccess": "Model combination deleted successfully",
"updateSuccess": "Model combination updated successfully",
"addSuccess": "Model combination added successfully",
"modelNotFound": "Model not found",
"reasoner": "Reasoner Model",
"target": "Target Model",
"description": {
"title": "Model Combination Description",
"content": "Model combination allows you to use one model for reasoning and another model for generating the final answer. This combination can leverage the strengths of different models to provide higher quality responses. You can also select a thinking library to guide the reasoning model to think in a specific domain-oriented way."
},
"thinkingLibrary": "Thinking Library",
"selectThinkingLibrary": "Select Thinking Library (Optional)"
},
"models.add.add_model": "Add Model", "models.add.add_model": "Add Model",
"models.add.group_name": "Group Name", "models.add.group_name": "Group Name",
"models.add.group_name.placeholder": "Optional e.g. ChatGPT", "models.add.group_name.placeholder": "Optional e.g. ChatGPT",
@ -1375,6 +1414,17 @@
"moresetting.check.warn": "Please be cautious when selecting this option. Incorrect selection may cause the model to malfunction!", "moresetting.check.warn": "Please be cautious when selecting this option. Incorrect selection may cause the model to malfunction!",
"moresetting.warn": "Risk Warning", "moresetting.warn": "Risk Warning",
"provider": { "provider": {
"gemini": {
"add_key": "Add Key",
"add_key_title": "Add New Key",
"enter_key": "Enter API key",
"import_keys": "Import Keys",
"import_keys_title": "Import API Keys",
"import_keys_desc": "Enter or upload a text file containing API keys, one per line",
"enter_keys": "Enter API keys, one per line",
"drop_file": "Click or drag file here",
"key_count": "{{count}} keys added"
},
"add.name": "Provider Name", "add.name": "Provider Name",
"add.name.placeholder": "Example: OpenAI", "add.name.placeholder": "Example: OpenAI",
"add.title": "Add Provider", "add.title": "Add Provider",

View File

@ -171,6 +171,7 @@
"input.web_search.button.ok": "去设置", "input.web_search.button.ok": "去设置",
"input.web_search.enable": "开启网络搜索", "input.web_search.enable": "开启网络搜索",
"input.web_search.enable_content": "需要先在设置中检查网络搜索连通性", "input.web_search.enable_content": "需要先在设置中检查网络搜索连通性",
"message.speak_selection": "朗读",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已创建", "message.new.branch.created": "新分支已创建",
"message.new.context": "清除上下文", "message.new.context": "清除上下文",
@ -798,6 +799,58 @@
"title": "数据恢复" "title": "数据恢复"
}, },
"settings": { "settings": {
"modelCombination": {
"title": "模型组合",
"add": "添加模型组合",
"addTitle": "添加模型组合",
"editTitle": "编辑模型组合",
"name": "名称",
"namePlaceholder": "输入模型组合名称",
"nameRequired": "请输入模型组合名称",
"reasonerModel": "推理模型",
"targetModel": "目标模型",
"reasonerModelRequired": "请选择推理模型",
"targetModelRequired": "请选择目标模型",
"selectModel": "选择模型",
"notSelected": "未选择",
"empty": "暂无模型组合",
"confirmDelete": "确认删除",
"confirmDeleteContent": "确定要删除此模型组合吗?",
"deleteSuccess": "删除成功",
"updateSuccess": "更新成功",
"addSuccess": "添加成功",
"modelNotFound": "找不到指定的模型",
"reasoner": "推理模型",
"target": "目标模型",
"description": {
"title": "说明",
"content": "模型组合允许你使用一个模型进行思考,然后使用另一个模型生成最终回答。这样可以结合不同模型的优势,获得更好的回答质量。"
},
"thinkingLibrary": "思考库",
"selectThinkingLibrary": "选择思考库(可选)"
},
"thinkingLibrary": {
"title": "思考库",
"select": "选择思考库",
"add": "添加思考库",
"edit": "编辑思考库",
"name": "名称",
"namePlaceholder": "输入思考库名称",
"nameRequired": "请输入思考库名称",
"category": "分类",
"categoryPlaceholder": "选择分类",
"categoryRequired": "请选择分类",
"description": "描述",
"descriptionPlaceholder": "输入思考库描述",
"descriptionRequired": "请输入思考库描述",
"prompt": "提示词",
"promptPlaceholder": "输入思考库提示词,使用 {question} 作为问题占位符",
"promptRequired": "请输入思考库提示词",
"promptHelp": "在提示词中使用 {question} 作为问题占位符,并确保包含 <think> 和 </think> 标签来包裹思考过程",
"default": "默认",
"deleteConfirm": "删除思考库",
"deleteConfirmContent": "确定要删除这个思考库吗?"
},
"about": "关于我们", "about": "关于我们",
"about.checkingUpdate": "正在检查更新...", "about.checkingUpdate": "正在检查更新...",
"about.checkUpdate": "检查更新", "about.checkUpdate": "检查更新",
@ -1082,6 +1135,7 @@
"launch.onboot": "开机自动启动", "launch.onboot": "开机自动启动",
"launch.title": "启动", "launch.title": "启动",
"launch.totray": "启动时最小化到托盘", "launch.totray": "启动时最小化到托盘",
"memory": { "memory": {
"historicalContext": { "historicalContext": {
"title": "历史对话上下文", "title": "历史对话上下文",
@ -1093,6 +1147,8 @@
"title": "记忆功能", "title": "记忆功能",
"description": "管理AI助手的长期记忆自动分析对话并提取重要信息", "description": "管理AI助手的长期记忆自动分析对话并提取重要信息",
"enableMemory": "启用记忆功能", "enableMemory": "启用记忆功能",
"enableShortMemory": "启用自动分析",
"enableAssistantMemory": "启用自动分析",
"enableAutoAnalyze": "启用自动分析", "enableAutoAnalyze": "启用自动分析",
"analyzeModel": "长期记忆分析模型", "analyzeModel": "长期记忆分析模型",
"shortMemoryAnalyzeModel": "短期记忆分析模型", "shortMemoryAnalyzeModel": "短期记忆分析模型",
@ -1243,6 +1299,17 @@
"toggleActive": "切换激活状态", "toggleActive": "切换激活状态",
"clearConfirmContentList": "确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。", "clearConfirmContentList": "确定要清空 {{name}} 中的所有记忆吗?此操作不可恢复。",
"shortMemory": "短期记忆", "shortMemory": "短期记忆",
"assistantMemory": "助手记忆",
"assistantMemorySettings": "助手记忆设置",
"assistantMemoryDescription": "助手记忆是与特定助手关联的记忆,可以帮助助手记住重要信息。",
"assistantMemoryAnalyzeModel": "助手记忆分析模型",
"addAssistantMemory": "添加助手记忆",
"addAssistantMemoryPlaceholder": "添加助手记忆...",
"noAssistantMemories": "无助手记忆",
"noCurrentAssistant": "无当前助手",
"selectAssistant": "选择助手",
"selectAssistantFirst": "请先选择助手",
"toggleAssistantMemoryActive": "切换助手记忆功能",
"loading": "加载中...", "loading": "加载中...",
"longMemory": "长期记忆", "longMemory": "长期记忆",
"shortMemorySettings": "短期记忆设置", "shortMemorySettings": "短期记忆设置",
@ -1436,6 +1503,17 @@
"moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!", "moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!",
"moresetting.warn": "风险警告", "moresetting.warn": "风险警告",
"provider": { "provider": {
"gemini": {
"add_key": "添加密钥",
"add_key_title": "添加新密钥",
"enter_key": "请输入API密钥",
"import_keys": "批量导入密钥",
"import_keys_title": "批量导入密钥",
"import_keys_desc": "请输入或上传包含API密钥的文本文件每行一个密钥",
"enter_keys": "请输入API密钥每行一个",
"drop_file": "点击或拖拽文件到此处",
"key_count": "已添加 {{count}} 个密钥"
},
"add.name": "提供商名称", "add.name": "提供商名称",
"add.name.placeholder": "例如 OpenAI", "add.name.placeholder": "例如 OpenAI",
"add.title": "添加提供商", "add.title": "添加提供商",

View File

@ -23,8 +23,86 @@ export function useSystemAgents() {
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
if (!resourcesPath || _agents.length > 0) return if (!resourcesPath || _agents.length > 0) return
const agents = await window.api.fs.read(resourcesPath + '/data/agents.json') try {
_agents = JSON.parse(agents) as Agent[] // 使用window.api.fs.read读取文件
const fileContent = await window.api.fs.read(resourcesPath + '/data/agents.json')
console.log('成功读取agents.json文件', typeof fileContent)
// 输出对象的结构,以便于调试
if (typeof fileContent === 'object' && fileContent !== null) {
console.log('文件内容对象的属性:', Object.keys(fileContent))
console.log('文件内容对象的类型:', Object.prototype.toString.call(fileContent))
if ('toString' in fileContent) {
console.log('文件内容的toString结果:', fileContent.toString().substring(0, 100) + '...')
}
}
// 处理Uint8Array类型二进制数据
if (fileContent instanceof Uint8Array || Object.prototype.toString.call(fileContent) === '[object Uint8Array]') {
console.log('文件内容是Uint8Array类型转换为字符串')
// 将Uint8Array转换为字符串
const decoder = new TextDecoder('utf-8')
const contentStr = decoder.decode(fileContent)
console.log('转换后的字符串前100个字符:', contentStr.substring(0, 100))
try {
// 尝试解析JSON
_agents = JSON.parse(contentStr) as Agent[]
console.log('成功解析Uint8Array内容')
} catch (parseError) {
console.error('Uint8Array解析失败:', parseError)
_agents = []
}
}
// 处理字符串类型
else if (typeof fileContent === 'string') {
console.log('文件内容是字符串类型')
try {
_agents = JSON.parse(fileContent) as Agent[]
console.log('成功解析字符串内容')
} catch (parseError) {
console.error('字符串解析失败:', parseError)
_agents = []
}
}
// 处理数组类型
else if (Array.isArray(fileContent)) {
console.log('文件内容是数组类型,直接使用')
_agents = fileContent as Agent[]
}
// 处理其他对象类型
else if (typeof fileContent === 'object' && fileContent !== null) {
console.log('文件内容是其他对象类型')
// 如果对象有data属性尝试使用它
if ('data' in fileContent) {
const data = (fileContent as any).data
if (Array.isArray(data)) {
_agents = data as Agent[]
console.log('成功使用对象的data属性')
} else {
console.error('data属性不是数组')
_agents = []
}
} else {
console.error('对象没有data属性')
_agents = []
}
}
// 处理其他类型
else {
console.error('未知类型的文件内容:', typeof fileContent)
_agents = []
}
// 确保_agents是数组
if (!Array.isArray(_agents)) {
console.error('_agents不是数组重置为空数组')
_agents = []
}
} catch (error) {
console.error('读取或解析agents.json失败:', error)
_agents = []
}
setAgents(_agents) setAgents(_agents)
}) })
}, [resourcesPath]) }, [resourcesPath])
@ -33,18 +111,36 @@ export function useSystemAgents() {
} }
export function groupByCategories(data: Agent[]) { export function groupByCategories(data: Agent[]) {
// 防止非数组输入
if (!Array.isArray(data)) {
console.error('groupByCategories函数收到非数组输入:', data)
return {}
}
const groupedMap = new Map<string, Agent[]>() const groupedMap = new Map<string, Agent[]>()
// 遍历数组中的每个项
data.forEach((item) => { data.forEach((item) => {
item.group?.forEach((category) => { // 确保item是对象且有group属性
if (item && typeof item === 'object' && item.group) {
// 确保group是数组
const groups = Array.isArray(item.group) ? item.group : [item.group]
groups.forEach((category) => {
if (typeof category === 'string') {
if (!groupedMap.has(category)) { if (!groupedMap.has(category)) {
groupedMap.set(category, []) groupedMap.set(category, [])
} }
groupedMap.get(category)?.push(item) groupedMap.get(category)?.push(item)
}
}) })
}
}) })
const result: Record<string, Agent[]> = {} const result: Record<string, Agent[]> = {}
Array.from(groupedMap.entries()).forEach(([category, items]) => { Array.from(groupedMap.entries()).forEach(([category, items]) => {
result[category] = items result[category] = items
}) })
return result return result
} }

View File

@ -1,125 +1,146 @@
import TTSProgressBar from '@renderer/components/TTSProgressBar' import TTSProgressBar from '@renderer/components/TTSProgressBar';
import { FONT_FAMILY } from '@renderer/config/constant' import { FONT_FAMILY } from '@renderer/config/constant';
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant';
import { useModel } from '@renderer/hooks/useModel' import { useModel } from '@renderer/hooks/useModel';
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime';
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings';
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService';
import { getMessageModelId } from '@renderer/services/MessagesService' import { getMessageModelId } from '@renderer/services/MessagesService';
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService';
import TTSService from '@renderer/services/TTSService' import TTSService from '@renderer/services/TTSService';
import { RootState, useAppDispatch } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store';
import { setLastPlayedMessageId, setSkipNextAutoTTS } from '@renderer/store/settings' import { setLastPlayedMessageId, setSkipNextAutoTTS } from '@renderer/store/settings';
import { Assistant, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } from '@renderer/types';
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils';
import { Divider, Dropdown } from 'antd' import { Divider, Dropdown } from 'antd';
import { ItemType } from 'antd/es/menu/interface' import { ItemType } from 'antd/es/menu/interface';
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux' // import { useSelector } from 'react-redux'; // Removed unused import
import styled from 'styled-components' import styled from 'styled-components'; // Ensure styled-components is imported
import MessageContent from './MessageContent' import MessageContent from './MessageContent';
import MessageErrorBoundary from './MessageErrorBoundary' import MessageErrorBoundary from './MessageErrorBoundary';
import MessageHeader from './MessageHeader' import MessageHeader from './MessageHeader';
import MessageMenubar from './MessageMenubar' import MessageMenubar from './MessageMenubar';
import MessageTokens from './MessageTokens' import MessageTokens from './MessageTokens';
interface Props { interface Props {
message: Message message: Message;
topic: Topic topic: Topic;
assistant?: Assistant assistant?: Assistant;
index?: number index?: number;
total?: number total?: number;
hidePresetMessages?: boolean hidePresetMessages?: boolean;
style?: React.CSSProperties style?: React.CSSProperties;
isGrouped?: boolean isGrouped?: boolean;
isStreaming?: boolean isStreaming?: boolean;
onSetMessages?: Dispatch<SetStateAction<Message[]>> onSetMessages?: Dispatch<SetStateAction<Message[]>>;
} }
// Function definition moved before its first use, fixing potential TS issue & improving readability
// FIX 1: Added explicit else to satisfy TS7030
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean): string | undefined => {
if (!isBubbleStyle) {
return undefined;
} else { // Explicit else block
return isAssistantMessage ? 'var(--chat-background-assistant)' : 'var(--chat-background-user)';
}
};
// FIX 2: Define styled component for the context menu trigger div
const ContextMenuTriggerDiv = styled.div<{ x: number; y: number }>`
position: fixed;
left: ${({ x }) => x}px;
top: ${({ y }) => y}px;
width: 1px;
height: 1px;
/* Optional: Ensure it doesn't interfere with other elements */
z-index: -1;
pointer-events: none;
`;
const MessageItem: FC<Props> = ({ const MessageItem: FC<Props> = ({
message, message,
topic, topic,
// assistant, // assistant: propAssistant,
index, index,
hidePresetMessages, hidePresetMessages,
isGrouped, isGrouped,
isStreaming = false, isStreaming = false,
style style
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation();
const { assistant, setModel } = useAssistant(message.assistantId) const { assistant, setModel } = useAssistant(message.assistantId);
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model const model = useModel(getMessageModelId(message), message.model?.provider) || message.model;
const { isBubbleStyle } = useMessageStyle() const { isBubbleStyle } = useMessageStyle();
const { showMessageDivider, messageFont, fontSize } = useSettings() const { showMessageDivider, messageFont, fontSize } = useSettings();
const { generating } = useRuntime() const { generating } = useRuntime();
const messageContainerRef = useRef<HTMLDivElement>(null) const messageContainerRef = useRef<HTMLDivElement>(null);
// const topic = useTopic(assistant, _topic?.id) const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null) const [selectedQuoteText, setSelectedQuoteText] = useState<string>('');
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('') const [selectedText, setSelectedText] = useState<string>('');
const dispatch = useAppDispatch();
// 获取TTS设置 // --- Consolidated State Selection ---
const { ttsEnabled, isVoiceCallActive, lastPlayedMessageId, skipNextAutoTTS } = useSelector( const ttsEnabled = useAppSelector((state) => state.settings.ttsEnabled);
(state: RootState) => state.settings const voiceCallEnabled = useAppSelector((state) => state.settings.voiceCallEnabled);
) const autoPlayTTSOutsideVoiceCall = useAppSelector((state) => state.settings.autoPlayTTSOutsideVoiceCall);
const dispatch = useAppDispatch() const isVoiceCallActive = useAppSelector((state) => state.settings.isVoiceCallActive);
const [selectedText, setSelectedText] = useState<string>('') const lastPlayedMessageId = useAppSelector((state) => state.settings.lastPlayedMessageId);
const skipNextAutoTTS = useAppSelector((state) => state.settings.skipNextAutoTTS);
// ---------------------------------
const isLastMessage = index === 0 const isLastMessage = index === 0;
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant';
const showMenubar = !isStreaming && !message.status.includes('ing') const showMenubar = !isStreaming && !message.status.includes('ing');
const fontFamily = useMemo(() => { const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY;
}, [messageFont]) }, [messageFont]);
const messageBorder = showMessageDivider ? '1px dotted var(--color-border)' : 'none'; // Applied directly in MessageFooter style
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage); // Call the fixed function
const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const handleContextMenu = useCallback((e: React.MouseEvent) => { const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault() e.preventDefault();
const _selectedText = window.getSelection()?.toString() || '' const _selectedText = window.getSelection()?.toString() || '';
setContextMenuPosition({ x: e.clientX, y: e.clientY });
// 无论是否选中文本,都设置上下文菜单位置
setContextMenuPosition({ x: e.clientX, y: e.clientY })
if (_selectedText) { if (_selectedText) {
const quotedText = const quotedText =
_selectedText _selectedText
.split('\n') .split('\n')
.map((line) => `> ${line}`) .map((line) => `> ${line}`)
.join('\n') + '\n-------------' .join('\n') + '\n-------------';
setSelectedQuoteText(quotedText) setSelectedQuoteText(quotedText);
setSelectedText(_selectedText) setSelectedText(_selectedText);
} else { } else {
setSelectedQuoteText('') setSelectedQuoteText('');
setSelectedText('') setSelectedText('');
} }
}, []) }, []);
// Close context menu on click outside
useEffect(() => { useEffect(() => {
const handleClick = () => { const handleClick = () => {
setContextMenuPosition(null) setContextMenuPosition(null);
} };
document.addEventListener('click', handleClick) document.addEventListener('click', handleClick);
return () => { return () => {
document.removeEventListener('click', handleClick) document.removeEventListener('click', handleClick);
} };
}, []) }, []);
// 使用 ref 跟踪消息状态变化 // --- Reset skipNextAutoTTS on New Message Completion ---
const prevGeneratingRef = useRef(generating) const prevGeneratingRef = useRef(generating);
// 更新 prevGeneratingRef 的值
useEffect(() => { useEffect(() => {
// 在每次渲染后更新 ref 值 prevGeneratingRef.current = generating;
prevGeneratingRef.current = generating }, [generating]);
}, [generating])
// 监听新消息生成,并在新消息生成时重置 skipNextAutoTTS
useEffect(() => { useEffect(() => {
// 如果从生成中变为非生成中,说明新消息刚刚生成完成
if ( if (
prevGeneratingRef.current && prevGeneratingRef.current &&
!generating && !generating &&
@ -127,124 +148,88 @@ const MessageItem: FC<Props> = ({
isAssistantMessage && isAssistantMessage &&
message.status === 'success' message.status === 'success'
) { ) {
console.log('新消息生成完成消息ID:', message.id) // 简化日志输出
console.log('消息生成完成重置skipNextAutoTTS为false, 消息ID:', message.id);
// 当新消息生成完成时,始终重置 skipNextAutoTTS 为 false dispatch(setSkipNextAutoTTS(false));
// 这样确保新生成的消息可以自动播放
console.log('新消息生成完成,重置 skipNextAutoTTS 为 false')
dispatch(setSkipNextAutoTTS(false))
} }
}, [isLastMessage, isAssistantMessage, message.status, message.id, generating, dispatch, prevGeneratingRef]) }, [generating, isLastMessage, isAssistantMessage, message.status, message.id, dispatch]);
// 当消息内容变化时,重置 skipNextAutoTTS
// --- Auto-play TTS Logic ---
useEffect(() => { useEffect(() => {
// 如果是最后一条助手消息,且消息状态为成功,且消息内容不为空 // 基本条件检查
if ( if (!isLastMessage || !isAssistantMessage || message.status !== 'success' || generating) {
isLastMessage && return;
isAssistantMessage &&
message.status === 'success' &&
message.content &&
message.content.trim()
) {
// 如果是新生成的消息,重置 skipNextAutoTTS 为 false
if (message.id !== lastPlayedMessageId) {
console.log(
'检测到新消息,重置 skipNextAutoTTS 为 false消息ID:',
message.id,
'消息内容前20个字符:',
message.content?.substring(0, 20)
)
dispatch(setSkipNextAutoTTS(false))
} }
if (!ttsEnabled) {
return;
} }
}, [isLastMessage, isAssistantMessage, message.status, message.content, message.id, lastPlayedMessageId, dispatch])
// 自动播放TTS的逻辑 // 语音通话相关条件检查
useEffect(() => { if (voiceCallEnabled === false && autoPlayTTSOutsideVoiceCall === false) {
// 如果是最后一条助手消息且消息状态为成功且不是正在生成中且TTS已启用 // 简化日志输出
// 注意只有在语音通话窗口打开时才自动播放TTS console.log('不自动播放TTS: 语音通话功能未启用 + 不允许在语音通话模式外自动播放');
if (isLastMessage && isAssistantMessage && message.status === 'success' && !generating && ttsEnabled) { return;
// 如果语音通话窗口没有打开则不自动播放TTS
if (!isVoiceCallActive) {
console.log('不自动播放TTS因为语音通话窗口没有打开:', isVoiceCallActive)
return
} }
if (voiceCallEnabled === true && isVoiceCallActive === false && autoPlayTTSOutsideVoiceCall === false) {
// 简化日志输出
console.log('不自动播放TTS: 语音通话窗口未打开 + 不允许在语音通话模式外自动播放');
return;
}
// 检查是否需要跳过自动TTS // 检查是否需要跳过自动TTS
if (skipNextAutoTTS) { if (skipNextAutoTTS === true) {
console.log( console.log('跳过自动TTS: skipNextAutoTTS = true, 消息ID:', message.id);
'跳过自动TTS因为 skipNextAutoTTS 为 true消息ID:', return;
message.id,
'消息内容前20个字符:',
message.content?.substring(0, 20),
'消息状态:',
message.status,
'是否最后一条消息:',
isLastMessage,
'是否助手消息:',
isAssistantMessage,
'是否正在生成中:',
generating,
'语音通话窗口状态:',
isVoiceCallActive
)
// 注意:不在这里重置 skipNextAutoTTS而是在新消息生成时重置
return
} }
console.log(
'准备自动播放TTS因为 skipNextAutoTTS 为 false消息ID:',
message.id,
'消息内容前20个字符:',
message.content?.substring(0, 20)
)
// 检查消息是否有内容,且消息是新的(不是上次播放过的消息) // 检查消息是否有内容,且消息是新的(不是上次播放过的消息)
if (message.content && message.content.trim() && message.id !== lastPlayedMessageId) { if (message.content && message.content.trim() && message.id !== lastPlayedMessageId) {
console.log('自动播放最新助手消息的TTS:', message.id, '语音通话窗口状态:', isVoiceCallActive) // 简化日志输出
console.log('准备自动播放TTS, 消息ID:', message.id);
// 更新最后播放的消息ID dispatch(setLastPlayedMessageId(message.id));
dispatch(setLastPlayedMessageId(message.id)) const playTimeout = setTimeout(() => {
console.log('自动播放TTS: 消息ID:', message.id);
// 使用延时确保消息已完全加载 TTSService.speakFromMessage(message);
setTimeout(() => { }, 500);
TTSService.speakFromMessage(message) return () => clearTimeout(playTimeout);
}, 500)
} else if (message.id === lastPlayedMessageId) { } else if (message.id === lastPlayedMessageId) {
console.log('不自动播放TTS因为该消息已经播放过:', message.id) // 简化日志输出
console.log('不自动播放TTS: 消息已播放过 (lastPlayedMessageId), ID:', message.id);
return; // 添加返回语句解决TypeScript错误
} }
}
}, [
isLastMessage,
isAssistantMessage,
message,
generating,
ttsEnabled,
isVoiceCallActive,
lastPlayedMessageId,
skipNextAutoTTS,
dispatch
])
// 添加默认返回值,确保所有代码路径都有返回值
return;
}, [
isLastMessage, isAssistantMessage, message, generating, ttsEnabled,
voiceCallEnabled, autoPlayTTSOutsideVoiceCall, isVoiceCallActive,
skipNextAutoTTS, lastPlayedMessageId, dispatch
]);
// --- Highlight message on event ---
const messageHighlightHandler = useCallback((highlight: boolean = true) => { const messageHighlightHandler = useCallback((highlight: boolean = true) => {
if (messageContainerRef.current) { if (messageContainerRef.current) {
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' }) messageContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
if (highlight) { if (highlight) {
const element = messageContainerRef.current;
element.classList.add('message-highlight');
setTimeout(() => { setTimeout(() => {
const classList = messageContainerRef.current?.classList element?.classList.remove('message-highlight');
classList?.add('message-highlight') }, 2500);
setTimeout(() => classList?.remove('message-highlight'), 2500)
}, 500)
} }
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)] const eventName = `${EVENT_NAMES.LOCATE_MESSAGE}:${message.id}`;
return () => unsubscribes.forEach((unsub) => unsub()) const unsubscribe = EventEmitter.on(eventName, messageHighlightHandler);
}, [message.id, messageHighlightHandler]) return () => unsubscribe();
}, [message.id, messageHighlightHandler]);
// --- Component Rendering ---
if (hidePresetMessages && message.isPreset) { if (hidePresetMessages && message.isPreset) {
return null return null;
} }
if (message.type === 'clear') { if (message.type === 'clear') {
@ -254,7 +239,7 @@ const MessageItem: FC<Props> = ({
{t('chat.message.new.context')} {t('chat.message.new.context')}
</Divider> </Divider>
</NewContextMessage> </NewContextMessage>
) );
} }
return ( return (
@ -270,17 +255,19 @@ const MessageItem: FC<Props> = ({
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}> style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
{contextMenuPosition && ( {contextMenuPosition && (
<Dropdown <Dropdown
overlayStyle={{ left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }} overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
menu={{ items: getContextMenuItems(t, selectedQuoteText, selectedText, message) }} menu={{ items: getContextMenuItems(t, selectedQuoteText, selectedText, message) }}
open={true} open={true}
trigger={['contextMenu']}> trigger={['contextMenu']}
<div /> >
{/* FIX 2: Use the styled component instead of inline style */}
<ContextMenuTriggerDiv x={contextMenuPosition.x} y={contextMenuPosition.y} />
</Dropdown> </Dropdown>
)} )}
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} /> <MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<MessageContentContainer <MessageContentContainer
className="message-content-container" className="message-content-container"
style={{ fontFamily, fontSize, background: messageBackground, overflowY: 'visible' }}> style={{ fontFamily, fontSize, background: messageBackground }}>
<MessageErrorBoundary> <MessageErrorBoundary>
<MessageContent message={message} model={model} /> <MessageContent message={message} model={model} />
</MessageErrorBoundary> </MessageErrorBoundary>
@ -292,8 +279,8 @@ const MessageItem: FC<Props> = ({
{showMenubar && ( {showMenubar && (
<MessageFooter <MessageFooter
style={{ style={{
border: messageBorder, borderTop: messageBorder, // Apply border style here
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined flexDirection: isBubbleStyle ? 'row-reverse' : undefined
}}> }}>
<MessageTokens message={message} isLastMessage={isLastMessage} /> <MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar <MessageMenubar
@ -305,91 +292,84 @@ const MessageItem: FC<Props> = ({
isLastMessage={isLastMessage} isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage} isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped} isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>} messageContainerRef={messageContainerRef}
setModel={setModel} setModel={setModel}
/> />
</MessageFooter> </MessageFooter>
)} )}
</MessageContentContainer> </MessageContentContainer>
</MessageContainer> </MessageContainer>
) );
} };
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) => {
return isBubbleStyle
? isAssistantMessage
? 'var(--chat-background-assistant)'
: 'var(--chat-background-user)'
: undefined
}
// Updated context menu items function
const getContextMenuItems = ( const getContextMenuItems = (
t: (key: string) => string, t: (key: string) => string,
selectedQuoteText: string, selectedQuoteText: string,
selectedText: string, selectedText: string,
message: Message, message: Message,
currentMessage?: Message
): ItemType[] => { ): ItemType[] => {
const items: ItemType[] = [] const items: ItemType[] = [];
// 只有在选中文本时,才添加复制和引用选项
if (selectedText) { if (selectedText) {
items.push({ items.push({
key: 'copy', key: 'copy',
label: t('common.copy'), label: t('common.copy'),
onClick: () => { onClick: () => {
navigator.clipboard.writeText(selectedText) navigator.clipboard.writeText(selectedText)
window.message.success({ content: t('message.copied'), key: 'copy-message' }) .then(() => window.message.success({ content: t('message.copied'), key: 'copy-message' }))
.catch(err => console.error('Failed to copy text: ', err));
} }
}) });
items.push({ items.push({
key: 'quote', key: 'quote',
label: t('chat.message.quote'), label: t('chat.message.quote'),
onClick: () => { onClick: () => {
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText) EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText);
} }
}) });
// 添加朗读选项
items.push({ items.push({
key: 'speak', key: 'speak_selected',
label: '朗读', label: t('chat.message.speak_selection') || '朗读选中部分',
onClick: () => { onClick: () => {
// 从选中的文本开始朗读后面的内容 // 首先手动关闭菜单
if (selectedText && currentMessage?.content) { document.dispatchEvent(new MouseEvent('click'));
// 找到选中文本在消息中的位置
const startIndex = currentMessage.content.indexOf(selectedText) // 使用setTimeout确保菜单关闭后再执行TTS功能
setTimeout(() => {
import('@renderer/services/TTSService').then(({ default: TTSServiceInstance }) => {
let textToSpeak = selectedText;
if (message.content) {
const startIndex = message.content.indexOf(selectedText);
if (startIndex !== -1) { if (startIndex !== -1) {
// 获取选中文本及其后面的所有内容 textToSpeak = selectedText; // Just speak selection
const textToSpeak = currentMessage.content.substring(startIndex)
import('@renderer/services/TTSService').then(({ default: TTSService }) => {
TTSService.speak(textToSpeak)
})
} else {
// 如果找不到精确位置,则只朗读选中的文本
import('@renderer/services/TTSService').then(({ default: TTSService }) => {
TTSService.speak(selectedText)
})
} }
} }
// 传递消息ID确保进度条和停止按钮正常工作
TTSServiceInstance.speak(textToSpeak, false, message.id); // 使用普通播放模式而非分段播放
}).catch(err => console.error('Failed to load or use TTSService:', err));
}, 100);
} }
}) });
items.push({ type: 'divider' });
} }
// 添加复制消息ID选项但不显示ID
items.push({ items.push({
key: 'copy_id', key: 'copy_id',
label: t('message.copy_id') || '复制消息ID', label: t('message.copy_id') || '复制消息ID',
onClick: () => { onClick: () => {
navigator.clipboard.writeText(message.id) navigator.clipboard.writeText(message.id)
window.message.success({ content: t('message.id_copied') || '消息ID已复制', key: 'copy-message-id' }) .then(() => window.message.success({ content: t('message.id_copied') || '消息ID已复制', key: 'copy-message-id' }))
.catch(err => console.error('Failed to copy message ID: ', err));
} }
}) });
return items return items;
} };
// Styled components definitions
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -397,36 +377,34 @@ const MessageContainer = styled.div`
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
padding: 0 20px; padding: 0 20px;
transform: translateZ(0); transform: translateZ(0);
will-change: transform; will-change: transform, background-color;
&.message-highlight { &.message-highlight {
background-color: var(--color-primary-mute); background-color: var(--color-primary-mute);
} }
.menubar { .menubar {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
transform: translateZ(0); transform: translateZ(0);
will-change: opacity; will-change: opacity;
&.show { pointer-events: none;
}
&:hover .menubar {
opacity: 1; opacity: 1;
pointer-events: auto;
} }
} `;
&:hover {
.menubar {
opacity: 1;
}
}
`
const MessageContentContainer = styled.div` const MessageContentContainer = styled.div`
max-width: 100%; max-width: 100%;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
justify-content: space-between;
margin-left: 46px; margin-left: 46px;
margin-top: 5px; margin-top: 5px;
overflow-y: auto; `;
`
const MessageFooter = styled.div` const MessageFooter = styled.div`
display: flex; display: flex;
@ -434,18 +412,19 @@ const MessageFooter = styled.div`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 2px 0; padding: 2px 0;
margin-top: 2px; margin-top: 8px;
border-top: 1px dotted var(--color-border); /* borderTop applied via style prop based on showMessageDivider */
gap: 20px; gap: 16px;
` `;
const NewContextMessage = styled.div` const NewContextMessage = styled.div`
cursor: pointer; cursor: pointer;
` `;
const ProgressBarWrapper = styled.div` const ProgressBarWrapper = styled.div`
width: 100%; width: calc(100% - 20px);
padding: 0 10px; padding: 5px 10px;
` margin-left: -10px;
`;
export default memo(MessageItem) export default memo(MessageItem);

View File

@ -1,53 +1,53 @@
import { SyncOutlined, TranslationOutlined } from '@ant-design/icons' import { SyncOutlined, TranslationOutlined } from '@ant-design/icons';
import TTSHighlightedText from '@renderer/components/TTSHighlightedText' import TTSHighlightedText from '@renderer/components/TTSHighlightedText';
import { isOpenAIWebSearch } from '@renderer/config/models' import { isOpenAIWebSearch } from '@renderer/config/models';
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService';
import { Message, Model } from '@renderer/types' import { Message, Model } from '@renderer/types';
import { getBriefInfo } from '@renderer/utils' import { getBriefInfo } from '@renderer/utils';
import { withMessageThought } from '@renderer/utils/formats' import { withMessageThought } from '@renderer/utils/formats';
import { Collapse, Divider, Flex } from 'antd' import { Collapse, Divider, Flex } from 'antd';
import { clone } from 'lodash' import { clone } from 'lodash';
import { Search } from 'lucide-react' import { Search } from 'lucide-react';
import React, { Fragment, useEffect, useMemo, useState } from 'react' import React, { Fragment, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import BarLoader from 'react-spinners/BarLoader' import BarLoader from 'react-spinners/BarLoader';
import BeatLoader from 'react-spinners/BeatLoader' import BeatLoader from 'react-spinners/BeatLoader';
import styled from 'styled-components' import styled from 'styled-components';
import Markdown from '../Markdown/Markdown' import Markdown from '../Markdown/Markdown';
import CitationsList from './CitationsList' import CitationsList from './CitationsList';
import MessageAttachments from './MessageAttachments' import MessageAttachments from './MessageAttachments';
import MessageError from './MessageError' import MessageError from './MessageError';
import MessageImage from './MessageImage' import MessageImage from './MessageImage';
import MessageThought from './MessageThought' import MessageThought from './MessageThought';
import MessageTools from './MessageTools' import MessageTools from './MessageTools';
interface Props { interface Props {
message: Message message: Message;
model?: Model model?: Model;
} }
const MessageContent: React.FC<Props> = ({ message: _message, model }) => { const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
const { t } = useTranslation() const { t } = useTranslation();
const message = withMessageThought(clone(_message)) const message = withMessageThought(clone(_message));
const isWebCitation = model && (isOpenAIWebSearch(model) || model.provider === 'openrouter') const isWebCitation = model && (isOpenAIWebSearch(model) || model.provider === 'openrouter');
const [isSegmentedPlayback, setIsSegmentedPlayback] = useState(false) const [isSegmentedPlayback, setIsSegmentedPlayback] = useState(false);
// 监听分段播放状态变化 // 监听分段播放状态变化
useEffect(() => { useEffect(() => {
const handleSegmentedPlaybackUpdate = (event: CustomEvent) => { const handleSegmentedPlaybackUpdate = (event: CustomEvent) => {
const { isSegmentedPlayback } = event.detail const { isSegmentedPlayback } = event.detail;
setIsSegmentedPlayback(isSegmentedPlayback) setIsSegmentedPlayback(isSegmentedPlayback);
} };
// 添加事件监听器 // 添加事件监听器
window.addEventListener('tts-segmented-playback-update', handleSegmentedPlaybackUpdate as EventListener) window.addEventListener('tts-segmented-playback-update', handleSegmentedPlaybackUpdate as EventListener);
// 组件卸载时移除事件监听器 // 组件卸载时移除事件监听器
return () => { return () => {
window.removeEventListener('tts-segmented-playback-update', handleSegmentedPlaybackUpdate as EventListener) window.removeEventListener('tts-segmented-playback-update', handleSegmentedPlaybackUpdate as EventListener);
} };
}, []) }, []);
// HTML实体编码辅助函数 // HTML实体编码辅助函数
const encodeHTML = (str: string) => { const encodeHTML = (str: string) => {
@ -58,47 +58,47 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
"'": '&apos;' "'": '&apos;'
} };
return entities[match] return entities[match];
}) });
} };
// Format citations for display // Format citations for display
const formattedCitations = useMemo(() => { const formattedCitations = useMemo(() => {
if (!message.metadata?.citations?.length && !message.metadata?.annotations?.length) return null if (!message.metadata?.citations?.length && !message.metadata?.annotations?.length) return null;
let citations: any[] = [] let citations: any[] = [];
if (model && isOpenAIWebSearch(model)) { if (model && isOpenAIWebSearch(model)) {
citations = citations =
message.metadata.annotations?.map((url, index) => { message.metadata.annotations?.map((url, index) => {
return { number: index + 1, url: url.url_citation?.url, hostname: url.url_citation.title } return { number: index + 1, url: url.url_citation?.url, hostname: url.url_citation.title };
}) || [] }) || [];
} else { } else {
citations = citations =
message.metadata?.citations?.map((url, index) => { message.metadata?.citations?.map((url, index) => {
try { try {
const hostname = new URL(url).hostname const hostname = new URL(url).hostname;
return { number: index + 1, url, hostname } return { number: index + 1, url, hostname };
} catch { } catch {
return { number: index + 1, url, hostname: url } return { number: index + 1, url, hostname: url };
} }
}) || [] }) || [];
} }
// Deduplicate by URL // Deduplicate by URL
const urlSet = new Set() const urlSet = new Set();
return citations return citations
.filter((citation) => { .filter((citation) => {
if (!citation.url || urlSet.has(citation.url)) return false if (!citation.url || urlSet.has(citation.url)) return false;
urlSet.add(citation.url) urlSet.add(citation.url);
return true return true;
}) })
.map((citation, index) => ({ .map((citation, index) => ({
...citation, ...citation,
number: index + 1 // Renumber citations sequentially after deduplication number: index + 1 // Renumber citations sequentially after deduplication
})) }));
}, [message.metadata?.citations, message.metadata?.annotations, model]) }, [message.metadata?.citations, message.metadata?.annotations, model]);
// 获取引用数据 // 获取引用数据
const citationsData = useMemo(() => { const citationsData = useMemo(() => {
@ -107,11 +107,11 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
message?.metadata?.webSearchInfo || message?.metadata?.webSearchInfo ||
message?.metadata?.groundingMetadata?.groundingChunks?.map((chunk) => chunk?.web) || message?.metadata?.groundingMetadata?.groundingChunks?.map((chunk) => chunk?.web) ||
message?.metadata?.annotations?.map((annotation) => annotation.url_citation) || message?.metadata?.annotations?.map((annotation) => annotation.url_citation) ||
[] [];
const citationsUrls = formattedCitations || [] const citationsUrls = formattedCitations || [];
// 合并引用数据 // 合并引用数据
const data = new Map() const data = new Map();
// 添加webSearch结果 // 添加webSearch结果
searchResults.forEach((result) => { searchResults.forEach((result) => {
@ -119,8 +119,8 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
url: result.url || result.uri || result.link, url: result.url || result.uri || result.link,
title: result.title || result.hostname, title: result.title || result.hostname,
content: result.content content: result.content
}) });
}) });
// 添加citations // 添加citations
citationsUrls.forEach((result) => { citationsUrls.forEach((result) => {
@ -129,18 +129,18 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
url: result.url, url: result.url,
title: result.title || result.hostname || undefined, title: result.title || result.hostname || undefined,
content: result.content || undefined content: result.content || undefined
}) });
} }
}) });
return data return data;
}, [ }, [
formattedCitations, formattedCitations,
message?.metadata?.annotations, message?.metadata?.annotations,
message?.metadata?.groundingMetadata?.groundingChunks, message?.metadata?.groundingMetadata?.groundingChunks,
message?.metadata?.webSearch?.results, message?.metadata?.webSearch?.results,
message?.metadata?.webSearchInfo message?.metadata?.webSearchInfo
]) ]);
// Process content to make citation numbers clickable // Process content to make citation numbers clickable
const processedContent = useMemo(() => { const processedContent = useMemo(() => {
@ -152,35 +152,37 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
message.metadata?.annotations message.metadata?.annotations
) )
) { ) {
return message.content return message.content;
} }
let content = message.content let content = message.content;
const searchResultsCitations = message?.metadata?.webSearch?.results?.map((result) => result.url) || [] const searchResultsCitations = message?.metadata?.webSearch?.results?.map((result) => result.url) || [];
const citations = message?.metadata?.citations || searchResultsCitations const citations = message?.metadata?.citations || searchResultsCitations;
// Convert [n] format to superscript numbers and make them clickable // Convert [n] format to superscript numbers and make them clickable
// Use <sup> tag for superscript and make it a link with citation data // Use <sup> tag for superscript and make it a link with citation data
if (message.metadata?.webSearch) { if (message.metadata?.webSearch) {
content = content.replace(/\[\[(\d+)\]\]|\[(\d+)\]/g, (match, num1, num2) => { content = content.replace(/\[\[(\d+)\]\]|\[(\d+)\]/g, (match, num1, num2) => {
const num = num1 || num2 const num = num1 || num2;
const index = parseInt(num) - 1 const index = parseInt(num) - 1;
if (index >= 0 && index < citations.length) { if (index >= 0 && index < citations.length) {
const link = citations[index] const link = citations[index];
const citationData = link ? encodeHTML(JSON.stringify(citationsData.get(link) || { url: link })) : null const citationData = link ? encodeHTML(JSON.stringify(citationsData.get(link) || { url: link })) : null;
return link ? `[<sup data-citation='${citationData}'>${num}</sup>](${link})` : `<sup>${num}</sup>` return link ? `[<sup data-citation='${citationData}'>${num}</sup>](${link})` : `<sup>${num}</sup>`;
} }
return match return match;
}) });
} else { } else {
// Handle other citation formats if necessary, potentially adjusting this logic
// The original else block seemed specific, ensure it covers necessary cases or adjust
content = content.replace(/\[<sup>(\d+)<\/sup>\]\(([^)]+)\)/g, (_, num, url) => { content = content.replace(/\[<sup>(\d+)<\/sup>\]\(([^)]+)\)/g, (_, num, url) => {
const citationData = url ? encodeHTML(JSON.stringify(citationsData.get(url) || { url })) : null const citationData = url ? encodeHTML(JSON.stringify(citationsData.get(url) || { url })) : null;
return `[<sup data-citation='${citationData}'>${num}</sup>](${url})` return `[<sup data-citation='${citationData}'>${num}</sup>](${url})`
}) });
} }
return content return content;
}, [ }, [
message.metadata?.citations, message.metadata?.citations,
message.metadata?.webSearch, message.metadata?.webSearch,
@ -188,14 +190,14 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
message.metadata?.annotations, message.metadata?.annotations,
message.content, message.content,
citationsData citationsData
]) ]);
if (message.status === 'sending') { if (message.status === 'sending') {
return ( return (
<MessageContentLoading> <MessageContentLoading>
<SyncOutlined spin size={24} /> <SyncOutlined spin size={24} />
</MessageContentLoading> </MessageContentLoading>
) );
} }
if (message.status === 'searching') { if (message.status === 'searching') {
@ -205,18 +207,23 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
<SearchingText>{t('message.searching')}</SearchingText> <SearchingText>{t('message.searching')}</SearchingText>
<BarLoader color="#1677ff" /> <BarLoader color="#1677ff" />
</SearchingContainer> </SearchingContainer>
) );
} }
if (message.status === 'error') { if (message.status === 'error') {
return <MessageError message={message} /> return <MessageError message={message} />;
} }
if (message.type === '@' && model) { if (message.type === '@' && model) {
const content = `[@${model.name}](#) ${getBriefInfo(message.content)}` const content = `[@${model.name}](#) ${getBriefInfo(message.content)}`;
return <Markdown message={{ ...message, content }} /> return <Markdown message={{ ...message, content }} />;
} }
const toolUseRegex = /<tool_use>([\s\S]*?)<\/tool_use>/g
// --- MODIFIED LINE BELOW ---
// This regex now matches <tool_use ...> OR <XML ...> tags (case-insensitive)
// and allows for attributes and whitespace, then removes the entire tag pair and content.
const tagsToRemoveRegex = /<(?:tool_use|XML)(?:[^>]*)?>(?:.*?)<\/\s*(?:tool_use|XML)\s*>/gsi;
return ( return (
<Fragment> <Fragment>
<Flex gap="8px" wrap style={{ marginBottom: 10 }}> <Flex gap="8px" wrap style={{ marginBottom: 10 }}>
@ -248,12 +255,12 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
<span <span
className="reference-id" className="reference-id"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
navigator.clipboard.writeText(refMsg.id) navigator.clipboard.writeText(refMsg.id);
window.message.success({ window.message.success({
content: t('message.id_copied') || '消息ID已复制', content: t('message.id_copied') || '消息ID已复制',
key: 'copy-reference-id' key: 'copy-reference-id'
}) });
}}> }}>
ID: {refMsg.id} ID: {refMsg.id}
</span> </span>
@ -292,12 +299,12 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
<span <span
className="reference-id" className="reference-id"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
navigator.clipboard.writeText((message as any).referencedMessage.id) navigator.clipboard.writeText((message as any).referencedMessage.id);
window.message.success({ window.message.success({
content: t('message.id_copied') || '消息ID已复制', content: t('message.id_copied') || '消息ID已复制',
key: 'copy-reference-id' key: 'copy-reference-id'
}) });
}}> }}>
ID: {(message as any).referencedMessage.id} ID: {(message as any).referencedMessage.id}
</span> </span>
@ -313,13 +320,16 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
/> />
)} )}
<div className="message-content-tools"> <div className="message-content-tools">
{/* These components display tool/thought info separately at the top */}
<MessageThought message={message} /> <MessageThought message={message} />
<MessageTools message={message} /> <MessageTools message={message} />
</div> </div>
{isSegmentedPlayback ? ( {isSegmentedPlayback ? (
<TTSHighlightedText text={processedContent.replace(toolUseRegex, '')} /> // Apply regex replacement here for TTS
<TTSHighlightedText text={processedContent.replace(tagsToRemoveRegex, '')} />
) : ( ) : (
<Markdown message={{ ...message, content: processedContent.replace(toolUseRegex, '') }} /> // Apply regex replacement here for Markdown display
<Markdown message={{ ...message, content: processedContent.replace(tagsToRemoveRegex, '') }} />
)} )}
{message.metadata?.generateImage && <MessageImage message={message} />} {message.metadata?.generateImage && <MessageImage message={message} />}
{message.translatedContent && ( {message.translatedContent && (
@ -330,6 +340,7 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
{message.translatedContent === t('translate.processing') ? ( {message.translatedContent === t('translate.processing') ? (
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginBottom: 15 }} /> <BeatLoader color="var(--color-text-2)" size="10" style={{ marginBottom: 15 }} />
) : ( ) : (
// Render translated content (assuming it doesn't need tag removal, adjust if needed)
<Markdown message={{ ...message, content: message.translatedContent }} /> <Markdown message={{ ...message, content: message.translatedContent }} />
)} )}
</Fragment> </Fragment>
@ -389,8 +400,10 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
)} )}
<MessageAttachments message={message} /> <MessageAttachments message={message} />
</Fragment> </Fragment>
) );
} };
// Styled components and global styles remain the same...
const MessageContentLoading = styled.div` const MessageContentLoading = styled.div`
display: flex; display: flex;
@ -399,7 +412,7 @@ const MessageContentLoading = styled.div`
height: 32px; height: 32px;
margin-top: -5px; margin-top: -5px;
margin-bottom: 5px; margin-bottom: 5px;
` `;
const SearchingContainer = styled.div` const SearchingContainer = styled.div`
display: flex; display: flex;
@ -410,22 +423,22 @@ const SearchingContainer = styled.div`
border-radius: 10px; border-radius: 10px;
margin-bottom: 10px; margin-bottom: 10px;
gap: 10px; gap: 10px;
` `;
const MentionTag = styled.span` const MentionTag = styled.span`
color: var(--color-link); color: var(--color-link);
` `;
const SearchingText = styled.div` const SearchingText = styled.div`
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
text-decoration: none; text-decoration: none;
color: var(--color-text-1); color: var(--color-text-1);
` `;
const SearchEntryPoint = styled.div` const SearchEntryPoint = styled.div`
margin: 10px 2px; margin: 10px 2px;
` `;
// 引用消息样式 - 使用全局样式 // 引用消息样式 - 使用全局样式
const referenceStyles = ` const referenceStyles = `
@ -536,23 +549,29 @@ const referenceStyles = `
} }
} }
} }
` `;
// 将样式添加到文档中 // 将样式添加到文档中
try { try {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
const styleElement = document.createElement('style') // Check if style already exists to prevent duplicates during HMR
let styleElement = document.getElementById('message-content-reference-styles');
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'message-content-reference-styles';
styleElement.textContent = styleElement.textContent =
referenceStyles + referenceStyles +
` `
.message-content-tools { .message-content-tools {
margin-top: 20px; margin-top: 20px; /* Adjust as needed */
margin-bottom: 10px; /* Add space before main content */
}
`;
document.head.appendChild(styleElement);
} }
`
document.head.appendChild(styleElement)
} }
} catch (error) { } catch (error) {
console.error('Failed to add reference styles:', error) console.error('Failed to add reference styles:', error);
} }
export default React.memo(MessageContent) export default React.memo(MessageContent);

View File

@ -1,4 +1,5 @@
import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons' import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined, BookOutlined } from '@ant-design/icons'
import AssistantMemoryPopup from '@renderer/components/AssistantMemoryPopup'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
@ -52,7 +53,7 @@ interface Props {
isGrouped?: boolean isGrouped?: boolean
isLastMessage: boolean isLastMessage: boolean
isAssistantMessage: boolean isAssistantMessage: boolean
messageContainerRef: React.RefObject<HTMLDivElement> messageContainerRef: React.RefObject<HTMLDivElement | null>
setModel: (model: Model) => void setModel: (model: Model) => void
} }
@ -414,6 +415,18 @@ const MessageMenubar: FC<Props> = (props) => {
</Tooltip> </Tooltip>
)} )}
{isAssistantMessage && ttsEnabled && <TTSButton message={message} className="message-action-button" />} {isAssistantMessage && ttsEnabled && <TTSButton message={message} className="message-action-button" />}
{isAssistantMessage && (
<Tooltip title={t('settings.memory.assistantMemory') || '助手记忆'} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => {
e.stopPropagation()
AssistantMemoryPopup.show({ assistantId: assistant.id })
}}>
<BookOutlined />
</ActionButton>
</Tooltip>
)}
{!isUserMessage && ( {!isUserMessage && (
<Dropdown <Dropdown
menu={{ menu={{

View File

@ -172,6 +172,6 @@ const NarrowIcon = styled(NavbarIcon)`
} }
` `
// AnalyzeButton组件已移动到话题右键工具栏中
export default HeaderNavbar export default HeaderNavbar

View File

@ -0,0 +1,556 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { useProviders } from '@renderer/hooks/useProvider'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Button, Divider, Form, Input, Switch, Tooltip, message } from 'antd'
import { InfoCircleOutlined, PlusOutlined, ApiOutlined } from '@ant-design/icons'
import { HStack, VStack } from '@renderer/components/Layout'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { Model } from '@renderer/types'
// 不再需要 useAppDispatch
import { createAllDeepClaudeProviders } from '@renderer/utils/createDeepClaudeProvider'
interface ModelCombination {
id: string
name: string
reasonerModel: string
targetModel: string
enabled: boolean
}
const DeepClaudeSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const { providers } = useProviders()
// 本地状态
const [combinations, setCombinations] = useState<ModelCombination[]>([])
const [newCombination, setNewCombination] = useState<{
name: string;
reasonerModel: string;
targetModel: string;
}>({
name: '',
reasonerModel: '',
targetModel: ''
})
// 编辑状态
const [editingCombination, setEditingCombination] = useState<string | null>(null)
const [editForm, setEditForm] = useState<{
name: string;
reasonerModel: string;
targetModel: string;
}>({
name: '',
reasonerModel: '',
targetModel: ''
})
// 获取所有可用的模型
const allModels = providers.flatMap(provider =>
provider.models.map(model => ({
...model,
providerName: provider.name,
providerId: provider.id
}))
)
// 推荐的推理模型
const recommendedReasonerModels = allModels.filter(model => {
// 推荐 DeepSeek 模型作为推理模型
return model.name.toLowerCase().includes('deepseek') ||
model.name.toLowerCase().includes('deep-seek') ||
model.name.toLowerCase().includes('yi') ||
model.name.toLowerCase().includes('qwen') ||
model.name.toLowerCase().includes('glm')
})
// 推荐的目标模型
const recommendedTargetModels = allModels.filter(model => {
// 推荐 Claude 和 Gemini 模型作为目标模型
return model.name.toLowerCase().includes('claude') ||
model.name.toLowerCase().includes('gemini') ||
model.name.toLowerCase().includes('gpt')
})
// 创建提供商
const createProvider = () => {
try {
// 使用新的方式创建提供商
message.info(t('settings.deepclaude.provider_created_info'))
} catch (error) {
console.error('创建DeepClaude提供商失败:', error)
message.error(t('settings.deepclaude.provider_create_failed'))
}
}
// 创建所有提供商
const createAllProviders = () => {
try {
const providers = createAllDeepClaudeProviders()
if (providers.length > 0) {
message.success(t('settings.deepclaude.all_providers_created', { count: providers.length }))
} else {
message.info(t('settings.deepclaude.no_combinations'))
}
} catch (error) {
console.error('创建所有DeepClaude提供商失败:', error)
message.error(t('settings.deepclaude.all_providers_create_failed'))
}
}
// 添加新组合
const addCombination = () => {
if (!newCombination.name || !newCombination.reasonerModel || !newCombination.targetModel) {
return
}
const newCombinations = [
...combinations,
{
id: `deepclaude-${Date.now()}`,
name: newCombination.name,
reasonerModel: newCombination.reasonerModel,
targetModel: newCombination.targetModel,
enabled: true
}
]
setCombinations(newCombinations)
// 重置表单
setNewCombination({
name: '',
reasonerModel: '',
targetModel: ''
})
}
// 开始编辑组合
const startEditCombination = (id: string) => {
const combination = combinations.find(c => c.id === id)
if (!combination) return
setEditingCombination(id)
setEditForm({
name: combination.name,
reasonerModel: combination.reasonerModel,
targetModel: combination.targetModel
})
}
// 保存编辑
const saveEditCombination = () => {
if (!editingCombination || !editForm.name || !editForm.reasonerModel || !editForm.targetModel) {
return
}
const newCombinations = combinations.map(c =>
c.id === editingCombination
? {
...c,
name: editForm.name,
reasonerModel: editForm.reasonerModel,
targetModel: editForm.targetModel
}
: c
)
setCombinations(newCombinations)
// 退出编辑模式
cancelEdit()
}
// 取消编辑
const cancelEdit = () => {
setEditingCombination(null)
setEditForm({
name: '',
reasonerModel: '',
targetModel: ''
})
}
// 删除组合
const deleteCombination = (id: string) => {
const newCombinations = combinations.filter(c => c.id !== id)
setCombinations(newCombinations)
}
// 更新组合状态
const updateCombinationStatus = (id: string, enabled: boolean) => {
const newCombinations = combinations.map(c =>
c.id === id ? { ...c, enabled } : c
)
setCombinations(newCombinations)
}
// 获取模型名称
const getModelFullName = (modelId: string) => {
const model = allModels.find(m => m.id === modelId)
if (!model) return modelId
return `${model.name} (${model.providerName})`
}
// 获取模型对象
const getModelById = (modelId: string): Model | undefined => {
return allModels.find(m => m.id === modelId)
}
// 选择推理模型
const selectReasonerModel = async () => {
try {
const currentModel = getModelById(newCombination.reasonerModel)
const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) {
// 保存模型 ID 和提供商信息
setNewCombination({
...newCombination,
reasonerModel: selectedModel.id
})
console.log('选择推理模型:', selectedModel.name, '提供商:', selectedModel.provider)
}
// 确保弹窗关闭
SelectModelPopup.hide()
} catch (error) {
console.error('选择推理模型出错:', error)
SelectModelPopup.hide()
}
}
// 选择目标模型
const selectTargetModel = async () => {
try {
const currentModel = getModelById(newCombination.targetModel)
const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) {
// 保存模型 ID 和提供商信息
setNewCombination({
...newCombination,
targetModel: selectedModel.id
})
console.log('选择目标模型:', selectedModel.name, '提供商:', selectedModel.provider)
}
// 确保弹窗关闭
SelectModelPopup.hide()
} catch (error) {
console.error('选择目标模型出错:', error)
SelectModelPopup.hide()
}
}
// 编辑时选择推理模型
const selectEditReasonerModel = async () => {
try {
const currentModel = getModelById(editForm.reasonerModel)
const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) {
// 保存模型 ID 和提供商信息
setEditForm({
...editForm,
reasonerModel: selectedModel.id
})
console.log('编辑时选择推理模型:', selectedModel.name, '提供商:', selectedModel.provider)
}
// 确保弹窗关闭
SelectModelPopup.hide()
} catch (error) {
console.error('编辑时选择推理模型出错:', error)
SelectModelPopup.hide()
}
}
// 编辑时选择目标模型
const selectEditTargetModel = async () => {
try {
const currentModel = getModelById(editForm.targetModel)
const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) {
// 保存模型 ID 和提供商信息
setEditForm({
...editForm,
targetModel: selectedModel.id
})
console.log('编辑时选择目标模型:', selectedModel.name, '提供商:', selectedModel.provider)
}
// 确保弹窗关闭
SelectModelPopup.hide()
} catch (error) {
console.error('编辑时选择目标模型出错:', error)
SelectModelPopup.hide()
}
}
return (
<SettingContainer theme={theme}>
<SettingGroup theme={theme}>
<SettingTitle>
<HStack alignItems="center" gap={10}>
{t('settings.deepclaude.title')}
<Tooltip title={t('settings.deepclaude.tooltip')}>
<InfoCircleOutlined />
</Tooltip>
</HStack>
</SettingTitle>
<SettingDivider />
{/* 现有组合列表 */}
{combinations.length > 0 && (
<>
<SettingRow>
<SettingRowTitle>{t('settings.deepclaude.combinations')}</SettingRowTitle>
</SettingRow>
{combinations.map(combination => (
<CombinationItem key={combination.id}>
<VStack gap={10}>
<HStack justifyContent="space-between" alignItems="center">
<strong>{combination.name}</strong>
<Switch
checked={combination.enabled}
onChange={(enabled) => updateCombinationStatus(combination.id, enabled)}
/>
</HStack>
<HStack gap={10}>
<ModelInfo>
<ModelLabel>{t('settings.deepclaude.reasoner')}:</ModelLabel>
<ModelValue>{getModelFullName(combination.reasonerModel)}</ModelValue>
</ModelInfo>
<ModelInfo>
<ModelLabel>{t('settings.deepclaude.target')}:</ModelLabel>
<ModelValue>{getModelFullName(combination.targetModel)}</ModelValue>
</ModelInfo>
</HStack>
<HStack justifyContent="flex-end" gap={8}>
<Button
type="primary"
size="small"
icon={<ApiOutlined />}
onClick={() => createProvider()}
>
{t('settings.deepclaude.create_provider')}
</Button>
<Button
type="default"
size="small"
onClick={() => startEditCombination(combination.id)}
>
{t('common.edit')}
</Button>
<Button
danger
size="small"
onClick={() => deleteCombination(combination.id)}
>
{t('common.delete')}
</Button>
</HStack>
</VStack>
</CombinationItem>
))}
<Divider style={{ margin: '20px 0' }} />
</>
)}
{combinations.length > 0 && (
<HStack justifyContent="flex-end" style={{ marginBottom: '20px' }}>
<Button
type="primary"
icon={<ApiOutlined />}
onClick={createAllProviders}
>
{t('settings.deepclaude.create_all_providers')}
</Button>
</HStack>
)}
{/* 编辑组合表单 */}
{editingCombination && (
<>
<SettingRow>
<SettingRowTitle>{t('settings.deepclaude.edit_combination')}</SettingRowTitle>
</SettingRow>
<Form layout="vertical">
<Form.Item label={t('settings.deepclaude.combination_name')}>
<Input
value={editForm.name}
onChange={(e) => setEditForm({...editForm, name: e.target.value})}
placeholder={t('settings.deepclaude.combination_name_placeholder')}
/>
</Form.Item>
<Form.Item label={t('settings.deepclaude.select_reasoner')}>
<ModelSelectButton
model={getModelById(editForm.reasonerModel)}
onClick={selectEditReasonerModel}
placeholder={t('settings.deepclaude.select_reasoner_placeholder')}
recommended={recommendedReasonerModels.some(m => m.id === editForm.reasonerModel) ? '★' : ''}
/>
</Form.Item>
<ModelTip>{t('settings.deepclaude.reasoner_tip')}</ModelTip>
<Form.Item label={t('settings.deepclaude.select_target')}>
<ModelSelectButton
model={getModelById(editForm.targetModel)}
onClick={selectEditTargetModel}
placeholder={t('settings.deepclaude.select_target_placeholder')}
recommended={recommendedTargetModels.some(m => m.id === editForm.targetModel) ? '★' : ''}
/>
</Form.Item>
<ModelTip>{t('settings.deepclaude.target_tip')}</ModelTip>
<Form.Item>
<HStack gap={8}>
<Button
type="primary"
onClick={saveEditCombination}
disabled={!editForm.name || !editForm.reasonerModel || !editForm.targetModel}
>
{t('common.save')}
</Button>
<Button onClick={cancelEdit}>
{t('common.cancel')}
</Button>
</HStack>
</Form.Item>
</Form>
<Divider style={{ margin: '20px 0' }} />
</>
)}
{/* 添加新组合表单 */}
{!editingCombination && (
<>
<SettingRow>
<SettingRowTitle>{t('settings.deepclaude.add_combination')}</SettingRowTitle>
</SettingRow>
<Form layout="vertical">
<Form.Item label={t('settings.deepclaude.combination_name')}>
<Input
value={newCombination.name}
onChange={(e) => setNewCombination({...newCombination, name: e.target.value})}
placeholder={t('settings.deepclaude.combination_name_placeholder')}
/>
</Form.Item>
<Form.Item label={t('settings.deepclaude.select_reasoner')}>
<ModelSelectButton
model={getModelById(newCombination.reasonerModel)}
onClick={selectReasonerModel}
placeholder={t('settings.deepclaude.select_reasoner_placeholder')}
recommended={recommendedReasonerModels.some(m => m.id === newCombination.reasonerModel) ? '★' : ''}
/>
</Form.Item>
<ModelTip>{t('settings.deepclaude.reasoner_tip')}</ModelTip>
<Form.Item label={t('settings.deepclaude.select_target')}>
<ModelSelectButton
model={getModelById(newCombination.targetModel)}
onClick={selectTargetModel}
placeholder={t('settings.deepclaude.select_target_placeholder')}
recommended={recommendedTargetModels.some(m => m.id === newCombination.targetModel) ? '★' : ''}
/>
</Form.Item>
<ModelTip>{t('settings.deepclaude.target_tip')}</ModelTip>
<Form.Item>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={addCombination}
disabled={!newCombination.name || !newCombination.reasonerModel || !newCombination.targetModel}
>
{t('settings.deepclaude.add')}
</Button>
</Form.Item>
</Form>
</>
)}
</SettingGroup>
</SettingContainer>
)
}
const CombinationItem = styled.div`
padding: 15px;
border: 1px solid var(--color-border);
border-radius: 8px;
margin-bottom: 10px;
`
const ModelInfo = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`
const ModelLabel = styled.span`
font-size: 12px;
color: var(--color-text-secondary);
`
const ModelValue = styled.span`
font-size: 14px;
color: var(--color-text);
`
const ModelTip = styled.div`
margin-top: -15px;
margin-bottom: 15px;
color: var(--color-text-secondary);
`
interface ModelSelectButtonProps {
model?: Model;
onClick: () => void;
placeholder: string;
recommended?: string;
}
const ModelSelectButton: FC<ModelSelectButtonProps> = ({ model, onClick, placeholder, recommended }) => {
return (
<ModelSelectButtonWrapper onClick={onClick}>
{model ? (
<>
<ModelAvatar model={model} size={20} />
<ModelName>
{model.name} ({model.provider}) {recommended}
</ModelName>
</>
) : (
<ModelName>{placeholder}</ModelName>
)}
</ModelSelectButtonWrapper>
)
}
const ModelSelectButtonWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: var(--color-primary);
}
`
const ModelName = styled.span`
font-size: 14px;
color: var(--color-text);
`
export default DeepClaudeSettings

View File

@ -0,0 +1,176 @@
import { DeleteOutlined } from '@ant-design/icons'
import { addAssistantMemoryItem } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { deleteAssistantMemory, setAssistantMemoryActive } from '@renderer/store/memory'
import { Button, Empty, Input, List, Select, Switch, Tooltip, Typography } from 'antd'
import _ from 'lodash'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
const AssistantMemoryManager = () => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
// 获取所有助手
const assistants = useAppSelector((state) => state.assistants?.assistants || [])
// 获取当前助手ID
const currentAssistantId = useAppSelector((state) => state.messages?.currentAssistant?.id)
// 添加助手选择器状态
const [selectedAssistantId, setSelectedAssistantId] = useState('')
// 初始化选中的助手ID
useEffect(() => {
if (currentAssistantId && !selectedAssistantId) {
setSelectedAssistantId(currentAssistantId)
}
}, [currentAssistantId, selectedAssistantId])
// 获取助手记忆状态
const assistantMemoryActive = useAppSelector((state) => state.memory?.assistantMemoryActive || false)
const assistantMemories = useAppSelector((state) => {
const allAssistantMemories = state.memory?.assistantMemories || []
// 只显示选中助手的记忆
return selectedAssistantId ? allAssistantMemories.filter((memory) => memory.assistantId === selectedAssistantId) : []
})
// 添加助手记忆的状态
const [newMemoryContent, setNewMemoryContent] = useState('')
// 切换助手记忆功能激活状态
const handleToggleActive = (checked: boolean) => {
dispatch(setAssistantMemoryActive(checked))
}
// 添加新的助手记忆 - 使用防抖减少频繁更新
const handleAddMemory = useCallback(
_.debounce(() => {
if (newMemoryContent.trim() && selectedAssistantId) {
addAssistantMemoryItem(newMemoryContent.trim(), selectedAssistantId)
setNewMemoryContent('') // 清空输入框
}
}, 300),
[newMemoryContent, selectedAssistantId]
)
// 删除助手记忆 - 直接删除无需确认,使用节流避免频繁删除操作
const handleDeleteMemory = useCallback(
_.throttle(async (id: string) => {
// 先从当前状态中获取要删除的记忆之外的所有记忆
const state = store.getState().memory
const filteredAssistantMemories = state.assistantMemories.filter((memory) => memory.id !== id)
// 执行删除操作
dispatch(deleteAssistantMemory(id))
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
try {
// 加载当前文件数据
const currentData = await window.api.memory.loadData()
// 替换 assistantMemories 数组,保留其他重要设置
const newData = {
...currentData,
assistantMemories: filteredAssistantMemories,
assistantMemoryActive: currentData.assistantMemoryActive,
assistantMemoryAnalyzeModel: currentData.assistantMemoryAnalyzeModel
}
// 使用 true 参数强制覆盖文件
const result = await window.api.memory.saveData(newData, true)
if (result) {
console.log(`[AssistantMemoryManager] Successfully deleted assistant memory with ID ${id}`)
// 移除消息提示,避免触发界面重新渲染
} else {
console.error(`[AssistantMemoryManager] Failed to delete assistant memory with ID ${id}`)
}
} catch (error) {
console.error('[AssistantMemoryManager] Failed to delete assistant memory:', error)
}
}, 500),
[dispatch]
)
return (
<div className="assistant-memory-manager">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Title level={4}>{t('settings.memory.assistantMemory') || '助手记忆'}</Title>
<Tooltip title={t('settings.memory.toggleAssistantMemoryActive') || '切换助手记忆功能'}>
<Switch checked={assistantMemoryActive} onChange={handleToggleActive} />
</Tooltip>
</div>
{/* 助手选择器 */}
<div style={{ marginBottom: 16 }}>
<Select
value={selectedAssistantId}
onChange={setSelectedAssistantId}
placeholder={t('settings.memory.selectAssistant') || '选择助手'}
style={{ width: '100%', marginBottom: 16 }}
disabled={!assistantMemoryActive}
>
{assistants.map((assistant) => (
<Select.Option key={assistant.id} value={assistant.id}>
{assistant.name}
</Select.Option>
))}
</Select>
</div>
<div style={{ marginBottom: 16 }}>
<Input.TextArea
value={newMemoryContent}
onChange={(e) => setNewMemoryContent(e.target.value)}
placeholder={t('settings.memory.addAssistantMemoryPlaceholder') || '添加助手记忆...'}
autoSize={{ minRows: 2, maxRows: 4 }}
disabled={!assistantMemoryActive || !selectedAssistantId}
/>
<Button
type="primary"
onClick={() => handleAddMemory()}
style={{ marginTop: 8 }}
disabled={!assistantMemoryActive || !newMemoryContent.trim() || !selectedAssistantId}>
{t('settings.memory.addAssistantMemory') || '添加助手记忆'}
</Button>
</div>
<div className="assistant-memories-list">
{assistantMemories.length > 0 ? (
<List
itemLayout="horizontal"
dataSource={assistantMemories}
renderItem={(memory) => (
<List.Item
actions={[
<Tooltip title={t('settings.memory.delete') || '删除'} key="delete">
<Button
icon={<DeleteOutlined />}
onClick={() => handleDeleteMemory(memory.id)}
type="text"
danger
/>
</Tooltip>
]}>
<List.Item.Meta
title={<div style={{ wordBreak: 'break-word' }}>{memory.content}</div>}
description={new Date(memory.createdAt).toLocaleString()}
/>
</List.Item>
)}
/>
) : (
<Empty
description={!selectedAssistantId ? t('settings.memory.selectAssistantFirst') || '请先选择助手' : t('settings.memory.noAssistantMemories') || '无助手记忆'}
/>
)}
</div>
</div>
)
}
export default AssistantMemoryManager

View File

@ -17,6 +17,7 @@ import {
} from '@renderer/services/MemoryService' } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import store from '@renderer/store' // Import store for direct access import store from '@renderer/store' // Import store for direct access
import { getModelUniqId } from '@renderer/utils'
import { import {
addMemory, addMemory,
clearMemories, clearMemories,
@ -27,9 +28,12 @@ import {
saveMemoryData, saveMemoryData,
setAnalyzeModel, setAnalyzeModel,
setAnalyzing, setAnalyzing,
setAssistantMemoryActive,
setAssistantMemoryAnalyzeModel,
setAutoAnalyze, setAutoAnalyze,
setFilterSensitiveInfo, setFilterSensitiveInfo,
setMemoryActive, setMemoryActive,
setShortMemoryActive,
setShortMemoryAnalyzeModel setShortMemoryAnalyzeModel
} from '@renderer/store/memory' } from '@renderer/store/memory'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
@ -47,6 +51,7 @@ import {
SettingRowTitle, SettingRowTitle,
SettingTitle SettingTitle
} from '..' } from '..'
import AssistantMemoryManager from './AssistantMemoryManager'
import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager' import CollapsibleShortMemoryManager from './CollapsibleShortMemoryManager'
import ContextualRecommendationSettings from './ContextualRecommendationSettings' import ContextualRecommendationSettings from './ContextualRecommendationSettings'
import HistoricalContextSettings from './HistoricalContextSettings' import HistoricalContextSettings from './HistoricalContextSettings'
@ -66,10 +71,13 @@ const MemorySettings: FC = () => {
const memoryLists = useAppSelector((state) => state.memory?.memoryLists || []) const memoryLists = useAppSelector((state) => state.memory?.memoryLists || [])
const currentListId = useAppSelector((state) => state.memory?.currentListId || null) const currentListId = useAppSelector((state) => state.memory?.currentListId || null)
const isActive = useAppSelector((state) => state.memory?.isActive || false) const isActive = useAppSelector((state) => state.memory?.isActive || false)
const shortMemoryActive = useAppSelector((state) => state.memory?.shortMemoryActive || false)
const assistantMemoryActive = useAppSelector((state) => state.memory?.assistantMemoryActive || false)
const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false) const autoAnalyze = useAppSelector((state) => state.memory?.autoAnalyze || false)
const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤 const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤
const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null) const analyzeModel = useAppSelector((state) => state.memory?.analyzeModel || null)
const shortMemoryAnalyzeModel = useAppSelector((state) => state.memory?.shortMemoryAnalyzeModel || null) const shortMemoryAnalyzeModel = useAppSelector((state) => state.memory?.shortMemoryAnalyzeModel || null)
const assistantMemoryAnalyzeModel = useAppSelector((state) => state.memory?.assistantMemoryAnalyzeModel || null)
const isAnalyzing = useAppSelector((state) => state.memory?.isAnalyzing || false) const isAnalyzing = useAppSelector((state) => state.memory?.isAnalyzing || false)
// 从 Redux 获取所有模型,不仅仅是可用的模型 // 从 Redux 获取所有模型,不仅仅是可用的模型
@ -256,6 +264,16 @@ const MemorySettings: FC = () => {
dispatch(setMemoryActive(checked)) dispatch(setMemoryActive(checked))
} }
// 处理切换短期记忆功能
const handleToggleShortMemory = (checked: boolean) => {
dispatch(setShortMemoryActive(checked))
}
// 处理切换助手记忆功能
const handleToggleAssistantMemory = (checked: boolean) => {
dispatch(setAssistantMemoryActive(checked))
}
// 处理切换自动分析 // 处理切换自动分析
const handleToggleAutoAnalyze = (checked: boolean) => { const handleToggleAutoAnalyze = (checked: boolean) => {
dispatch(setAutoAnalyze(checked)) dispatch(setAutoAnalyze(checked))
@ -276,33 +294,53 @@ const MemorySettings: FC = () => {
} }
// 处理选择长期记忆分析模型 // 处理选择长期记忆分析模型
const handleSelectModel = async (modelId: string) => { const handleSelectModel = async (model: any) => {
dispatch(setAnalyzeModel(modelId)) // 保存完整的模型信息,包含供应商
console.log('[Memory Settings] Analyze model set:', modelId) const modelUniqId = getModelUniqId(model)
dispatch(setAnalyzeModel(modelUniqId))
console.log('[Memory Settings] Analyze model set:', modelUniqId, 'Provider:', model.provider)
// 使用Redux Thunk保存到JSON文件 // 使用Redux Thunk保存到JSON文件
try { try {
await dispatch(saveMemoryData({ analyzeModel: modelId })).unwrap() await dispatch(saveMemoryData({ analyzeModel: modelUniqId })).unwrap()
console.log('[Memory Settings] Analyze model saved to file successfully:', modelId) console.log('[Memory Settings] Analyze model saved to file successfully:', modelUniqId)
} catch (error) { } catch (error) {
console.error('[Memory Settings] Failed to save analyze model to file:', error) console.error('[Memory Settings] Failed to save analyze model to file:', error)
} }
} }
// 处理选择短期记忆分析模型 // 处理选择短期记忆分析模型
const handleSelectShortMemoryModel = async (modelId: string) => { const handleSelectShortMemoryModel = async (model: any) => {
dispatch(setShortMemoryAnalyzeModel(modelId)) // 保存完整的模型信息,包含供应商
console.log('[Memory Settings] Short memory analyze model set:', modelId) const modelUniqId = getModelUniqId(model)
dispatch(setShortMemoryAnalyzeModel(modelUniqId))
console.log('[Memory Settings] Short memory analyze model set:', modelUniqId, 'Provider:', model.provider)
// 使用Redux Thunk保存到JSON文件 // 使用Redux Thunk保存到JSON文件
try { try {
await dispatch(saveMemoryData({ shortMemoryAnalyzeModel: modelId })).unwrap() await dispatch(saveMemoryData({ shortMemoryAnalyzeModel: modelUniqId })).unwrap()
console.log('[Memory Settings] Short memory analyze model saved to file successfully:', modelId) console.log('[Memory Settings] Short memory analyze model saved to file successfully:', modelUniqId)
} catch (error) { } catch (error) {
console.error('[Memory Settings] Failed to save short memory analyze model to file:', error) console.error('[Memory Settings] Failed to save short memory analyze model to file:', error)
} }
} }
// 处理选择助手记忆分析模型
const handleSelectAssistantMemoryModel = async (model: any) => {
// 保存完整的模型信息,包含供应商
const modelUniqId = getModelUniqId(model)
dispatch(setAssistantMemoryAnalyzeModel(modelUniqId))
console.log('[Memory Settings] Assistant memory analyze model set:', modelUniqId, 'Provider:', model.provider)
// 使用Redux Thunk保存到JSON文件
try {
await dispatch(saveMemoryData({ assistantMemoryAnalyzeModel: modelUniqId })).unwrap()
console.log('[Memory Settings] Assistant memory analyze model saved to file successfully:', modelUniqId)
} catch (error) {
console.error('[Memory Settings] Failed to save assistant memory analyze model to file:', error)
}
}
// 手动触发分析 // 手动触发分析
const handleManualAnalyze = async (isShortMemory: boolean = false) => { const handleManualAnalyze = async (isShortMemory: boolean = false) => {
if (!isActive) { if (!isActive) {
@ -360,13 +398,38 @@ const MemorySettings: FC = () => {
const getSelectedModelName = () => { const getSelectedModelName = () => {
if (!analyzeModel) return '' if (!analyzeModel) return ''
// 遍历所有服务商的模型找到匹配的模型 try {
// 尝试解析JSON格式的模型ID
let modelId = analyzeModel
if (typeof analyzeModel === 'string' && analyzeModel.startsWith('{')) {
const parsedModel = JSON.parse(analyzeModel)
modelId = parsedModel.id
// 遍历所有服务商的模型找到匹配的模型和供应商
for (const provider of Object.values(providers)) { for (const provider of Object.values(providers)) {
const model = provider.models.find((m) => m.id === analyzeModel) if (provider.id === parsedModel.provider) {
const model = provider.models.find((m) => m.id === modelId)
if (model) { if (model) {
return `${model.name} | ${provider.name}` return `${model.name} | ${provider.name}`
} }
} }
}
// 如果没找到匹配的模型返回模型ID和供应商ID
return `${modelId} | ${parsedModel.provider}`
} else {
// 兼容旧格式直接根据ID查找
for (const provider of Object.values(providers)) {
const model = provider.models.find((m) => m.id === modelId)
if (model) {
return `${model.name} | ${provider.name}`
}
}
}
} catch (error) {
console.error('Error parsing model ID:', error)
}
return analyzeModel return analyzeModel
} }
@ -375,17 +438,87 @@ const MemorySettings: FC = () => {
const getSelectedShortMemoryModelName = () => { const getSelectedShortMemoryModelName = () => {
if (!shortMemoryAnalyzeModel) return '' if (!shortMemoryAnalyzeModel) return ''
// 遍历所有服务商的模型找到匹配的模型 try {
// 尝试解析JSON格式的模型ID
let modelId = shortMemoryAnalyzeModel
if (typeof shortMemoryAnalyzeModel === 'string' && shortMemoryAnalyzeModel.startsWith('{')) {
const parsedModel = JSON.parse(shortMemoryAnalyzeModel)
modelId = parsedModel.id
// 遍历所有服务商的模型找到匹配的模型和供应商
for (const provider of Object.values(providers)) { for (const provider of Object.values(providers)) {
const model = provider.models.find((m) => m.id === shortMemoryAnalyzeModel) if (provider.id === parsedModel.provider) {
const model = provider.models.find((m) => m.id === modelId)
if (model) { if (model) {
return `${model.name} | ${provider.name}` return `${model.name} | ${provider.name}`
} }
} }
}
// 如果没找到匹配的模型返回模型ID和供应商ID
return `${modelId} | ${parsedModel.provider}`
} else {
// 兼容旧格式直接根据ID查找
for (const provider of Object.values(providers)) {
const model = provider.models.find((m) => m.id === modelId)
if (model) {
return `${model.name} | ${provider.name}`
}
}
}
} catch (error) {
console.error('Error parsing short memory model ID:', error)
}
return shortMemoryAnalyzeModel return shortMemoryAnalyzeModel
} }
// 获取当前选中助手记忆模型的名称
const getSelectedAssistantMemoryModelName = () => {
if (!assistantMemoryAnalyzeModel) return ''
try {
// 尝试解析JSON格式的模型ID
let modelId = assistantMemoryAnalyzeModel
if (typeof assistantMemoryAnalyzeModel === 'string' && assistantMemoryAnalyzeModel.startsWith('{')) {
const parsedModel = JSON.parse(assistantMemoryAnalyzeModel)
modelId = parsedModel.id
// 遍历所有服务商的模型找到匹配的模型和供应商
for (const provider of Object.values(providers)) {
if (provider.id === parsedModel.provider) {
const model = provider.models.find((m) => m.id === modelId)
if (model) {
return `${model.name} | ${provider.name}`
}
}
}
// 如果没找到匹配的模型返回模型ID和供应商ID
return `${modelId} | ${parsedModel.provider}`
} else {
// 兼容旧格式直接根据ID查找
for (const provider of Object.values(providers)) {
const model = provider.models.find((m) => m.id === modelId)
if (model) {
return `${model.name} | ${provider.name}`
}
}
}
} catch (error) {
console.error('Error parsing assistant memory model ID:', error)
}
return assistantMemoryAnalyzeModel
}
// 获取模型的完整ID包含供应商信息
const getModelUniqId = (model: any) => {
return JSON.stringify({ id: model.id, provider: model.provider })
}
// 重置长期记忆分析标记 // 重置长期记忆分析标记
const handleResetLongTermMemoryAnalyzedMessageIds = async () => { const handleResetLongTermMemoryAnalyzedMessageIds = async () => {
if (!selectedTopicId) { if (!selectedTopicId) {
@ -544,6 +677,90 @@ const MemorySettings: FC = () => {
size="large" size="large"
animated={{ inkBar: true, tabPane: true }} animated={{ inkBar: true, tabPane: true }}
items={[ items={[
{
key: 'assistantMemory',
label: (
<TabLabelContainer>
<TabDot color="#f5222d"></TabDot>
{t('settings.memory.assistantMemory') || '助手记忆'}
</TabLabelContainer>
),
children: (
<TabPaneSettingGroup theme={theme}>
<SettingTitle>{t('settings.memory.title')}</SettingTitle>
<SettingHelpText>{t('settings.memory.description')}</SettingHelpText>
<SettingDivider />
<SettingTitle>{t('settings.memory.assistantMemorySettings') || '助手记忆设置'}</SettingTitle>
<SettingHelpText>{t('settings.memory.assistantMemoryDescription') || '助手记忆是与特定助手关联的记忆,可以帮助助手记住重要信息。'}</SettingHelpText>
<SettingDivider />
{/* 助手记忆设置 */}
<SettingRow>
<SettingRowTitle>{t('settings.memory.enableAssistantMemory') || '启用助手记忆'}</SettingRowTitle>
<Switch checked={assistantMemoryActive} onChange={handleToggleAssistantMemory} />
</SettingRow>
<SettingRow>
<SettingRowTitle>{t('settings.memory.enableAutoAnalyze')}</SettingRowTitle>
<Switch checked={autoAnalyze} onChange={handleToggleAutoAnalyze} disabled={!isActive} />
</SettingRow>
<SettingRow>
<SettingRowTitle>
{t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'}
<Tooltip
title={
t('settings.memory.filterSensitiveInfoTip') ||
'启用后记忆功能将不会提取API密钥、密码等敏感信息'
}>
<InfoCircleOutlined style={{ marginLeft: 8 }} />
</Tooltip>
</SettingRowTitle>
<Switch
checked={filterSensitiveInfo}
onChange={handleToggleFilterSensitiveInfo}
disabled={!isActive}
/>
</SettingRow>
{/* 助手记忆分析模型选择 */}
<SettingRow>
<SettingRowTitle>
{t('settings.memory.assistantMemoryAnalyzeModel') || '助手记忆分析模型'}
</SettingRowTitle>
<Button
onClick={async () => {
// 找到当前选中的模型对象
let currentModel: { id: string; provider: string; name: string; group: string } | undefined
if (assistantMemoryAnalyzeModel) {
for (const provider of Object.values(providers)) {
const model = provider.models.find((m) => m.id === assistantMemoryAnalyzeModel)
if (model) {
currentModel = model
break
}
}
}
const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) {
handleSelectAssistantMemoryModel(selectedModel)
}
}}
style={{ width: 300 }}
disabled={!isActive}>
{assistantMemoryAnalyzeModel
? getSelectedAssistantMemoryModelName()
: t('settings.memory.selectModel') || '选择模型'}
</Button>
</SettingRow>
<SettingDivider />
{/* 助手记忆管理器 */}
<AssistantMemoryManager />
</TabPaneSettingGroup>
)
},
{ {
key: 'shortMemory', key: 'shortMemory',
label: ( label: (
@ -563,10 +780,10 @@ const MemorySettings: FC = () => {
<SettingHelpText>{t('settings.memory.shortMemoryDescription')}</SettingHelpText> <SettingHelpText>{t('settings.memory.shortMemoryDescription')}</SettingHelpText>
<SettingDivider /> <SettingDivider />
{/* 保留原有的短期记忆设置 */} {/* 短期记忆设置 */}
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.memory.enableMemory')}</SettingRowTitle> <SettingRowTitle>{t('settings.memory.enableShortMemory') || '启用短期记忆'}</SettingRowTitle>
<Switch checked={isActive} onChange={handleToggleMemory} /> <Switch checked={shortMemoryActive} onChange={handleToggleShortMemory} />
</SettingRow> </SettingRow>
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.memory.enableAutoAnalyze')}</SettingRowTitle> <SettingRowTitle>{t('settings.memory.enableAutoAnalyze')}</SettingRowTitle>
@ -611,7 +828,7 @@ const MemorySettings: FC = () => {
const selectedModel = await SelectModelPopup.show({ model: currentModel }) const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) { if (selectedModel) {
handleSelectShortMemoryModel(selectedModel.id) handleSelectShortMemoryModel(selectedModel)
} }
}} }}
style={{ width: 300 }} style={{ width: 300 }}
@ -738,7 +955,7 @@ const MemorySettings: FC = () => {
<SettingHelpText>{t('settings.memory.longMemoryDescription')}</SettingHelpText> <SettingHelpText>{t('settings.memory.longMemoryDescription')}</SettingHelpText>
<SettingDivider /> <SettingDivider />
{/* 保留原有的长期记忆设置 */} {/* 长期记忆设置 */}
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.memory.enableMemory')}</SettingRowTitle> <SettingRowTitle>{t('settings.memory.enableMemory')}</SettingRowTitle>
<Switch checked={isActive} onChange={handleToggleMemory} /> <Switch checked={isActive} onChange={handleToggleMemory} />
@ -784,7 +1001,7 @@ const MemorySettings: FC = () => {
const selectedModel = await SelectModelPopup.show({ model: currentModel }) const selectedModel = await SelectModelPopup.show({ model: currentModel })
if (selectedModel) { if (selectedModel) {
handleSelectModel(selectedModel.id) handleSelectModel(selectedModel)
} }
}} }}
style={{ width: 300 }} style={{ width: 300 }}

View File

@ -0,0 +1,823 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAppDispatch } from '@renderer/store'
import { Button, Form, Input, Modal, Select, Switch, Tabs, message } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { useProviders } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { ThinkingLibrary, createDeepClaudeProvider, checkModelCombinationsInLocalStorage } from '@renderer/utils/createDeepClaudeProvider'
import { addProvider, removeProvider } from '@renderer/store/llm'
import { getThinkingLibraries, addThinkingLibrary, updateThinkingLibrary, debugThinkingLibraries, saveThinkingLibraries, DEFAULT_THINKING_LIBRARIES } from '@renderer/utils/thinkingLibrary'
// 模型组合类型
interface ModelCombination {
id: string
name: string
reasonerModel: Model | null
targetModel: Model | null
isActive: boolean
thinkingLibraryId?: string
}
const ModelCombinationSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { providers } = useProviders()
// 从本地存储获取模型组合列表
const [combinations, setCombinations] = useState<ModelCombination[]>([])
const [isModalVisible, setIsModalVisible] = useState(false)
const [editingCombination, setEditingCombination] = useState<ModelCombination | null>(null)
const [form] = Form.useForm()
const [libraryForm] = Form.useForm()
const [thinkingLibraries, setThinkingLibraries] = useState<ThinkingLibrary[]>([])
const [isLibraryModalVisible, setIsLibraryModalVisible] = useState(false)
const [editingLibrary, setEditingLibrary] = useState<ThinkingLibrary | null>(null)
const [activeTab, setActiveTab] = useState('combinations')
// 获取所有可用的模型
const allModels = providers.flatMap(provider =>
provider.models.map(model => ({
...model,
providerName: provider.name
}))
)
// 根据ID查找模型
const findModelById = (id: string): Model | null => {
for (const provider of providers) {
const model = provider.models.find(m => m.id === id)
if (model) return model
}
return null
}
// 初始化时加载思考库
useEffect(() => {
console.log('[ModelCombinationSettings] 加载思考库')
const libraries = getThinkingLibraries()
console.log('[ModelCombinationSettings] 获取到思考库数量:', libraries.length)
setThinkingLibraries(libraries)
}, [])
// 每次切换到思考库标签页时重新加载
useEffect(() => {
if (activeTab === 'libraries') {
console.log('[ModelCombinationSettings] 切换到思考库标签页,重新加载思考库')
const libraries = getThinkingLibraries()
console.log('[ModelCombinationSettings] 重新加载思考库数量:', libraries.length)
setThinkingLibraries(libraries)
}
}, [activeTab])
// 初始化时从localStorage加载模型组合
useEffect(() => {
const savedCombinations = localStorage.getItem('modelCombinations')
if (savedCombinations) {
try {
const parsed = JSON.parse(savedCombinations)
// 确保reasonerModel和targetModel是完整的模型对象
const restoredCombinations = parsed.map((comb: any) => ({
...comb,
reasonerModel: comb.reasonerModel ? findModelById(comb.reasonerModel.id) : null,
targetModel: comb.targetModel ? findModelById(comb.targetModel.id) : null
}))
setCombinations(restoredCombinations)
} catch (e) {
console.error('Failed to parse saved model combinations:', e)
}
}
}, [providers])
// 单独的useEffect来处理DeepClaude提供商的更新
useEffect(() => {
// 使用延迟来确保所有模型都已加载
const timer = setTimeout(() => {
if (combinations.length > 0 && providers.length > 0) {
// 只有当combinations和providers都有值时才更新DeepClaude提供商
console.log('[ModelCombinationSettings] 更新DeepClaude提供商', combinations.length, providers.length)
updateDeepClaudeProviders(combinations)
}
}, 500) // 等待500ms确保所有状态都已更新
return () => clearTimeout(timer) // 清理定时器
}, [combinations.length, providers.length])
// 保存模型组合到localStorage
const saveCombinations = (newCombinations: ModelCombination[]) => {
console.log('[ModelCombinationSettings] 保存模型组合:',
newCombinations.map(c => ({
id: c.id,
name: c.name,
reasonerModel: {
id: c.reasonerModel?.id,
name: c.reasonerModel?.name,
provider: c.reasonerModel?.provider
},
targetModel: {
id: c.targetModel?.id,
name: c.targetModel?.name,
provider: c.targetModel?.provider
},
isActive: c.isActive
})))
// 确保模型组合中的模型对象是完整的
const combinationsToSave = newCombinations.map(c => ({
id: c.id,
name: c.name,
reasonerModel: c.reasonerModel ? {
id: c.reasonerModel.id,
name: c.reasonerModel.name,
provider: c.reasonerModel.provider,
group: c.reasonerModel.group,
type: c.reasonerModel.type
} : null,
targetModel: c.targetModel ? {
id: c.targetModel.id,
name: c.targetModel.name,
provider: c.targetModel.provider,
group: c.targetModel.group,
type: c.targetModel.type
} : null,
isActive: c.isActive
}))
localStorage.setItem('modelCombinations', JSON.stringify(combinationsToSave))
console.log('[ModelCombinationSettings] 已保存模型组合到localStorage')
checkModelCombinationsInLocalStorage() // 检查保存的数据
setCombinations(newCombinations)
// 更新DeepClaude提供商
updateDeepClaudeProviders(newCombinations)
}
// 更新DeepClaude提供商
const updateDeepClaudeProviders = (combinations: ModelCombination[]) => {
// 使用setTimeout来避免在渲染周期内进行多次状态更新
setTimeout(() => {
// 移除所有现有的DeepClaude提供商
const existingDeepClaudeProviders = providers.filter(p => p.type === 'deepclaude')
console.log('[ModelCombinationSettings] 移除现有DeepClaude提供商数量:', existingDeepClaudeProviders.length)
existingDeepClaudeProviders.forEach(provider => {
dispatch(removeProvider(provider))
})
// 创建并添加新的DeepClaude提供商
const activeCombinations = combinations.filter(c => c.isActive && c.reasonerModel && c.targetModel)
console.log('[ModelCombinationSettings] 激活的模型组合数量:', activeCombinations.length)
console.log('[ModelCombinationSettings] 激活的模型组合详情:',
activeCombinations.map(c => ({
id: c.id,
name: c.name,
reasonerModel: {
id: c.reasonerModel?.id,
name: c.reasonerModel?.name,
provider: c.reasonerModel?.provider
},
targetModel: {
id: c.targetModel?.id,
name: c.targetModel?.name,
provider: c.targetModel?.provider
}
})))
if (activeCombinations.length > 0) {
// 创建一个单一的DeepClaude提供商包含所有激活的模型组合
const provider = createDeepClaudeProvider(activeCombinations)
console.log('[ModelCombinationSettings] 创建的DeepClaude提供商:',
provider.id, provider.name, provider.type,
provider.models.map(m => ({ id: m.id, name: m.name, provider: m.provider })))
dispatch(addProvider(provider))
}
}, 0)
}
// 添加或编辑模型组合
const handleAddOrEditCombination = (values: any) => {
const { name, reasonerModelId, targetModelId, isActive, thinkingLibraryId } = values
const reasonerModel = findModelById(reasonerModelId)
const targetModel = findModelById(targetModelId)
if (!reasonerModel || !targetModel) {
message.error(t('settings.modelCombination.modelNotFound'))
return
}
if (editingCombination) {
// 编辑现有组合
const updatedCombinations = combinations.map(comb =>
comb.id === editingCombination.id
? { ...comb, name, reasonerModel, targetModel, isActive: isActive !== false, thinkingLibraryId }
: comb
)
saveCombinations(updatedCombinations)
message.success(t('settings.modelCombination.updateSuccess'))
} else {
// 添加新组合
const newCombination: ModelCombination = {
id: uuid(),
name,
reasonerModel,
targetModel,
isActive: isActive !== false,
thinkingLibraryId
}
saveCombinations([...combinations, newCombination])
message.success(t('settings.modelCombination.addSuccess'))
}
setIsModalVisible(false)
setEditingCombination(null)
form.resetFields()
}
// 删除模型组合
const handleDeleteCombination = (id: string) => {
Modal.confirm({
title: t('settings.modelCombination.confirmDelete'),
content: t('settings.modelCombination.confirmDeleteContent'),
onOk: () => {
const updatedCombinations = combinations.filter(comb => comb.id !== id)
saveCombinations(updatedCombinations)
message.success(t('settings.modelCombination.deleteSuccess'))
}
})
}
// 编辑模型组合
const handleEditCombination = (combination: ModelCombination) => {
setEditingCombination(combination)
form.setFieldsValue({
name: combination.name,
reasonerModelId: combination.reasonerModel?.id,
targetModelId: combination.targetModel?.id,
isActive: combination.isActive,
thinkingLibraryId: combination.thinkingLibraryId
})
setIsModalVisible(true)
}
// 切换模型组合的激活状态
const toggleCombinationActive = (id: string, isActive: boolean) => {
const updatedCombinations = combinations.map(comb =>
comb.id === id ? { ...comb, isActive } : comb
)
saveCombinations(updatedCombinations)
}
// 添加或编辑思考库
const handleAddOrEditLibrary = (values: any) => {
const { name, description, category, prompt } = values
console.log('[ModelCombinationSettings] 添加/编辑思考库:', name)
if (editingLibrary) {
// 编辑现有思考库
const updatedLibrary: ThinkingLibrary = {
...editingLibrary,
name,
description,
category,
prompt
}
console.log('[ModelCombinationSettings] 更新思考库:', updatedLibrary.id)
updateThinkingLibrary(updatedLibrary)
// 重新加载思考库列表
const updatedLibraries = getThinkingLibraries()
console.log('[ModelCombinationSettings] 更新后思考库数量:', updatedLibraries.length)
setThinkingLibraries(updatedLibraries)
message.success(t('settings.thinkingLibrary.updateSuccess'))
} else {
// 添加新思考库
console.log('[ModelCombinationSettings] 添加新思考库:', name, category)
try {
// 先清除缓存,确保获取最新数据
const currentLibraries = getThinkingLibraries()
console.log('[ModelCombinationSettings] 当前思考库数量:', currentLibraries.length)
// 添加新思考库
const newLibrary = addThinkingLibrary({
name,
description,
category,
prompt
})
console.log('[ModelCombinationSettings] 新思考库已添加:', newLibrary.id)
// 直接构造新的思考库数组,而不是从 localStorage 重新加载
const updatedLibraries = [...currentLibraries, newLibrary]
console.log('[ModelCombinationSettings] 添加后思考库数量:', updatedLibraries.length)
// 强制更新状态
setThinkingLibraries(updatedLibraries)
// 调用调试函数查看存储状态
debugThinkingLibraries()
message.success(t('settings.thinkingLibrary.addSuccess'))
} catch (e) {
console.error('[ModelCombinationSettings] 添加思考库失败:', e)
message.error('添加思考库失败,请查看控制台日志')
}
}
// 关闭模态框
setIsLibraryModalVisible(false)
setEditingLibrary(null)
libraryForm.resetFields()
}
// 删除思考库
const handleDeleteLibrary = (id: string) => {
Modal.confirm({
title: t('settings.thinkingLibrary.confirmDelete'),
content: t('settings.thinkingLibrary.confirmDeleteContent'),
onOk: () => {
try {
console.log('[ModelCombinationSettings] 删除思考库:', id)
// 先获取当前思考库列表
const currentLibraries = getThinkingLibraries()
console.log('[ModelCombinationSettings] 当前思考库数量:', currentLibraries.length)
// 直接在内存中过滤要删除的思考库
const filteredLibraries = currentLibraries.filter(lib => lib.id !== id)
console.log('[ModelCombinationSettings] 过滤后思考库数量:', filteredLibraries.length)
// 保存到localStorage
saveThinkingLibraries(filteredLibraries)
// 强制更新状态
setThinkingLibraries([...filteredLibraries])
// 调用调试函数查看存储状态
debugThinkingLibraries()
message.success(t('settings.thinkingLibrary.deleteSuccess'))
} catch (e) {
console.error('[ModelCombinationSettings] 删除思考库失败:', e)
message.error('删除思考库失败,请查看控制台日志')
}
}
})
}
// 编辑思考库
const handleEditLibrary = (library: ThinkingLibrary) => {
setEditingLibrary(library)
libraryForm.setFieldsValue({
name: library.name,
description: library.description,
category: library.category,
prompt: library.prompt
})
setIsLibraryModalVisible(true)
}
return (
<SettingContainer theme={theme}>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{
key: 'combinations',
label: t('settings.modelCombination.title'),
children: (
<SettingGroup theme={theme}>
<SettingTitle>
{t('settings.modelCombination.title')}
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingCombination(null)
form.resetFields()
setIsModalVisible(true)
}}
>
{t('settings.modelCombination.add')}
</Button>
</SettingTitle>
<SettingDivider />
{combinations.length === 0 ? (
<EmptyState>{t('settings.modelCombination.empty')}</EmptyState>
) : (
<CombinationList>
{combinations.map(combination => (
<CombinationItem key={combination.id}>
<CombinationInfo>
<CombinationName>{combination.name}</CombinationName>
<CombinationDetail>
{t('settings.modelCombination.reasoner')}: {combination.reasonerModel?.name || t('settings.modelCombination.notSelected')}
</CombinationDetail>
<CombinationDetail>
{t('settings.modelCombination.target')}: {combination.targetModel?.name || t('settings.modelCombination.notSelected')}
</CombinationDetail>
</CombinationInfo>
<CombinationActions>
<Switch
checked={combination.isActive}
onChange={(checked) => toggleCombinationActive(combination.id, checked)}
/>
<Button
icon={<EditOutlined />}
type="text"
onClick={() => handleEditCombination(combination)}
/>
<Button
icon={<DeleteOutlined />}
type="text"
danger
onClick={() => handleDeleteCombination(combination.id)}
/>
</CombinationActions>
</CombinationItem>
))}
</CombinationList>
)}
</SettingGroup>
)
},
{
key: 'libraries',
label: t('settings.thinkingLibrary.title'),
children: (
<SettingGroup theme={theme}>
<SettingTitle>
{t('settings.thinkingLibrary.title')}
<ButtonGroup>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingLibrary(null)
libraryForm.resetFields()
setIsLibraryModalVisible(true)
}}
>
{t('settings.thinkingLibrary.add')}
</Button>
<Button
onClick={() => {
Modal.confirm({
title: '重置思考库',
content: '确定要重置思考库吗?这将删除所有自定义思考库,并恢复默认思考库。',
onOk: () => {
try {
console.log('[ModelCombinationSettings] 重置思考库')
// 删除localStorage中的思考库数据
localStorage.removeItem('thinkingLibraries')
// 重新加载默认思考库
const defaultLibraries = getThinkingLibraries() // 这将返回默认思考库
console.log('[ModelCombinationSettings] 默认思考库数量:', defaultLibraries.length)
// 更新状态
setThinkingLibraries([...defaultLibraries])
// 调用调试函数查看存储状态
debugThinkingLibraries()
message.success('思考库已重置为默认状态')
} catch (e) {
console.error('[ModelCombinationSettings] 重置思考库失败:', e)
message.error('重置思考库失败,请查看控制台日志')
}
}
})
}}
>
</Button>
<Button
onClick={() => {
// 调用调试函数,在控制台显示思考库数据
debugThinkingLibraries()
message.info('思考库调试信息已输出到控制台请按F12查看')
}}
>
</Button>
<Button
onClick={() => {
Modal.confirm({
title: '强制更新思考库',
content: '确定要强制更新思考库吗?这将保留现有思考库,并添加缺失的默认思考库。',
onOk: () => {
try {
console.log('[ModelCombinationSettings] 强制更新思考库')
// 获取当前思考库
const currentLibraries = getThinkingLibraries()
console.log('[ModelCombinationSettings] 当前思考库数量:', currentLibraries.length)
// 获取默认思考库中缺失的思考库
const existingIds = new Set(currentLibraries.map(lib => lib.id))
const missingLibraries = DEFAULT_THINKING_LIBRARIES.filter((lib: ThinkingLibrary) => !existingIds.has(lib.id))
console.log('[ModelCombinationSettings] 缺失的默认思考库数量:', missingLibraries.length)
if (missingLibraries.length > 0) {
// 合并思考库
const updatedLibraries = [...currentLibraries, ...missingLibraries]
console.log('[ModelCombinationSettings] 更新后思考库数量:', updatedLibraries.length)
// 保存到localStorage
saveThinkingLibraries(updatedLibraries)
// 更新状态
setThinkingLibraries([...updatedLibraries])
// 调用调试函数查看存储状态
debugThinkingLibraries()
message.success(`思考库已更新,添加了${missingLibraries.length}个缺失的默认思考库`)
} else {
message.info('所有默认思考库已存在,无需更新')
}
} catch (e) {
console.error('[ModelCombinationSettings] 强制更新思考库失败:', e)
message.error('强制更新思考库失败,请查看控制台日志')
}
}
})
}}
>
</Button>
</ButtonGroup>
</SettingTitle>
<SettingDivider />
{thinkingLibraries.length === 0 ? (
<EmptyState>{t('settings.thinkingLibrary.empty')}</EmptyState>
) : (
<CombinationList>
{thinkingLibraries.map(library => (
<CombinationItem key={library.id}>
<CombinationInfo>
<CombinationName>{library.name}</CombinationName>
<CombinationDetail>
{t('settings.thinkingLibrary.category')}: {library.category}
</CombinationDetail>
<CombinationDetail>
{t('settings.thinkingLibrary.description')}: {library.description}
</CombinationDetail>
</CombinationInfo>
<CombinationActions>
<Button
icon={<EditOutlined />}
type="text"
onClick={() => handleEditLibrary(library)}
/>
<Button
icon={<DeleteOutlined />}
type="text"
danger
onClick={() => handleDeleteLibrary(library.id)}
/>
</CombinationActions>
</CombinationItem>
))}
</CombinationList>
)}
</SettingGroup>
)
}
]}
/>
{/* 添加/编辑模型组合的模态框 */}
<Modal
title={editingCombination
? t('settings.modelCombination.editTitle')
: t('settings.modelCombination.addTitle')
}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false)
setEditingCombination(null)
form.resetFields()
}}
footer={null}
>
<Form
form={form}
layout="vertical"
onFinish={handleAddOrEditCombination}
>
<Form.Item
name="name"
label={t('settings.modelCombination.name')}
rules={[{ required: true, message: t('settings.modelCombination.nameRequired') }]}
>
<Input placeholder={t('settings.modelCombination.namePlaceholder')} />
</Form.Item>
<Form.Item
name="reasonerModelId"
label={t('settings.modelCombination.reasonerModel')}
rules={[{ required: true, message: t('settings.modelCombination.reasonerModelRequired') }]}
>
<Select
placeholder={t('settings.modelCombination.selectModel')}
showSearch
optionFilterProp="label"
>
{allModels.map(model => (
<Select.Option
key={model.id}
value={model.id}
label={`${model.name} (${model.providerName})`}
>
{model.name} ({model.providerName})
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="targetModelId"
label={t('settings.modelCombination.targetModel')}
rules={[{ required: true, message: t('settings.modelCombination.targetModelRequired') }]}
>
<Select
placeholder={t('settings.modelCombination.selectModel')}
showSearch
optionFilterProp="label"
>
{allModels.map(model => (
<Select.Option
key={model.id}
value={model.id}
label={`${model.name} (${model.providerName})`}
>
{model.name} ({model.providerName})
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="thinkingLibraryId"
label="思考库"
>
<Select
placeholder="选择思考库(可选)"
allowClear
>
{thinkingLibraries.map(library => (
<Select.Option
key={library.id}
value={library.id}
label={`${library.name} (${library.category})`}
>
{library.name} ({library.category})
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="isActive"
valuePropName="checked"
initialValue={true}
>
<Switch checkedChildren={t('common.enabled')} unCheckedChildren={t('common.disabled')} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{editingCombination ? t('common.save') : t('common.add')}
</Button>
</Form.Item>
</Form>
</Modal>
{/* 添加/编辑思考库的模态框 */}
<Modal
title={editingLibrary
? t('settings.thinkingLibrary.editTitle')
: t('settings.thinkingLibrary.addTitle')
}
open={isLibraryModalVisible}
onCancel={() => {
setIsLibraryModalVisible(false)
setEditingLibrary(null)
libraryForm.resetFields()
}}
footer={null}
>
<Form
form={libraryForm}
layout="vertical"
onFinish={handleAddOrEditLibrary}
>
<Form.Item
name="name"
label={t('settings.thinkingLibrary.name')}
rules={[{ required: true, message: t('settings.thinkingLibrary.nameRequired') }]}
>
<Input placeholder={t('settings.thinkingLibrary.namePlaceholder')} />
</Form.Item>
<Form.Item
name="description"
label={t('settings.thinkingLibrary.description')}
rules={[{ required: true, message: t('settings.thinkingLibrary.descriptionRequired') }]}
>
<Input.TextArea
placeholder={t('settings.thinkingLibrary.descriptionPlaceholder')}
rows={2}
/>
</Form.Item>
<Form.Item
name="category"
label={t('settings.thinkingLibrary.category')}
rules={[{ required: true, message: t('settings.thinkingLibrary.categoryRequired') }]}
>
<Input placeholder={t('settings.thinkingLibrary.categoryPlaceholder')} />
</Form.Item>
<Form.Item
name="prompt"
label={t('settings.thinkingLibrary.prompt')}
rules={[{ required: true, message: t('settings.thinkingLibrary.promptRequired') }]}
>
<Input.TextArea
placeholder={t('settings.thinkingLibrary.promptPlaceholder')}
rows={10}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{editingLibrary ? t('common.save') : t('common.add')}
</Button>
</Form.Item>
</Form>
</Modal>
</SettingContainer>
)
}
// 样式组件
const EmptyState = styled.div`
text-align: center;
padding: 20px;
color: var(--color-text-3);
`
const CombinationList = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
`
const CombinationItem = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-radius: 8px;
background-color: var(--color-bg-2);
border: 1px solid var(--color-border);
`
const CombinationInfo = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`
const CombinationName = styled.div`
font-weight: 500;
font-size: 16px;
color: var(--color-text-1);
`
const CombinationDetail = styled.div`
font-size: 14px;
color: var(--color-text-2);
`
const CombinationActions = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const ButtonGroup = styled.div`
display: flex;
gap: 8px;
`
export default ModelCombinationSettings

View File

@ -191,7 +191,8 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
{ label: 'OpenAI', value: 'openai' }, { label: 'OpenAI', value: 'openai' },
{ label: 'Gemini', value: 'gemini' }, { label: 'Gemini', value: 'gemini' },
{ label: 'Anthropic', value: 'anthropic' }, { label: 'Anthropic', value: 'anthropic' },
{ label: 'Azure OpenAI', value: 'azure-openai' } { label: 'Azure OpenAI', value: 'azure-openai' },
{ label: 'DeepClaude', value: 'deepclaude' }
]} ]}
/> />
</Form.Item> </Form.Item>

View File

@ -0,0 +1,164 @@
import { PlusOutlined, UploadOutlined } from '@ant-design/icons'
import { formatApiKeys } from '@renderer/services/ApiService'
import { Provider } from '@renderer/types'
import { Button, Input, Modal, Space, Typography, Upload } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const { Text } = Typography
interface GeminiKeyManagerProps {
provider: Provider
currentApiKey: string
onApiKeyChange: (newApiKey: string) => void
}
const GeminiKeyManager: React.FC<GeminiKeyManagerProps> = ({ currentApiKey, onApiKeyChange }) => {
const { t } = useTranslation()
const [isAddKeyModalVisible, setIsAddKeyModalVisible] = useState(false)
const [isImportModalVisible, setIsImportModalVisible] = useState(false)
const [newKey, setNewKey] = useState('')
const [importText, setImportText] = useState('')
// 当前密钥列表
const currentKeys = currentApiKey.split(',').filter(key => key.trim() !== '')
// 添加新密钥
const handleAddKey = () => {
if (!newKey.trim()) return
const formattedKey = newKey.trim()
const keys = [...currentKeys, formattedKey]
const uniqueKeys = [...new Set(keys)]
onApiKeyChange(uniqueKeys.join(','))
setNewKey('')
setIsAddKeyModalVisible(false)
}
// 批量导入密钥
const handleImportKeys = () => {
if (!importText.trim()) return
const importedKeys = importText
.split('\n')
.map(line => line.trim())
.filter(line => line !== '')
const allKeys = [...currentKeys, ...importedKeys]
const uniqueKeys = [...new Set(allKeys)]
onApiKeyChange(uniqueKeys.join(','))
setImportText('')
setIsImportModalVisible(false)
}
// 从文件导入密钥
const handleFileImport = (info: any) => {
const file = info.file.originFileObj
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target?.result as string
if (content) {
setImportText(content)
}
}
reader.readAsText(file)
}
return (
<>
<KeyManagerContainer>
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsAddKeyModalVisible(true)}
>
{t('settings.provider.gemini.add_key')}
</Button>
<Button
icon={<UploadOutlined />}
onClick={() => setIsImportModalVisible(true)}
>
{t('settings.provider.gemini.import_keys')}
</Button>
</Space>
<KeyCountInfo>
{currentKeys.length > 0 && (
<Text type="secondary">
{t('settings.provider.gemini.key_count', { count: currentKeys.length })}
</Text>
)}
</KeyCountInfo>
</KeyManagerContainer>
{/* 添加新密钥的模态框 */}
<Modal
title={t('settings.provider.gemini.add_key_title')}
open={isAddKeyModalVisible}
onOk={handleAddKey}
onCancel={() => setIsAddKeyModalVisible(false)}
okButtonProps={{ disabled: !newKey.trim() }}
>
<Input.Password
value={newKey}
onChange={(e) => setNewKey(formatApiKeys(e.target.value))}
placeholder={t('settings.provider.gemini.enter_key')}
autoFocus
/>
</Modal>
{/* 批量导入密钥的模态框 */}
<Modal
title={t('settings.provider.gemini.import_keys_title')}
open={isImportModalVisible}
onOk={handleImportKeys}
onCancel={() => setIsImportModalVisible(false)}
okButtonProps={{ disabled: !importText.trim() }}
width={600}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Text>{t('settings.provider.gemini.import_keys_desc')}</Text>
<Upload.Dragger
accept=".txt"
beforeUpload={() => false}
onChange={handleFileImport}
showUploadList={false}
style={{ marginBottom: 16 }}
>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">{t('settings.provider.gemini.drop_file')}</p>
</Upload.Dragger>
<Input.TextArea
value={importText}
onChange={(e) => setImportText(e.target.value)}
placeholder={t('settings.provider.gemini.enter_keys')}
rows={8}
/>
</Space>
</Modal>
</>
)
}
const KeyManagerContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
margin-bottom: 16px;
`
const KeyCountInfo = styled.div`
display: flex;
align-items: center;
`
export default GeminiKeyManager

View File

@ -31,6 +31,7 @@ import {
SettingTitle SettingTitle
} from '..' } from '..'
import ApiCheckPopup from './ApiCheckPopup' import ApiCheckPopup from './ApiCheckPopup'
import GeminiKeyManager from './GeminiKeyManager'
import GithubCopilotSettings from './GithubCopilotSettings' import GithubCopilotSettings from './GithubCopilotSettings'
import GPUStackSettings from './GPUStackSettings' import GPUStackSettings from './GPUStackSettings'
import HealthCheckPopup from './HealthCheckPopup' import HealthCheckPopup from './HealthCheckPopup'
@ -383,6 +384,11 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
{provider.id === 'lmstudio' && <LMStudioSettings />} {provider.id === 'lmstudio' && <LMStudioSettings />}
{provider.id === 'gpustack' && <GPUStackSettings />} {provider.id === 'gpustack' && <GPUStackSettings />}
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />} {provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}
{provider.id === 'gemini' && <GeminiKeyManager provider={provider} currentApiKey={apiKey} onApiKeyChange={(newApiKey) => {
setApiKey(newApiKey)
setInputValue(newApiKey)
updateProvider({ ...provider, apiKey: newApiKey })
}} />}
<SettingSubtitle style={{ marginBottom: 5 }}> <SettingSubtitle style={{ marginBottom: 5 }}>
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}> <Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<HStack alignItems="center" gap={5}> <HStack alignItems="center" gap={5}>

View File

@ -170,7 +170,10 @@ const ProvidersList: FC = () => {
return menus return menus
} }
if (provider.isSystem) { // 系统内置的供应商不允许删除,但以 provider 开头的自动生成供应商或DeepClaude供应商允许删除
if (provider.isSystem &&
!provider.id.includes('provider') &&
provider.type !== 'deepclaude') {
return [] return []
} }
@ -252,7 +255,7 @@ const ProvidersList: FC = () => {
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}> style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown menu={{ items: getDropdownMenus(provider) }} trigger={['contextMenu']}> <Dropdown menu={{ items: getDropdownMenus(provider) }} trigger={['contextMenu']} destroyPopupOnHide>
<ProviderListItem <ProviderListItem
key={JSON.stringify(provider)} key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''} className={provider.id === selectedProvider?.id ? 'active' : ''}
@ -272,6 +275,7 @@ const ProvidersList: FC = () => {
)} )}
</Draggable> </Draggable>
))} ))}
{provided.placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>

View File

@ -31,6 +31,7 @@ import MCPSettings from './MCPSettings'
import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar' import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar'
import MemorySettings from './MemorySettings' import MemorySettings from './MemorySettings'
import MiniAppSettings from './MiniappSettings/MiniAppSettings' import MiniAppSettings from './MiniappSettings/MiniAppSettings'
import ModelCombinationSettings from './ModelCombinationSettings'
import ProvidersList from './ProviderSettings' import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings' import QuickAssistantSettings from './QuickAssistantSettings'
import QuickPhraseSettings from './QuickPhraseSettings' import QuickPhraseSettings from './QuickPhraseSettings'
@ -66,6 +67,12 @@ const SettingsPage: FC = () => {
{t('settings.model')} {t('settings.model')}
</MenuItem> </MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/model-combination">
<MenuItem className={isRoute('/settings/model-combination')}>
<Package size={18} />
{t('settings.modelCombination.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/web-search"> <MenuItemLink to="/settings/web-search">
<MenuItem className={isRoute('/settings/web-search')}> <MenuItem className={isRoute('/settings/web-search')}>
<Globe size={18} /> <Globe size={18} />
@ -145,6 +152,7 @@ const SettingsPage: FC = () => {
<Routes> <Routes>
<Route path="provider" element={<ProvidersList />} /> <Route path="provider" element={<ProvidersList />} />
<Route path="model" element={<ModelSettings />} /> <Route path="model" element={<ModelSettings />} />
<Route path="model-combination" element={<ModelCombinationSettings />} />
<Route path="web-search" element={<WebSearchSettings />} /> <Route path="web-search" element={<WebSearchSettings />} />
<Route path="mcp/*" element={<MCPSettings />} /> <Route path="mcp/*" element={<MCPSettings />} />
<Route path="memory" element={<MemorySettings />} /> <Route path="memory" element={<MemorySettings />} />

View File

@ -8,6 +8,7 @@ import {
removeTtsCustomModel, removeTtsCustomModel,
removeTtsCustomVoice, removeTtsCustomVoice,
resetTtsCustomValues, resetTtsCustomValues,
setAutoPlayTTSOutsideVoiceCall,
setShowTTSProgressBar, setShowTTSProgressBar,
setTtsApiKey, setTtsApiKey,
setTtsApiUrl, setTtsApiUrl,
@ -530,6 +531,17 @@ const TTSSettings: FC = () => {
<Switch checked={ttsEnabled} onChange={(checked) => dispatch(setTtsEnabled(checked))} /> <Switch checked={ttsEnabled} onChange={(checked) => dispatch(setTtsEnabled(checked))} />
</SettingRow> </SettingRow>
<SettingHelpText>{t('settings.tts.enable.help')}</SettingHelpText> <SettingHelpText>{t('settings.tts.enable.help')}</SettingHelpText>
{/* 自动播放TTS设置 */}
<SettingRow style={{ marginTop: 16 }}>
<SettingRowTitle>TTS</SettingRowTitle>
<Switch
checked={settings.autoPlayTTSOutsideVoiceCall}
onChange={(checked) => dispatch(setAutoPlayTTSOutsideVoiceCall(checked))}
disabled={!ttsEnabled}
/>
</SettingRow>
<SettingHelpText>使TTS</SettingHelpText>
</SettingGroup> </SettingGroup>
{/* 重置按钮 */} {/* 重置按钮 */}

View File

@ -0,0 +1,494 @@
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
import { isEmpty, takeRight } from 'lodash'
import store from '@renderer/store'
import { addAbortController, removeAbortController } from '@renderer/utils/abortController'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { getThinkingLibraryById } from '@renderer/utils/thinkingLibrary'
import { CompletionsParams } from '.'
import BaseProvider from './BaseProvider'
import ProviderFactory from './ProviderFactory'
interface ModelCombination {
id: string
name: string
reasonerModel: Model
targetModel: Model
isActive: boolean
thinkingLibraryId?: string
}
export default class DeepClaudeProvider extends BaseProvider {
private reasonerProvider: BaseProvider
private targetProvider: BaseProvider
private modelCombination: ModelCombination
constructor(provider: Provider, modelCombination: ModelCombination) {
super(provider)
console.log('[DeepClaudeProvider] 构造函数被调用,接收到的模型组合:',
modelCombination.id, modelCombination.name,
'推理模型:', modelCombination.reasonerModel?.id, modelCombination.reasonerModel?.name,
'目标模型:', modelCombination.targetModel?.id, modelCombination.targetModel?.name)
// 查找推理模型和目标模型的提供商
const providers = store.getState().llm.providers
console.log('[DeepClaudeProvider] 当前提供商数量:', providers.length)
const reasonerModelProvider = providers.find((p: Provider) =>
p.models.some((m: Model) => m.id === modelCombination.reasonerModel.id)
)
console.log('[DeepClaudeProvider] 推理模型提供商:', reasonerModelProvider?.id, reasonerModelProvider?.name)
const targetModelProvider = providers.find((p: Provider) =>
p.models.some((m: Model) => m.id === modelCombination.targetModel.id)
)
console.log('[DeepClaudeProvider] 目标模型提供商:', targetModelProvider?.id, targetModelProvider?.name)
if (!reasonerModelProvider || !targetModelProvider) {
console.error('[DeepClaudeProvider] 无法找到模型对应的提供商')
throw new Error('无法找到模型对应的提供商')
}
// 创建推理模型和目标模型的Provider实例
console.log('[DeepClaudeProvider] 开始创建推理模型提供商实例')
this.reasonerProvider = ProviderFactory.create(reasonerModelProvider)
console.log('[DeepClaudeProvider] 开始创建目标模型提供商实例')
this.targetProvider = ProviderFactory.create(targetModelProvider)
console.log('[DeepClaudeProvider] 提供商实例创建完成')
this.modelCombination = modelCombination
console.log('[DeepClaudeProvider] 初始化完成,推理模型:', this.modelCombination.reasonerModel.name,
'推理模型提供商:', reasonerModelProvider.name,
'目标模型:', this.modelCombination.targetModel.name,
'目标模型提供商:', targetModelProvider.name)
}
/**
*
*/
public async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
// 获取设置
const contextCount = assistant.settings?.contextCount || 10
// 过滤消息
const filteredMessages = takeRight(messages.filter(m => !isEmpty(m.content)), contextCount + 2)
if (onFilterMessages) {
onFilterMessages(filteredMessages)
}
// 如果没有消息,直接返回
if (isEmpty(filteredMessages)) {
return
}
// 获取最后一条用户消息
const lastUserMessage = filteredMessages[filteredMessages.length - 1]
// 创建中止控制器
const abortController = new AbortController()
const requestId = Date.now().toString()
const abortFn = () => abortController.abort()
addAbortController(requestId, abortFn)
try {
// 创建状态对象来跟踪推理过程
const state = {
isReasoningStarted: false, // 是否已经开始显示思考过程
isReasoningFinished: false, // 推理模型是否已完成
isTargetStarted: false, // 目标模型是否已开始
accumulatedThinking: '', // 累积的思考过程
extractedThinking: '', // 提取的思考过程
isFirstTargetChunk: true // 是否是目标模型的第一个chunk
}
// 同时启动两个模型的调用
await Promise.all([
// 推理模型任务
(async () => {
try {
console.log('[DeepClaudeProvider] 启动推理模型任务,使用模型:',
this.modelCombination.reasonerModel.name,
'模型ID:', this.modelCombination.reasonerModel.id,
'提供商:', this.modelCombination.reasonerModel.provider)
// 检查推理模型是否是专门的推理模型
const isSpecialReasonerModel = this.modelCombination.reasonerModel.group === 'DeepSeek' ||
this.modelCombination.reasonerModel.name.toLowerCase().includes('reason');
// 根据模型类型和思考库选择不同的提示词
let reasoningPrompt = '';
if (isSpecialReasonerModel) {
// 专门的推理模型使用简单提示词
reasoningPrompt = `你是一个思考助手。请对以下问题进行深入思考,分析问题的各个方面,并提供详细的推理过程。
<thinking></thinking>
问题: ${lastUserMessage.content}`;
} else {
// 普通模型使用思考库提示词或默认提示词
const thinkingLibrary = getThinkingLibraryById(this.modelCombination.thinkingLibraryId);
if (thinkingLibrary) {
// 使用选定的思考库提示词
console.log('[DeepClaudeProvider] 使用思考库:', thinkingLibrary.name);
reasoningPrompt = thinkingLibrary.prompt.replace('{question}', lastUserMessage.content);
} else {
// 使用默认提示词
console.log('[DeepClaudeProvider] 使用默认思考提示词');
reasoningPrompt = `你是一个思考助手。请对以下问题进行深入思考,分析问题的各个方面,并提供详细的推理过程。
AI助手的思考基础
<think></think>
问题: ${lastUserMessage.content}`;
}
}
// 创建推理模型的消息列表
// 保留历史消息,但修改最后一条用户消息
console.log('[DeepClaudeProvider] 推理模型使用原始对话历史消息数量:', filteredMessages.length);
// 复制历史消息,但修改最后一条用户消息
const reasoningMessages = filteredMessages.map((msg, index) => {
// 只修改最后一条用户消息
if (index === filteredMessages.length - 1 && msg.role === 'user') {
return {
...msg,
content: reasoningPrompt
}
}
return msg
});
// 使用completions方法调用推理模型
await this.reasonerProvider.completions({
messages: reasoningMessages,
assistant: {
...assistant,
model: this.modelCombination.reasonerModel,
prompt: '' // 不使用assistant的prompt而是使用我们自定义的reasoningPrompt
},
mcpTools: [], // 不使用工具,避免干扰推理过程
onChunk: (chunk) => {
// 累积推理过程
if (chunk.text) {
state.accumulatedThinking += chunk.text;
// 实时将思考过程传递给前端
if (!state.isTargetStarted) {
// 只有在目标模型尚未开始时才发送思考过程
if (!state.isReasoningStarted) {
state.isReasoningStarted = true;
// 第一次发送思考过程使用reasoning_content字段
onChunk({
reasoning_content: chunk.text,
text: '' // 不显示文本,只显示思考过程
});
} else {
// 后续发送思考过程继续使用reasoning_content字段
onChunk({
reasoning_content: chunk.text,
text: '' // 不显示文本,只显示思考过程
});
}
}
// 输出日志,让用户看到推理过程
console.log('[DeepClaudeProvider] 推理模型输出:', chunk.text.length, '字符');
}
},
onFilterMessages: () => {}
});
// 如果不是专门的推理模型,将其输出包装在<think></think>标签中
if (!isSpecialReasonerModel &&
!state.accumulatedThinking.includes('<think>') &&
!state.accumulatedThinking.includes('<thinking>')) {
state.accumulatedThinking = `<think>${state.accumulatedThinking}</think>`;
}
// 提取思考过程
let extractedThinking = '';
// 检查是否是Gemini模型的JSON格式输出
if (state.accumulatedThinking.includes('data: {"candidates"') || state.accumulatedThinking.includes('data: {\"candidates\"')) {
console.log('[DeepClaudeProvider] 检测到Gemini模型的JSON格式输出');
try {
// 尝试提取JSON中的文本内容
const lines = state.accumulatedThinking.split('\n');
let combinedText = '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.substring(6);
const jsonData = JSON.parse(jsonStr);
if (jsonData.candidates &&
jsonData.candidates[0] &&
jsonData.candidates[0].content &&
jsonData.candidates[0].content.parts &&
jsonData.candidates[0].content.parts[0] &&
jsonData.candidates[0].content.parts[0].text) {
combinedText += jsonData.candidates[0].content.parts[0].text;
}
} catch (e) {
// 忽略JSON解析错误
}
}
}
if (combinedText) {
// 尝试从组合的文本中提取<think>标签
const thinkRegex = new RegExp('<think>([\\s\\S]*?)</think>');
const thinkMatch = combinedText.match(thinkRegex);
if (thinkMatch && thinkMatch[1]) {
extractedThinking = thinkMatch[1].trim();
console.log('[DeepClaudeProvider] 成功从 Gemini JSON 输出中提取<think>标签的思考过程');
} else {
// 如果没有标签,使用整个文本作为思考过程
extractedThinking = combinedText.trim();
console.log('[DeepClaudeProvider] 从 Gemini JSON 输出中提取了思考过程,但没有<think>标签');
}
}
} catch (error) {
console.error('[DeepClaudeProvider] 解析 Gemini JSON 输出时出错:', error);
extractedThinking = state.accumulatedThinking;
}
} else {
// 常规模型输出处理
// 先尝试匹配<think>标签
const thinkRegex = new RegExp('<think>([\\s\\S]*?)</think>');
const thinkMatch = state.accumulatedThinking.match(thinkRegex);
// 如果没有匹配到<think>标签,尝试匹配<thinking>标签
if (thinkMatch && thinkMatch[1]) {
extractedThinking = thinkMatch[1].trim();
console.log('[DeepClaudeProvider] 成功提取<think>标签中的思考过程');
} else {
const thinkingRegex = new RegExp('<thinking>([\\s\\S]*?)</thinking>');
const thinkingMatch = state.accumulatedThinking.match(thinkingRegex);
if (thinkingMatch && thinkingMatch[1]) {
extractedThinking = thinkingMatch[1].trim();
console.log('[DeepClaudeProvider] 成功提取<thinking>标签中的思考过程');
} else {
extractedThinking = state.accumulatedThinking;
console.log('[DeepClaudeProvider] 未能提取思考过程,使用原始输出');
}
}
}
// 更新思考过程
state.extractedThinking = extractedThinking;
console.log('[DeepClaudeProvider] 推理模型完成,思考过程长度:', state.extractedThinking.length);
console.log('[DeepClaudeProvider] 推理模型输出示例:', state.extractedThinking.substring(0, 100) + '...');
console.log('[DeepClaudeProvider] 推理模型信息:', this.modelCombination.reasonerModel.name, this.modelCombination.reasonerModel.id);
// 标记推理模型已完成
state.isReasoningFinished = true;
} catch (error) {
console.error('[DeepClaudeProvider] 推理模型错误:', error);
// 即使出错,也要标记推理模型已完成,以便目标模型可以继续
state.isReasoningFinished = true;
state.extractedThinking = '推理模型出错,无法获取思考过程。';
}
})(),
// 目标模型任务
(async () => {
try {
console.log('[DeepClaudeProvider] 等待推理模型开始生成思考过程...')
// 等待推理模型开始生成思考过程
while (!state.isReasoningStarted && !state.isReasoningFinished) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// 等待推理模型完成
while (!state.isReasoningFinished) {
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('[DeepClaudeProvider] 推理模型已完成,立即启动目标模型任务')
// 标记目标模型已开始
state.isTargetStarted = true;
console.log('[DeepClaudeProvider] 启动目标模型任务')
console.log('[DeepClaudeProvider] 目标模型信息:', this.modelCombination.targetModel.name, this.modelCombination.targetModel.id)
// 构建目标模型的提示词
const targetPrompt = `以下是对这个问题的思考过程,请基于这个思考过程回答我的问题,但不要重复思考过程,不要在回答中包含“思考过程”或类似的标题,直接给出清晰、准确的回答:
${state.extractedThinking}`
// 构建系统提示词
const systemPrompt = await buildSystemPrompt(assistant.prompt || '', mcpTools || [], [])
// 保留原始对话历史,但修改最后一条用户消息
// 将思考过程添加到最后一条用户消息中
console.log('[DeepClaudeProvider] 原始对话历史消息数量:', filteredMessages.length)
// 创建最终的消息列表,保留所有历史消息
const finalMessages = filteredMessages.map((msg, index) => {
// 只修改最后一条用户消息
if (index === filteredMessages.length - 1 && msg.role === 'user') {
return {
...msg,
content: `${msg.content}\n\n${targetPrompt}`
}
}
return msg
})
console.log('[DeepClaudeProvider] 最终消息列表数量:', finalMessages.length)
// 使用目标模型生成最终回答
await this.targetProvider.completions({
messages: finalMessages,
assistant: {
...assistant,
model: this.modelCombination.targetModel,
prompt: systemPrompt
},
mcpTools,
onChunk: (chunk) => {
// 直接传递chunk不再添加思考过程
// 因为思考过程已经在推理模型的onChunk回调中实时传递给前端了
onChunk(chunk)
},
onFilterMessages
})
} catch (error) {
console.error('[DeepClaudeProvider] 目标模型错误:', error)
throw error
}
})()
])
} catch (error) {
console.error('DeepClaudeProvider completions error:', error)
throw error
} finally {
removeAbortController(requestId, abortFn)
}
}
/**
*
*/
public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void): Promise<string> {
// 使用目标模型进行翻译
return this.targetProvider.translate(message, {
...assistant,
model: this.modelCombination.targetModel
}, onResponse)
}
/**
*
*/
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
// 使用目标模型生成摘要
return this.targetProvider.summaries(messages, {
...assistant,
model: this.modelCombination.targetModel
})
}
/**
*
*/
public async summaryForSearch(messages: Message[], assistant: Assistant): Promise<string | null> {
// 使用目标模型为搜索生成摘要
return this.targetProvider.summaryForSearch(messages, {
...assistant,
model: this.modelCombination.targetModel
})
}
/**
*
*/
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
// 使用目标模型生成建议
return this.targetProvider.suggestions(messages, {
...assistant,
model: this.modelCombination.targetModel
})
}
/**
*
*/
public async generateText({
prompt,
content,
modelId
}: {
prompt: string
content: string
modelId?: string
}): Promise<string> {
// 如果指定了模型ID则使用指定的模型
if (modelId) {
const providers = store.getState().llm.providers
const modelProvider = providers.find((p: Provider) => p.models.some((m: Model) => m.id === modelId))
if (modelProvider) {
const provider = ProviderFactory.create(modelProvider)
return provider.generateText({ prompt, content, modelId })
}
}
// 默认使用目标模型生成文本
return this.targetProvider.generateText({
prompt,
content,
modelId: this.modelCombination.targetModel.id
})
}
/**
*
*/
public async check(_model: Model): Promise<{ valid: boolean; error: Error | null }> {
// 检查推理模型和目标模型
const reasonerCheck = await this.reasonerProvider.check(this.modelCombination.reasonerModel)
if (!reasonerCheck.valid) {
return reasonerCheck
}
return this.targetProvider.check(this.modelCombination.targetModel)
}
/**
*
*/
public async models(): Promise<any> {
// 返回目标提供商的模型列表
return this.targetProvider.models()
}
/**
*
*/
public async generateImage(params: any): Promise<string[]> {
// 使用目标模型生成图像
return this.targetProvider.generateImage(params)
}
/**
*
*/
public async getEmbeddingDimensions(model: Model): Promise<number> {
// 使用目标模型获取嵌入维度
return this.targetProvider.getEmbeddingDimensions(model)
}
}

View File

@ -48,15 +48,74 @@ export default class GeminiProvider extends BaseProvider {
private sdk: GoogleGenerativeAI private sdk: GoogleGenerativeAI
private requestOptions: RequestOptions private requestOptions: RequestOptions
private imageSdk: GoogleGenAI private imageSdk: GoogleGenAI
// 存储对话ID到SDK实例的映射
private conversationSdks: Map<string, GoogleGenerativeAI> = new Map()
// 存储对话ID到图像SDK实例的映射
private conversationImageSdks: Map<string, GoogleGenAI> = new Map()
constructor(provider: Provider) { constructor(provider: Provider) {
super(provider) super(provider)
this.sdk = new GoogleGenerativeAI(this.apiKey) // 获取新的API密钥实现轮流使用多个密钥
const apiKey = this.getApiKey()
this.sdk = new GoogleGenerativeAI(apiKey)
/// this sdk is experimental /// this sdk is experimental
this.imageSdk = new GoogleGenAI({ apiKey: this.apiKey, httpOptions: { baseUrl: this.getBaseURL() } }) this.imageSdk = new GoogleGenAI({ apiKey: apiKey, httpOptions: { baseUrl: this.getBaseURL() } })
this.requestOptions = { this.requestOptions = {
baseUrl: this.getBaseURL() baseUrl: this.getBaseURL()
} }
console.log(`[GeminiProvider] Initialized with API key`)
}
/**
* SDK实例
* @param conversationId - ID
* @returns SDK实例
*/
private getOrCreateSdk(conversationId: string): GoogleGenerativeAI {
// 获取新的API密钥实现轮流使用多个密钥
const apiKey = this.getApiKey()
// 如果没有提供对话ID创建一个新的SDK实例
if (!conversationId) {
this.sdk = new GoogleGenerativeAI(apiKey)
return this.sdk
}
// 创建新的SDK实例
const newSdk = new GoogleGenerativeAI(apiKey)
// 存储SDK实例覆盖之前的实例
this.conversationSdks.set(conversationId, newSdk)
console.log(`[GeminiProvider] Created new SDK for conversation ${conversationId} with API key`)
return newSdk
}
/**
* SDK实例
* @param conversationId - ID
* @returns SDK实例
*/
private getOrCreateImageSdk(conversationId: string): GoogleGenAI {
// 获取新的API密钥实现轮流使用多个密钥
const apiKey = this.getApiKey()
// 如果没有提供对话ID创建一个新的SDK实例
if (!conversationId) {
this.imageSdk = new GoogleGenAI({ apiKey: apiKey, httpOptions: { baseUrl: this.getBaseURL() } })
return this.imageSdk
}
// 创建新的SDK实例
const newSdk = new GoogleGenAI({ apiKey: apiKey, httpOptions: { baseUrl: this.getBaseURL() } })
// 存储SDK实例覆盖之前的实例
this.conversationImageSdks.set(conversationId, newSdk)
console.log(`[GeminiProvider] Created new Image SDK for conversation ${conversationId} with API key`)
return newSdk
} }
public getBaseURL(): string { public getBaseURL(): string {
@ -207,6 +266,9 @@ export default class GeminiProvider extends BaseProvider {
* @param onFilterMessages - The onFilterMessages callback * @param onFilterMessages - The onFilterMessages callback
*/ */
public async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams) { public async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams) {
// 获取对话ID用于关联SDK实例
const conversationId = assistant.id || ''
if (assistant.enableGenerateImage) { if (assistant.enableGenerateImage) {
await this.generateImageExp({ messages, assistant, onFilterMessages, onChunk }) await this.generateImageExp({ messages, assistant, onFilterMessages, onChunk })
} else { } else {
@ -227,14 +289,23 @@ export default class GeminiProvider extends BaseProvider {
history.push(await this.getMessageContents(message)) history.push(await this.getMessageContents(message))
} }
let systemInstruction = assistant.prompt // 获取当前话题ID
const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined
if (mcpTools && mcpTools.length > 0) { // 应用记忆功能到系统提示词
systemInstruction = await buildSystemPrompt( const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
assistant.prompt || '', const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId)
mcpTools, console.log(
getActiveServers(store.getState()) '[GeminiProvider.completions] Applied memories to prompt, length difference:',
enhancedPrompt.length - (assistant.prompt || '').length
) )
// 使用增强后的提示词
let systemInstruction = enhancedPrompt
// 如果有MCP工具进一步处理
if (mcpTools && mcpTools.length > 0) {
systemInstruction = await buildSystemPrompt(enhancedPrompt, mcpTools, getActiveServers(store.getState()))
} }
// const tools = mcpToolsToGeminiTools(mcpTools) // const tools = mcpToolsToGeminiTools(mcpTools)
@ -248,7 +319,10 @@ export default class GeminiProvider extends BaseProvider {
}) })
} }
const geminiModel = this.sdk.getGenerativeModel( // 使用与对话关联的SDK实例
const sdk = this.getOrCreateSdk(conversationId)
const geminiModel = sdk.getGenerativeModel(
{ {
model: model.id, model: model.id,
...(isGemmaModel(model) ? {} : { systemInstruction: systemInstruction }), ...(isGemmaModel(model) ? {} : { systemInstruction: systemInstruction }),
@ -383,10 +457,27 @@ export default class GeminiProvider extends BaseProvider {
const { maxTokens } = getAssistantSettings(assistant) const { maxTokens } = getAssistantSettings(assistant)
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const geminiModel = this.sdk.getGenerativeModel( // 获取对话ID用于关联SDK实例
const conversationId = assistant.id || ''
// 获取当前话题ID
const currentTopicId = message.topicId
// 应用记忆功能到系统提示词
const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId)
console.log(
'[GeminiProvider.translate] Applied memories to prompt, length difference:',
enhancedPrompt.length - (assistant.prompt || '').length
)
// 使用与对话关联的SDK实例
const sdk = this.getOrCreateSdk(conversationId)
const geminiModel = sdk.getGenerativeModel(
{ {
model: model.id, model: model.id,
...(isGemmaModel(model) ? {} : { systemInstruction: assistant.prompt }), ...(isGemmaModel(model) ? {} : { systemInstruction: enhancedPrompt }),
generationConfig: { generationConfig: {
maxOutputTokens: maxTokens, maxOutputTokens: maxTokens,
temperature: assistant?.settings?.temperature temperature: assistant?.settings?.temperature
@ -396,8 +487,8 @@ export default class GeminiProvider extends BaseProvider {
) )
const content = const content =
isGemmaModel(model) && assistant.prompt isGemmaModel(model) && enhancedPrompt
? `<start_of_turn>user\n${assistant.prompt}<end_of_turn>\n<start_of_turn>user\n${message.content}<end_of_turn>` ? `<start_of_turn>user\n${enhancedPrompt}<end_of_turn>\n<start_of_turn>user\n${message.content}<end_of_turn>`
: message.content : message.content
if (!onResponse) { if (!onResponse) {
@ -438,9 +529,23 @@ export default class GeminiProvider extends BaseProvider {
return prev + (prev ? '\n' : '') + content return prev + (prev ? '\n' : '') + content
}, '') }, '')
// 获取原始提示词
const originalPrompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
// 获取当前话题ID
const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined
// 应用记忆功能到系统提示词
const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
const enhancedPrompt = await applyMemoriesToPrompt(originalPrompt, currentTopicId)
console.log(
'[GeminiProvider.summaries] Applied memories to prompt, length difference:',
enhancedPrompt.length - originalPrompt.length
)
const systemMessage = { const systemMessage = {
role: 'system', role: 'system',
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') content: enhancedPrompt
} }
const userMessage = { const userMessage = {
@ -448,7 +553,13 @@ export default class GeminiProvider extends BaseProvider {
content: userMessageContent content: userMessageContent
} }
const geminiModel = this.sdk.getGenerativeModel( // 获取对话ID用于关联SDK实例
const conversationId = assistant.id || ''
// 使用与对话关联的SDK实例
const sdk = this.getOrCreateSdk(conversationId)
const geminiModel = sdk.getGenerativeModel(
{ {
model: model.id, model: model.id,
...(isGemmaModel(model) ? {} : { systemInstruction: systemMessage.content }), ...(isGemmaModel(model) ? {} : { systemInstruction: systemMessage.content }),
@ -459,9 +570,9 @@ export default class GeminiProvider extends BaseProvider {
this.requestOptions this.requestOptions
) )
const chat = await geminiModel.startChat() const chat = geminiModel.startChat()
const content = isGemmaModel(model) const content = isGemmaModel(model)
? `<start_of_turn>user\n${systemMessage.content}<end_of_turn>\n<start_of_turn>user\n${userMessage.content}<end_of_turn>` ? `<start_of_turn>user\n${enhancedPrompt}<end_of_turn>\n<start_of_turn>user\n${userMessage.content}<end_of_turn>`
: userMessage.content : userMessage.content
const { response } = await chat.sendMessage(content) const { response } = await chat.sendMessage(content)
@ -479,11 +590,13 @@ export default class GeminiProvider extends BaseProvider {
public async generateText({ public async generateText({
prompt, prompt,
content, content,
modelId modelId,
conversationId = ''
}: { }: {
prompt: string prompt: string
content: string content: string
modelId?: string modelId?: string
conversationId?: string
}): Promise<string> { }): Promise<string> {
// 使用指定的模型或默认模型 // 使用指定的模型或默认模型
const model = modelId const model = modelId
@ -508,7 +621,10 @@ export default class GeminiProvider extends BaseProvider {
const systemMessage = { role: 'system', content: enhancedPrompt } const systemMessage = { role: 'system', content: enhancedPrompt }
const geminiModel = this.sdk.getGenerativeModel( // 使用与对话关联的SDK实例
const sdk = this.getOrCreateSdk(conversationId)
const geminiModel = sdk.getGenerativeModel(
{ {
model: model.id, model: model.id,
...(isGemmaModel(model) ? {} : { systemInstruction: systemMessage.content }) ...(isGemmaModel(model) ? {} : { systemInstruction: systemMessage.content })
@ -516,7 +632,7 @@ export default class GeminiProvider extends BaseProvider {
this.requestOptions this.requestOptions
) )
const chat = await geminiModel.startChat() const chat = geminiModel.startChat()
const messageContent = isGemmaModel(model) const messageContent = isGemmaModel(model)
? `<start_of_turn>user\n${enhancedPrompt}<end_of_turn>\n<start_of_turn>user\n${content}<end_of_turn>` ? `<start_of_turn>user\n${enhancedPrompt}<end_of_turn>\n<start_of_turn>user\n${content}<end_of_turn>`
: content : content
@ -543,20 +659,34 @@ export default class GeminiProvider extends BaseProvider {
public async summaryForSearch(messages: Message[], assistant: Assistant): Promise<string> { public async summaryForSearch(messages: Message[], assistant: Assistant): Promise<string> {
const model = assistant.model || getDefaultModel() const model = assistant.model || getDefaultModel()
const systemMessage = { // 获取当前话题ID
role: 'system', const currentTopicId = messages.length > 0 ? messages[0].topicId : undefined
content: assistant.prompt
} // 应用记忆功能到系统提示词
const { applyMemoriesToPrompt } = await import('@renderer/services/MemoryService')
const enhancedPrompt = await applyMemoriesToPrompt(assistant.prompt || '', currentTopicId)
console.log(
'[GeminiProvider.summaryForSearch] Applied memories to prompt, length difference:',
enhancedPrompt.length - (assistant.prompt || '').length
)
// 不再需要单独的systemMessage变量因为我们直接使用enhancedPrompt
const userMessage = { const userMessage = {
role: 'user', role: 'user',
content: messages.map((m) => m.content).join('\n') content: messages.map((m) => m.content).join('\n')
} }
const geminiModel = this.sdk.getGenerativeModel( // 获取对话ID用于关联SDK实例
const conversationId = assistant.id || ''
// 使用与对话关联的SDK实例
const sdk = this.getOrCreateSdk(conversationId)
const geminiModel = sdk.getGenerativeModel(
{ {
model: model.id, model: model.id,
systemInstruction: systemMessage.content, systemInstruction: enhancedPrompt,
generationConfig: { generationConfig: {
temperature: assistant?.settings?.temperature temperature: assistant?.settings?.temperature
} }
@ -567,7 +697,7 @@ export default class GeminiProvider extends BaseProvider {
} }
) )
const chat = await geminiModel.startChat() const chat = geminiModel.startChat()
const { response } = await chat.sendMessage(userMessage.content) const { response } = await chat.sendMessage(userMessage.content)
return response.text() return response.text()
@ -594,6 +724,9 @@ export default class GeminiProvider extends BaseProvider {
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const { contextCount, streamOutput, maxTokens } = getAssistantSettings(assistant) const { contextCount, streamOutput, maxTokens } = getAssistantSettings(assistant)
// 获取对话ID用于关联SDK实例
const conversationId = assistant.id || ''
const userMessages = filterUserRoleStartMessages(filterContextMessages(takeRight(messages, contextCount + 2))) const userMessages = filterUserRoleStartMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
onFilterMessages(userMessages) onFilterMessages(userMessages)
@ -615,8 +748,11 @@ export default class GeminiProvider extends BaseProvider {
contents = await this.addImageFileToContents(userLastMessage, contents) contents = await this.addImageFileToContents(userLastMessage, contents)
// 使用与对话关联的图像SDK实例
const imageSdk = this.getOrCreateImageSdk(conversationId)
if (!streamOutput) { if (!streamOutput) {
const response = await this.callGeminiGenerateContent(model.id, contents, maxTokens) const response = await this.callGeminiGenerateContent(model.id, contents, maxTokens, imageSdk)
const { isValid, message } = this.isValidGeminiResponse(response) const { isValid, message } = this.isValidGeminiResponse(response)
if (!isValid) { if (!isValid) {
@ -626,7 +762,7 @@ export default class GeminiProvider extends BaseProvider {
this.processGeminiImageResponse(response, onChunk) this.processGeminiImageResponse(response, onChunk)
return return
} }
const response = await this.callGeminiGenerateContentStream(model.id, contents, maxTokens) const response = await this.callGeminiGenerateContentStream(model.id, contents, maxTokens, imageSdk)
for await (const chunk of response) { for await (const chunk of response) {
this.processGeminiImageResponse(chunk, onChunk) this.processGeminiImageResponse(chunk, onChunk)
@ -661,10 +797,17 @@ export default class GeminiProvider extends BaseProvider {
private async callGeminiGenerateContent( private async callGeminiGenerateContent(
modelId: string, modelId: string,
contents: ContentListUnion, contents: ContentListUnion,
maxTokens?: number maxTokens?: number,
sdk?: GoogleGenAI
): Promise<GenerateContentResponse> { ): Promise<GenerateContentResponse> {
try { try {
return await this.imageSdk.models.generateContent({ // 获取新的API密钥实现轮流使用多个密钥
const apiKey = this.getApiKey()
// 创建新的SDK实例
const apiSdk = sdk || new GoogleGenAI({ apiKey: apiKey, httpOptions: { baseUrl: this.getBaseURL() } })
return await apiSdk.models.generateContent({
model: modelId, model: modelId,
contents: contents, contents: contents,
config: { config: {
@ -682,10 +825,17 @@ export default class GeminiProvider extends BaseProvider {
private async callGeminiGenerateContentStream( private async callGeminiGenerateContentStream(
modelId: string, modelId: string,
contents: ContentListUnion, contents: ContentListUnion,
maxTokens?: number maxTokens?: number,
sdk?: GoogleGenAI
): Promise<AsyncGenerator<GenerateContentResponse>> { ): Promise<AsyncGenerator<GenerateContentResponse>> {
try { try {
return await this.imageSdk.models.generateContentStream({ // 获取新的API密钥实现轮流使用多个密钥
const apiKey = this.getApiKey()
// 创建新的SDK实例
const apiSdk = sdk || new GoogleGenAI({ apiKey: apiKey, httpOptions: { baseUrl: this.getBaseURL() } })
return await apiSdk.models.generateContentStream({
model: modelId, model: modelId,
contents: contents, contents: contents,
config: { config: {
@ -775,7 +925,11 @@ export default class GeminiProvider extends BaseProvider {
} }
try { try {
const geminiModel = this.sdk.getGenerativeModel({ model: body.model }, this.requestOptions) // 使用新的API密钥创建一个临时SDK实例进行检查
const apiKey = this.getApiKey()
const tempSdk = new GoogleGenerativeAI(apiKey)
const geminiModel = tempSdk.getGenerativeModel({ model: body.model }, this.requestOptions)
const result = await geminiModel.generateContent(body.messages[0].content) const result = await geminiModel.generateContent(body.messages[0].content)
return { return {
valid: !isEmpty(result.response.text()), valid: !isEmpty(result.response.text()),
@ -799,7 +953,7 @@ export default class GeminiProvider extends BaseProvider {
const { data } = await axios.get(api, { params: { key: this.apiKey } }) const { data } = await axios.get(api, { params: { key: this.apiKey } })
return data.models.map( return data.models.map(
(m) => (m: { name: string; displayName: string; description: string }) =>
({ ({
id: m.name.replace('models/', ''), id: m.name.replace('models/', ''),
name: m.displayName, name: m.displayName,
@ -820,7 +974,11 @@ export default class GeminiProvider extends BaseProvider {
* @returns The embedding dimensions * @returns The embedding dimensions
*/ */
public async getEmbeddingDimensions(model: Model): Promise<number> { public async getEmbeddingDimensions(model: Model): Promise<number> {
const data = await this.sdk.getGenerativeModel({ model: model.id }, this.requestOptions).embedContent('hi') // 使用新的API密钥创建一个临时SDK实例
const apiKey = this.getApiKey()
const tempSdk = new GoogleGenerativeAI(apiKey)
const data = await tempSdk.getGenerativeModel({ model: model.id }, this.requestOptions).embedContent('hi')
return data.embedding.values.length return data.embedding.values.length
} }
} }

View File

@ -1,12 +1,116 @@
import { Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import store from '@renderer/store'
import AnthropicProvider from './AnthropicProvider' import AnthropicProvider from './AnthropicProvider'
import BaseProvider from './BaseProvider' import BaseProvider from './BaseProvider'
import DeepClaudeProvider from './DeepClaudeProvider'
import GeminiProvider from './GeminiProvider' import GeminiProvider from './GeminiProvider'
import OpenAIProvider from './OpenAIProvider' import OpenAIProvider from './OpenAIProvider'
// 模型组合接口
interface ModelCombination {
id: string
name: string
reasonerModel: Model
targetModel: Model
isActive: boolean
}
export default class ProviderFactory { export default class ProviderFactory {
static create(provider: Provider): BaseProvider { static create(provider: Provider): BaseProvider {
// 检查是否是模型组合
if (provider.type === 'deepclaude') {
// 从localStorage获取模型组合配置
const savedCombinations = localStorage.getItem('modelCombinations')
if (savedCombinations) {
try {
const combinations = JSON.parse(savedCombinations) as ModelCombination[]
// 查找与当前选择的模型ID匹配的组合
// 注意在新的实现中所有模型组合共享同一个provider但每个模型有自己的ID
// 我们需要找到当前选择的模型对应的组合
const selectedModelId = provider.models.length > 0 ? provider.models[0].id : null
// 如果没有选择模型,使用第一个激活的组合
let combination: ModelCombination | undefined = undefined
if (selectedModelId) {
// 在provider的models中查找匹配的模型
const selectedModel = provider.models.find(m => m.id === selectedModelId)
if (selectedModel) {
// 直接使用模型ID查找对应的组合
// 在DeepClaude中模型ID就是组合ID
combination = combinations.find(c => c.id === selectedModelId && c.isActive)
}
}
// 如果没有找到匹配的组合,使用第一个激活的组合
if (!combination) {
combination = combinations.find(c => c.isActive) || undefined
}
if (combination) {
console.log('[ProviderFactory] 创建DeepClaudeProvider使用模型组合:', combination.name,
'推理模型:', combination.reasonerModel?.name,
'目标模型:', combination.targetModel?.name)
// 确保reasonerModel和targetModel是完整的模型对象
const allProviders = store.getState().llm.providers
// 查找完整的推理模型
const reasonerModel = combination.reasonerModel
const reasonerProvider = allProviders.find((p: Provider) =>
p.models.some((m: Model) => m.id === reasonerModel.id)
)
if (!reasonerProvider) {
console.error('[ProviderFactory] 无法找到推理模型的提供商:', reasonerModel.id)
return new OpenAIProvider(provider)
}
const fullReasonerModel = reasonerProvider.models.find((m: Model) => m.id === reasonerModel.id)
if (!fullReasonerModel) {
console.error('[ProviderFactory] 无法找到推理模型:', reasonerModel.id)
return new OpenAIProvider(provider)
}
// 查找完整的目标模型
const targetModel = combination.targetModel
const targetProvider = allProviders.find((p: Provider) =>
p.models.some((m: Model) => m.id === targetModel.id)
)
if (!targetProvider) {
console.error('[ProviderFactory] 无法找到目标模型的提供商:', targetModel.id)
return new OpenAIProvider(provider)
}
const fullTargetModel = targetProvider.models.find((m: Model) => m.id === targetModel.id)
if (!fullTargetModel) {
console.error('[ProviderFactory] 无法找到目标模型:', targetModel.id)
return new OpenAIProvider(provider)
}
// 创建完整的模型组合
const fullCombination: ModelCombination = {
id: combination.id,
name: combination.name,
isActive: combination.isActive,
reasonerModel: fullReasonerModel,
targetModel: fullTargetModel
}
console.log('[ProviderFactory] 创建完整的模型组合:',
fullCombination.id, fullCombination.name,
'推理模型:', fullCombination.reasonerModel.id, fullCombination.reasonerModel.name,
'目标模型:', fullCombination.targetModel.id, fullCombination.targetModel.name)
return new DeepClaudeProvider(provider, fullCombination)
}
} catch (e) {
console.error('[ProviderFactory] Failed to parse model combinations:', e)
}
}
// 如果找不到匹配的组合使用默认的OpenAI提供商
console.error('[ProviderFactory] 无法找到匹配的模型组合使用默认的OpenAI提供商')
return new OpenAIProvider(provider)
}
// 处理常规提供商
switch (provider.type) { switch (provider.type) {
case 'anthropic': case 'anthropic':
return new AnthropicProvider(provider) return new AnthropicProvider(provider)

View File

@ -6,6 +6,7 @@ import i18n from '@renderer/i18n'
class ASRServerService { class ASRServerService {
private serverProcess: any = null private serverProcess: any = null
private isServerRunning = false private isServerRunning = false
private serverPort: number = 34515 // 默认端口
/** /**
* ASR服务器 * ASR服务器
@ -34,7 +35,13 @@ class ASRServerService {
if (result.success) { if (result.success) {
this.isServerRunning = true this.isServerRunning = true
this.serverProcess = result.pid this.serverProcess = result.pid
console.log('[ASRServerService] ASR服务器启动成功PID:', result.pid) // 如果返回了端口号,则更新端口
if (result.port) {
this.serverPort = result.port
console.log('[ASRServerService] ASR服务器启动成功PID:', result.pid, '端口:', result.port)
} else {
console.log('[ASRServerService] ASR服务器启动成功PID:', result.pid, '使用默认端口:', this.serverPort)
}
if (window.message) { if (window.message) {
window.message.success({ content: i18n.t('settings.asr.server.started'), key: 'asr-server' }) window.message.success({ content: i18n.t('settings.asr.server.started'), key: 'asr-server' })
} }
@ -126,8 +133,11 @@ class ASRServerService {
* @returns string URL * @returns string URL
*/ */
getServerUrl = (): string => { getServerUrl = (): string => {
console.log('[ASRServerService] 获取服务器URL: http://localhost:34515') // 将端口保存到localStorage中便于浏览器页面读取
return 'http://localhost:34515' localStorage.setItem('asr-server-port', this.serverPort.toString())
const url = `http://localhost:${this.serverPort}`
console.log('[ASRServerService] 获取服务器URL:', url)
return url
} }
/** /**

View File

@ -1,5 +1,6 @@
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import ASRServerService from './ASRServerService'
/** /**
* ASR服务 * ASR服务
@ -66,7 +67,11 @@ class ASRService {
console.log('[ASRService] 正在连接WebSocket服务器...') console.log('[ASRService] 正在连接WebSocket服务器...')
window.message.loading({ content: '正在连接语音识别服务...', key: 'ws-connect' }) window.message.loading({ content: '正在连接语音识别服务...', key: 'ws-connect' })
this.ws = new WebSocket('ws://localhost:34515') // 使用正确的端口 34515 // 使用ASRServerService获取正确的端口
const serverUrl = ASRServerService.getServerUrl()
const wsUrl = serverUrl.replace('http://', 'ws://')
console.log('[ASRService] 连接到WebSocket服务器:', wsUrl)
this.ws = new WebSocket(wsUrl)
this.wsConnected = false this.wsConnected = false
this.browserReady = false this.browserReady = false
@ -76,6 +81,23 @@ class ASRService {
this.wsConnected = true this.wsConnected = true
this.reconnectAttempt = 0 this.reconnectAttempt = 0
this.ws?.send(JSON.stringify({ type: 'identify', role: 'electron' })) this.ws?.send(JSON.stringify({ type: 'identify', role: 'electron' }))
// 在WebSocket连接成功后自动打开浏览器页面
try {
const serverUrl = ASRServerService.getServerUrl()
console.log('自动打开语音识别服务器页面:', serverUrl)
window.open(serverUrl, '_blank')
// 延迟设置browserReady标志给浏览器页面足够的时间加载和连接
setTimeout(() => {
this.browserReady = true
console.log('[ASRService] 浏览器页面已就绪(延迟设置)')
window.message.success({ content: '语音识别浏览器已就绪', key: 'browser-status' })
}, 3000) // 给浏览器页面 3 秒时间加载和连接
} catch (error) {
console.error('打开语音识别浏览器页面失败:', error)
}
resolve(true) resolve(true)
} }
@ -112,13 +134,14 @@ class ASRService {
if (data.type === 'status') { if (data.type === 'status') {
if (data.message === 'browser_ready' || data.message === 'Browser connected') { if (data.message === 'browser_ready' || data.message === 'Browser connected') {
console.log('[ASRService] 浏览器已准备好') console.log('[ASRService] 浏览器已准备好 (来自服务器消息)')
this.browserReady = true this.browserReady = true
window.message.success({ content: '语音识别浏览器已准备好', key: 'browser-status' }) window.message.success({ content: '语音识别浏览器已准备好', key: 'browser-status' })
} else if (data.message === 'Browser disconnected' || data.message === 'Browser connection error') { } else if (data.message === 'Browser disconnected' || data.message === 'Browser connection error') {
console.log('[ASRService] 浏览器断开连接') console.log('[ASRService] 浏览器断开连接 (来自服务器消息)')
this.browserReady = false // 不设置 browserReady = false避免影响当前录音
window.message.error({ content: '语音识别浏览器断开连接', key: 'browser-status' }) // this.browserReady = false
window.message.warning({ content: '语音识别浏览器可能已断开连接,但当前录音不受影响', key: 'browser-status' })
} else if (data.message === 'stopped') { } else if (data.message === 'stopped') {
// 语音识别已停止 // 语音识别已停止
console.log('[ASRService] 语音识别已停止') console.log('[ASRService] 语音识别已停止')
@ -258,48 +281,20 @@ class ASRService {
// 检查浏览器是否准备好 // 检查浏览器是否准备好
if (!this.browserReady) { if (!this.browserReady) {
// 尝试等待浏览器准备好 // 如果浏览器还没有准备好,等待一下
let waitAttempts = 0
const maxWaitAttempts = 5
// 尝试打开浏览器页面
try {
// 发送消息提示用户
window.message.info({
content: '正在准备语音识别服务...',
key: 'browser-status'
})
// 尝试自动打开浏览器页面
try {
// 使用ASRServerService获取服务器URL
const serverUrl = 'http://localhost:34515' // 使用正确的端口 34515
console.log('尝试打开语音识别服务器页面:', serverUrl)
window.open(serverUrl, '_blank')
} catch (error) {
console.error('获取服务器URL失败:', error)
}
} catch (error) {
console.error('打开语音识别浏览器页面失败:', error)
}
while (!this.browserReady && waitAttempts < maxWaitAttempts) {
window.message.loading({ window.message.loading({
content: `等待浏览器准备就绪 (${waitAttempts + 1}/${maxWaitAttempts})...`, content: '正在等待浏览器准备就绪...',
key: 'browser-status' key: 'browser-status'
}) })
// 等待一秒 // 等待 2 秒
await new Promise((resolve) => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 2000))
waitAttempts++
}
// 如果还是没有准备好,就强制设置为就绪
if (!this.browserReady) { if (!this.browserReady) {
window.message.warning({ this.browserReady = true
content: '语音识别浏览器尚未准备好,请确保已打开浏览器页面', console.log('[ASRService] 强制设置浏览器就绪状态')
key: 'browser-status' window.message.success({ content: '语音识别浏览器已就绪', key: 'browser-status' })
})
throw new Error('浏览器尚未准备好')
} }
} }

View File

@ -49,6 +49,10 @@ export async function fetchChatCompletion({
onResponse: (message: Message) => void onResponse: (message: Message) => void
}) { }) {
const provider = getAssistantProvider(assistant) const provider = getAssistantProvider(assistant)
console.log('[fetchChatCompletion] 使用提供商:', provider.id, provider.name, provider.type)
if (assistant.model) {
console.log('[fetchChatCompletion] 使用模型:', assistant.model.id, assistant.model.name, assistant.model.provider)
}
const webSearchProvider = WebSearchService.getWebSearchProvider() const webSearchProvider = WebSearchService.getWebSearchProvider()
const AI = new AiProvider(provider) const AI = new AiProvider(provider)
@ -130,6 +134,13 @@ export async function fetchChatCompletion({
let _messages: Message[] = [] let _messages: Message[] = []
let isFirstChunk = true let isFirstChunk = true
// chunk buffer相关变量用于合并 chunk 减轻主线程压力。
let _bufferedText = ''
let _bufferTimer: NodeJS.Timeout | null = null
const CHUNK_BUFFER_INTERVAL = 33 // 毫秒
const CHUNK_BUFFER_SIZE = 100 // 字符数
const CHUNK_SEMBOUNDARY_REGEX = /[.!?。!?\n]$/
// Search web // Search web
await searchTheWeb() await searchTheWeb()
@ -178,6 +189,13 @@ export async function fetchChatCompletion({
if (isFirstChunk) { if (isFirstChunk) {
isFirstChunk = false isFirstChunk = false
} }
// 累积文本到缓冲区
_bufferedText += text || ''
if (reasoning_content) {
_bufferedText += reasoning_content || ''
}
message.content = message.content + text || '' message.content = message.content + text || ''
message.usage = usage message.usage = usage
message.metrics = metrics message.metrics = metrics
@ -246,11 +264,51 @@ export async function fetchChatCompletion({
} }
} }
// 设置更新条件
const shouldUpdate =
_bufferedText.length >= CHUNK_BUFFER_SIZE || // 大小阈值
(text && CHUNK_SEMBOUNDARY_REGEX.test(text)) || // 正文语义边界
(reasoning_content && CHUNK_SEMBOUNDARY_REGEX.test(reasoning_content)) || // 推理内容语义边界
!text || // 可能是结束信号
citations ||
annotations || // 重要元数据
mcpToolResponse ||
generateImage // 工具响应或图像生成
if (shouldUpdate) {
if (_bufferTimer) {
clearTimeout(_bufferTimer)
_bufferTimer = null
}
onResponse({ ...message, status: 'pending' }) onResponse({ ...message, status: 'pending' })
_bufferedText = ''
} else if (!_bufferTimer) {
// 确保即使没达到条件也会更新
_bufferTimer = setTimeout(() => {
if (_bufferedText) {
onResponse({ ...message, status: 'pending' })
_bufferedText = ''
}
_bufferTimer = null
}, CHUNK_BUFFER_INTERVAL)
}
}, },
mcpTools: mcpTools mcpTools: mcpTools
}) })
// 确保定时器被清理
if (_bufferTimer) {
clearTimeout(_bufferTimer)
_bufferTimer = null
// 如果还有未发送的缓冲文本,发送一次
if (_bufferedText) {
onResponse({ ...message, status: 'pending' })
}
}
message.status = 'success' message.status = 'success'
message = withGenerateImage(message) message = withGenerateImage(message)

View File

@ -0,0 +1,278 @@
// src/renderer/src/services/AssistantMemoryService.ts
import { fetchGenerate } from '@renderer/services/ApiService'
import store from '@renderer/store'
import { addAssistantMemory, saveMemoryData } from '@renderer/store/memory'
import { Message } from '@renderer/types'
/**
*
* @param assistantId ID
* @param messages
* @returns
*/
export const analyzeAndAddAssistantMemories = async (assistantId: string, messages: Message[]): Promise<boolean> => {
// 获取当前状态
const state = store.getState()
const assistantMemoryActive = state.memory?.assistantMemoryActive
const assistantMemoryAnalyzeModel = state.memory?.assistantMemoryAnalyzeModel
const filterSensitiveInfo = state.memory?.filterSensitiveInfo ?? true
// 检查功能是否启用
if (!assistantMemoryActive || !assistantMemoryAnalyzeModel) {
console.log('[Assistant Memory Analysis] Assistant memory feature is not active or no model selected')
return false
}
// 获取当前助手的记忆
const assistantMemories = state.memory?.assistantMemories || []
const currentAssistantMemories = assistantMemories.filter(memory => memory.assistantId === assistantId)
// 获取已分析过的消息ID
const analyzedMessageIds = new Set<string>()
currentAssistantMemories.forEach(memory => {
if (memory.analyzedMessageIds) {
memory.analyzedMessageIds.forEach(id => analyzedMessageIds.add(id))
}
})
// 过滤出未分析的消息
const newMessages = messages.filter(msg =>
msg.id && !analyzedMessageIds.has(msg.id) && msg.content && msg.content.trim() !== ''
)
if (newMessages.length === 0) {
console.log('[Assistant Memory Analysis] No new messages to analyze')
return false
}
console.log(`[Assistant Memory Analysis] Found ${newMessages.length} new messages to analyze.`)
// 构建新消息的对话内容
const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
// 获取已有的助手记忆内容
const existingMemoriesContent = currentAssistantMemories.map((memory) => memory.content).join('\n')
try {
console.log('[Assistant Memory Analysis] Starting analysis...')
console.log(`[Assistant Memory Analysis] Analyzing assistant: ${assistantId}`)
console.log('[Assistant Memory Analysis] New conversation length:', newConversation.length)
// 构建助手记忆分析提示词
let prompt = `
1.
2.
3.
4.
5.
${filterSensitiveInfo ? '6. 不要提取任何敏感信息如API密钥、密码、个人身份信息等' : ''}
${existingMemoriesContent ? `已有的助手记忆:\n${existingMemoriesContent}\n\n` : ''}
:
${newConversation}
JSON数组格式返回提取的记忆
["用户喜欢简洁的回答", "用户对技术话题特别感兴趣", "用户希望得到具体的代码示例"]
[]
`
// 获取模型
let modelId = assistantMemoryAnalyzeModel
let providerId = ''
// 尝试解析JSON格式的模型ID
if (typeof assistantMemoryAnalyzeModel === 'string') {
if (assistantMemoryAnalyzeModel.startsWith('{')) {
try {
const parsedModel = JSON.parse(assistantMemoryAnalyzeModel)
modelId = parsedModel.id
providerId = parsedModel.provider
console.log(`[Assistant Memory Analysis] Using model ${modelId} from provider ${providerId}`)
} catch (error) {
console.error('[Assistant Memory Analysis] Failed to parse model ID:', error)
}
} else {
// 如果不是JSON格式直接使用字符串作为模型ID
modelId = assistantMemoryAnalyzeModel
console.log(`[Assistant Memory Analysis] Using model ID directly: ${modelId}`)
}
}
// 先尝试根据供应商和模型ID查找
let model: any = null
if (providerId) {
const provider = state.llm.providers.find(p => p.id === providerId)
if (provider) {
const foundModel = provider.models.find(m => m.id === modelId)
if (foundModel) {
model = foundModel
}
}
}
// 如果没找到,尝试在所有模型中查找
if (!model) {
const foundModel = state.llm.providers
.flatMap((provider) => provider.models)
.find((m) => m.id === modelId)
if (foundModel) {
model = foundModel
}
}
if (!model) {
console.error(`[Assistant Memory Analysis] Model ${assistantMemoryAnalyzeModel} not found`)
return false
}
// 调用AI生成文本
console.log('[Assistant Memory Analysis] Calling AI.generateText...')
const result = await fetchGenerate({
prompt: prompt,
content: newConversation,
modelId: model.id
})
console.log('[Assistant Memory Analysis] AI.generateText response received')
if (!result || typeof result !== 'string' || result.trim() === '') {
console.log('[Assistant Memory Analysis] No valid result from AI analysis.')
return false
}
// 解析结果
let memories: string[] = []
try {
// 尝试直接解析JSON
const jsonMatch = result.match(/\[[\s\S]*\]/)
if (jsonMatch) {
memories = JSON.parse(jsonMatch[0])
} else {
// 如果没有找到JSON数组尝试按行分割并处理
memories = result
.split('\n')
.filter(line => line.trim().startsWith('"') || line.trim().startsWith('-'))
.map(line => line.trim().replace(/^["'\-\s]+|["'\s]+$/g, ''))
}
} catch (error) {
console.error('[Assistant Memory Analysis] Failed to parse memories:', error)
// 尝试使用正则表达式提取引号中的内容
const quotedStrings = result.match(/"([^"]*)"/g)
if (quotedStrings) {
memories = quotedStrings.map(str => str.slice(1, -1))
} else {
// 最后尝试按行分割
memories = result
.split('\n')
.filter(line => line.trim() && !line.includes('```'))
.map(line => line.trim().replace(/^["'\-\s]+|["'\s]+$/g, ''))
}
}
// 过滤空字符串和已存在的记忆
memories = memories.filter(
memory =>
memory &&
memory.trim() !== '' &&
!currentAssistantMemories.some(m => m.content.toLowerCase() === memory.toLowerCase())
)
console.log(`[Assistant Memory Analysis] Extracted ${memories.length} new memories`)
// 添加新记忆
const addedMemories: string[] = []
const newMessageIds = newMessages.map(msg => msg.id).filter(Boolean) as string[]
const lastMessageId = newMessages.length > 0 ? newMessages[newMessages.length - 1].id : undefined
for (const memoryContent of memories) {
// 添加到Redux状态
store.dispatch(
addAssistantMemory({
content: memoryContent,
assistantId,
analyzedMessageIds: newMessageIds,
lastMessageId
})
)
addedMemories.push(memoryContent)
}
// 显式触发保存操作,确保数据被持久化,并强制覆盖
try {
const state = store.getState().memory
await store
.dispatch(
saveMemoryData({
assistantMemories: state.assistantMemories,
assistantMemoryActive: state.assistantMemoryActive,
assistantMemoryAnalyzeModel: state.assistantMemoryAnalyzeModel,
forceOverwrite: true // 强制覆盖文件,确保数据正确保存
})
)
.unwrap()
console.log('[Assistant Memory Analysis] Memory data saved successfully (force overwrite)')
} catch (error) {
console.error('[Assistant Memory Analysis] Failed to save memory data:', error)
// 即使保存失败我们仍然返回true因为记忆已经添加到Redux状态中
}
return addedMemories.length > 0
} catch (error) {
console.error('[Assistant Memory Analysis] Failed to analyze and add assistant memories:', error)
return false
}
}
/**
*
* @param assistantId ID
* @returns
*/
export const resetAssistantMemoryAnalyzedMessageIds = async (assistantId: string): Promise<boolean> => {
try {
// 获取当前状态
const state = store.getState()
const assistantMemories = state.memory?.assistantMemories || []
// 获取当前助手的记忆
const currentAssistantMemories = assistantMemories.filter(memory => memory.assistantId === assistantId)
if (currentAssistantMemories.length === 0) {
console.log(`[Assistant Memory] No memories found for assistant ${assistantId}`)
return false
}
// 创建新的助手记忆数组,清除分析标记
const updatedMemories = assistantMemories.map(memory => {
if (memory.assistantId === assistantId) {
return {
...memory,
analyzedMessageIds: [],
lastMessageId: undefined
}
}
return memory
})
// 保存更新后的记忆
await store.dispatch(
saveMemoryData({
assistantMemories: updatedMemories,
assistantMemoryActive: state.memory?.assistantMemoryActive,
assistantMemoryAnalyzeModel: state.memory?.assistantMemoryAnalyzeModel,
forceOverwrite: true
})
).unwrap()
console.log(`[Assistant Memory] Reset analysis markers for assistant ${assistantId}`)
return true
} catch (error) {
console.error('[Assistant Memory] Failed to reset assistant memory analyzed message IDs:', error)
return false
}
}

View File

@ -70,8 +70,44 @@ export function getTranslateModel() {
export function getAssistantProvider(assistant: Assistant): Provider { export function getAssistantProvider(assistant: Assistant): Provider {
const providers = store.getState().llm.providers const providers = store.getState().llm.providers
// 检查是否是DeepClaude模型
if (assistant.model?.provider === 'deepclaude') {
console.log('[getAssistantProvider] 检测到DeepClaude模型:', assistant.model.id, assistant.model.name)
// 列出所有提供商,便于调试
console.log('[getAssistantProvider] 当前所有提供商:',
providers.map(p => ({ id: p.id, name: p.name, type: p.type })))
// 查找所有DeepClaude类型的提供商
const deepClaudeProviders = providers.filter(p => p.type === 'deepclaude')
console.log('[getAssistantProvider] 找到DeepClaude类型的提供商数量:', deepClaudeProviders.length)
if (deepClaudeProviders.length > 0) {
// 先尝试查找与model.id匹配的提供商
const matchingProvider = deepClaudeProviders.find(p => p.id === assistant.model?.id)
if (matchingProvider) {
console.log('[getAssistantProvider] 找到匹配的DeepClaude提供商:', matchingProvider.id, matchingProvider.name)
return matchingProvider
}
// 如果没有找到匹配的使用第一个DeepClaude提供商
console.log('[getAssistantProvider] 使用第一个DeepClaude提供商:', deepClaudeProviders[0].id, deepClaudeProviders[0].name)
return deepClaudeProviders[0]
}
console.log('[getAssistantProvider] 未找到DeepClaude提供商将使用默认提供商')
}
// 常规模型处理
const provider = providers.find((p) => p.id === assistant.model?.provider) const provider = providers.find((p) => p.id === assistant.model?.provider)
return provider || getDefaultProvider() if (provider) {
return provider
}
// 如果没有找到提供商,使用默认提供商
console.log('[getAssistantProvider] 未找到提供商,使用默认提供商')
return getDefaultProvider()
} }
export function getProviderByModel(model?: Model): Provider { export function getProviderByModel(model?: Model): Provider {

View File

@ -23,28 +23,28 @@ export const analyzeAndSelectHistoricalContext = async (
const isEnabled = state.settings?.enableHistoricalContext ?? false const isEnabled = state.settings?.enableHistoricalContext ?? false
if (!isEnabled) { if (!isEnabled) {
console.log('[HistoricalContext] Feature is disabled') // 减少日志输出
return null return null
} }
// 2. 获取最近的消息 // 2. 获取最近的消息
const recentMessages = await getRecentMessages(topicId, recentMessageCount) const recentMessages = await getRecentMessages(topicId, recentMessageCount)
if (!recentMessages || recentMessages.length === 0) { if (!recentMessages || recentMessages.length === 0) {
console.log('[HistoricalContext] No recent messages found') // 减少日志输出
return null return null
} }
// 3. 获取所有短期记忆(已分析的对话) // 3. 获取所有短期记忆(已分析的对话)
const shortMemories = state.memory?.shortMemories || [] const shortMemories = state.memory?.shortMemories || []
if (shortMemories.length === 0) { if (shortMemories.length === 0) {
console.log('[HistoricalContext] No short memories available') // 减少日志输出
return null return null
} }
// 4. 使用快速模型分析是否需要历史上下文 // 4. 使用快速模型分析是否需要历史上下文
const analysisResult = await analyzeNeedForHistoricalContext(recentMessages, shortMemories) const analysisResult = await analyzeNeedForHistoricalContext(recentMessages, shortMemories)
if (!analysisResult.needsHistoricalContext) { if (!analysisResult.needsHistoricalContext) {
console.log('[HistoricalContext] Analysis indicates no need for historical context') // 减少日志输出
return null return null
} }
@ -70,7 +70,7 @@ export const analyzeAndSelectHistoricalContext = async (
return null return null
} catch (error) { } catch (error) {
console.error('[HistoricalContext] Error analyzing and selecting historical context:', error) // 静默处理错误,减少日志输出
return null return null
} }
} }
@ -97,7 +97,7 @@ const getRecentMessages = async (topicId: string, count: number): Promise<Messag
// 返回最近的count条消息 // 返回最近的count条消息
return messages.slice(-count) return messages.slice(-count)
} catch (error) { } catch (error) {
console.error('[HistoricalContext] Error getting recent messages:', error) // 静默处理错误,减少日志输出
return [] return []
} }
} }
@ -157,12 +157,11 @@ ${memoriesContent}
state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel state.memory?.historicalContextAnalyzeModel || state.memory?.shortMemoryAnalyzeModel || state.memory?.analyzeModel
if (!analyzeModel) { if (!analyzeModel) {
console.log('[HistoricalContext] No analyze model set') // 减少日志输出
return { needsHistoricalContext: false } return { needsHistoricalContext: false }
} }
// 调用模型进行分析 // 调用模型进行分析
console.log('[HistoricalContext] Calling AI model for analysis...')
const result = await fetchGenerate({ const result = await fetchGenerate({
prompt, prompt,
content: '', content: '',
@ -170,7 +169,7 @@ ${memoriesContent}
}) })
if (!result) { if (!result) {
console.log('[HistoricalContext] No result from AI analysis') // 减少日志输出
return { needsHistoricalContext: false } return { needsHistoricalContext: false }
} }
@ -195,7 +194,7 @@ ${memoriesContent}
reason: extractedJson.reason reason: extractedJson.reason
} }
} catch (extractError) { } catch (extractError) {
console.error('[HistoricalContext] Failed to extract JSON from result:', extractError) // 静默处理错误,减少日志输出
} }
} }
@ -211,7 +210,7 @@ ${memoriesContent}
} }
} }
} catch (error) { } catch (error) {
console.error('[HistoricalContext] Error analyzing need for historical context:', error) // 静默处理错误,减少日志输出
return { needsHistoricalContext: false } return { needsHistoricalContext: false }
} }
} }
@ -224,7 +223,7 @@ const getOriginalDialogContent = async (topicId: string): Promise<string | null>
// 获取话题的原始消息 // 获取话题的原始消息
const messages = await TopicManager.getTopicMessages(topicId) const messages = await TopicManager.getTopicMessages(topicId)
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
console.log(`[HistoricalContext] No messages found for topic ${topicId}`) // 减少日志输出
return null return null
} }
@ -233,7 +232,7 @@ const getOriginalDialogContent = async (topicId: string): Promise<string | null>
return dialogContent return dialogContent
} catch (error) { } catch (error) {
console.error('[HistoricalContext] Error getting original dialog content:', error) // 静默处理错误,减少日志输出
return null return null
} }
} }

View File

@ -11,6 +11,7 @@ import {
addAnalysisLatency, addAnalysisLatency,
addMemory, addMemory,
addShortMemory, addShortMemory,
addAssistantMemory,
clearCurrentRecommendations, clearCurrentRecommendations,
Memory, Memory,
MemoryRecommendation, MemoryRecommendation,
@ -28,8 +29,10 @@ import { Message } from '@renderer/types' // Import Message type
import { useCallback, useEffect, useRef } from 'react' // Add useRef back import { useCallback, useEffect, useRef } from 'react' // Add useRef back
import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service import { contextualMemoryService } from './ContextualMemoryService' // Import contextual memory service
import { analyzeAndAddAssistantMemories, resetAssistantMemoryAnalyzedMessageIds } from './AssistantMemoryService' // Import assistant memory service
// 计算对话复杂度,用于调整分析深度 // calculateConversationComplexity is unused, removing its definition
/*
const calculateConversationComplexity = (conversation: string): 'low' | 'medium' | 'high' => { const calculateConversationComplexity = (conversation: string): 'low' | 'medium' | 'high' => {
const wordCount = conversation.split(/\s+/).length const wordCount = conversation.split(/\s+/).length
const sentenceCount = conversation.split(/[.!?]+/).length const sentenceCount = conversation.split(/[.!?]+/).length
@ -44,6 +47,7 @@ const calculateConversationComplexity = (conversation: string): 'low' | 'medium'
return 'medium' return 'medium'
} }
} }
*/
// 根据分析深度调整提示词 // 根据分析深度调整提示词
// 注意:该函数当前未使用,保留供将来可能的功能扩展 // 注意:该函数当前未使用,保留供将来可能的功能扩展
@ -495,7 +499,7 @@ export const useMemoryService = () => {
return return
} }
console.log(`[Memory Analysis] Found ${newMessages.length} new messages to analyze.`) // 减少日志输出
// 构建新消息的对话内容 // 构建新消息的对话内容
const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n') const newConversation = newMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
@ -506,7 +510,7 @@ export const useMemoryService = () => {
.join('\n') .join('\n')
if (!newConversation) { if (!newConversation) {
console.log('[Memory Analysis] No conversation content to analyze.') // 减少日志输出
return return
} }
@ -515,19 +519,16 @@ export const useMemoryService = () => {
const startTime = performance.now() const startTime = performance.now()
dispatch(setAnalyzing(true)) dispatch(setAnalyzing(true))
console.log('[Memory Analysis] Starting analysis...')
console.log(`[Memory Analysis] Analyzing topic: ${targetTopicId}`)
console.log('[Memory Analysis] Conversation length:', newConversation.length)
// 自适应分析:根据对话复杂度调整分析深度 // 自适应分析:根据对话复杂度调整分析深度 (analysisDepth is unused, removing related code)
const conversationComplexity = calculateConversationComplexity(newConversation) // const conversationComplexity = calculateConversationComplexity(newConversation)
let analysisDepth = memoryState.analysisDepth || 'medium' // let analysisDepth = memoryState.analysisDepth || 'medium'
// 如果启用了自适应分析,根据复杂度调整深度 // 如果启用了自适应分析,根据复杂度调整深度 (analysisDepth is unused, removing related code)
if (memoryState.adaptiveAnalysisEnabled) { // if (memoryState.adaptiveAnalysisEnabled) {
analysisDepth = conversationComplexity // analysisDepth = conversationComplexity
console.log(`[Memory Analysis] Adjusted analysis depth to ${analysisDepth} based on conversation complexity`) // // 减少日志输出
} // }
// 构建长期记忆分析提示词,包含已有记忆 // 构建长期记忆分析提示词,包含已有记忆
const basePrompt = ` const basePrompt = `
@ -793,8 +794,8 @@ ${existingMemoriesContent}
}, [analyzeAndAddMemories]) }, [analyzeAndAddMemories])
// 记录记忆访问 // 记录记忆访问
const recordMemoryAccess = useCallback((memoryId: string, isShortMemory: boolean = false) => { const recordMemoryAccess = useCallback((memoryId: string, isShortMemory: boolean = false, isAssistantMemory: boolean = false) => {
store.dispatch(accessMemory({ id: memoryId, isShortMemory })) store.dispatch(accessMemory({ id: memoryId, isShortMemory, isAssistantMemory }))
}, []) }, [])
// Effect 来设置/清除定时器,只依赖于启动条件 // Effect 来设置/清除定时器,只依赖于启动条件
@ -1009,6 +1010,45 @@ export const addMemoryItem = async (
} }
} }
// 手动添加助手记忆
export const addAssistantMemoryItem = async (
content: string,
assistantId: string,
analyzedMessageIds?: string[],
lastMessageId?: string
) => {
// Use imported store directly
store.dispatch(
addAssistantMemory({
content,
assistantId,
analyzedMessageIds,
lastMessageId
})
)
// 保存到文件,并强制覆盖
try {
const state = store.getState().memory
await store
.dispatch(
saveMemoryData({
assistantMemories: state.assistantMemories,
assistantMemoryActive: state.assistantMemoryActive,
assistantMemoryAnalyzeModel: state.assistantMemoryAnalyzeModel,
forceOverwrite: true // 强制覆盖文件,确保数据正确保存
})
)
.unwrap()
console.log('[Memory] Assistant memory saved to file after manual addition (force overwrite)')
} catch (error) {
console.error('[Memory] Failed to save assistant memory data after manual addition:', error)
}
}
// 导出助手记忆分析函数
export { analyzeAndAddAssistantMemories, resetAssistantMemoryAnalyzedMessageIds }
/** /**
* *
* @param topicId ID * @param topicId ID
@ -1259,10 +1299,43 @@ ${newConversation}
` `
// 获取模型 // 获取模型
const model = store let modelId = shortMemoryAnalyzeModel
let providerId = ''
// 尝试解析JSON格式的模型ID
if (typeof shortMemoryAnalyzeModel === 'string' && shortMemoryAnalyzeModel.startsWith('{')) {
try {
const parsedModel = JSON.parse(shortMemoryAnalyzeModel)
modelId = parsedModel.id
providerId = parsedModel.provider
console.log(`[Short Memory Analysis] Using model ${modelId} from provider ${providerId}`)
} catch (error) {
console.error('[Short Memory Analysis] Failed to parse model ID:', error)
}
}
// 先尝试根据供应商和模型ID查找
let model: any = null
if (providerId) {
const provider = store.getState().llm.providers.find(p => p.id === providerId)
if (provider) {
const foundModel = provider.models.find(m => m.id === modelId)
if (foundModel) {
model = foundModel
}
}
}
// 如果没找到,尝试在所有模型中查找
if (!model) {
const foundModel = store
.getState() .getState()
.llm.providers.flatMap((provider) => provider.models) .llm.providers.flatMap((provider) => provider.models)
.find((model) => model.id === shortMemoryAnalyzeModel) .find((m) => m.id === modelId)
if (foundModel) {
model = foundModel
}
}
if (!model) { if (!model) {
console.error(`[Short Memory Analysis] Model ${shortMemoryAnalyzeModel} not found`) console.error(`[Short Memory Analysis] Model ${shortMemoryAnalyzeModel} not found`)
@ -1274,7 +1347,7 @@ ${newConversation}
const result = await fetchGenerate({ const result = await fetchGenerate({
prompt: prompt, prompt: prompt,
content: newConversation, content: newConversation,
modelId: shortMemoryAnalyzeModel modelId: model.id
}) })
console.log('[Short Memory Analysis] AI.generateText response:', result) console.log('[Short Memory Analysis] AI.generateText response:', result)
@ -1412,6 +1485,8 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
memoryLists, memoryLists,
shortMemoryActive, shortMemoryActive,
shortMemories, shortMemories,
assistantMemoryActive,
assistantMemories,
priorityManagementEnabled, priorityManagementEnabled,
contextualRecommendationEnabled, contextualRecommendationEnabled,
currentRecommendations currentRecommendations
@ -1421,6 +1496,8 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
memoryLists: [], memoryLists: [],
shortMemoryActive: false, shortMemoryActive: false,
shortMemories: [], shortMemories: [],
assistantMemoryActive: false,
assistantMemories: [],
priorityManagementEnabled: false, priorityManagementEnabled: false,
contextualRecommendationEnabled: false, contextualRecommendationEnabled: false,
currentRecommendations: [] currentRecommendations: []
@ -1435,6 +1512,8 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
listsCount: memoryLists?.length, listsCount: memoryLists?.length,
shortMemoryActive, shortMemoryActive,
shortMemoriesCount: shortMemories?.length, shortMemoriesCount: shortMemories?.length,
assistantMemoryActive,
assistantMemoriesCount: assistantMemories?.length,
currentTopicId, currentTopicId,
priorityManagementEnabled priorityManagementEnabled
}) })
@ -1455,12 +1534,21 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
memory = memories.find((m) => m.id === recommendation.memoryId) memory = memories.find((m) => m.id === recommendation.memoryId)
} else if (recommendation.source === 'short-term') { } else if (recommendation.source === 'short-term') {
memory = shortMemories.find((m) => m.id === recommendation.memoryId) memory = shortMemories.find((m) => m.id === recommendation.memoryId)
} else if (recommendation.source === 'assistant') {
memory = assistantMemories.find((m) => m.id === recommendation.memoryId)
} }
if (memory) { if (memory) {
let sourceLabel = '长期记忆' // 默认为长期记忆
if (recommendation.source === 'short-term') {
sourceLabel = '短期记忆'
} else if (recommendation.source === 'assistant') {
sourceLabel = '助手记忆'
}
recommendedMemories.push({ recommendedMemories.push({
content: memory.content, content: memory.content,
source: recommendation.source === 'long-term' ? '长期记忆' : '短期记忆', source: sourceLabel,
reason: recommendation.matchReason || '与当前对话相关' reason: recommendation.matchReason || '与当前对话相关'
}) })
@ -1468,7 +1556,8 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
store.dispatch( store.dispatch(
accessMemory({ accessMemory({
id: memory.id, id: memory.id,
isShortMemory: recommendation.source === 'short-term' isShortMemory: recommendation.source === 'short-term',
isAssistantMemory: recommendation.source === 'assistant'
}) })
) )
} }
@ -1479,9 +1568,13 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
// 按重要性排序 // 按重要性排序
recommendedMemories.sort((a, b) => { recommendedMemories.sort((a, b) => {
const memoryA = const memoryA =
memories.find((m) => m.content === a.content) || shortMemories.find((m) => m.content === a.content) memories.find((m) => m.content === a.content) ||
shortMemories.find((m) => m.content === a.content) ||
assistantMemories.find((m) => m.content === a.content)
const memoryB = const memoryB =
memories.find((m) => m.content === b.content) || shortMemories.find((m) => m.content === b.content) memories.find((m) => m.content === b.content) ||
shortMemories.find((m) => m.content === b.content) ||
assistantMemories.find((m) => m.content === b.content)
const importanceA = memoryA?.importance || 0.5 const importanceA = memoryA?.importance || 0.5
const importanceB = memoryB?.importance || 0.5 const importanceB = memoryB?.importance || 0.5
return importanceB - importanceA return importanceB - importanceA
@ -1500,6 +1593,82 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
} }
} }
// 处理助手记忆
const currentAssistant = state.messages?.currentAssistant
const currentAssistantId = currentAssistant?.id
// 获取当前话题的助手ID
let topicAssistantId = currentAssistantId
if (topicId) {
try {
// 从当前状态中获取话题的助手ID
const assistants = state.assistants.assistants
for (const assistant of assistants) {
const topic = assistant.topics.find(t => t.id === topicId)
if (topic) {
topicAssistantId = assistant.id
console.log('[Memory] Using topic assistant ID:', topicAssistantId)
break
}
}
} catch (error) {
console.error('[Memory] Error getting topic assistant ID:', error)
}
}
// 使用话题助手ID或当前助手ID
const assistantIdToUse = topicAssistantId || currentAssistantId
if (assistantMemoryActive && assistantMemories && assistantMemories.length > 0 && assistantIdToUse) {
// 获取相关助手的记忆
let assistantSpecificMemories = assistantMemories.filter((memory) => memory.assistantId === assistantIdToUse)
// 如果启用了智能优先级管理,根据优先级排序
if (priorityManagementEnabled && assistantSpecificMemories.length > 0) {
// 计算每个记忆的综合分数(重要性 * 衰减因子 * 鲜度)
const scoredMemories = assistantSpecificMemories.map((memory) => {
// 记录访问
store.dispatch(accessMemory({ id: memory.id, isAssistantMemory: true }))
// 计算综合分数
const importance = memory.importance || 0.5
const decayFactor = memory.decayFactor || 1
const freshness = memory.freshness || 0.5
const score = importance * decayFactor * (freshness * 1.5) // 助手记忆的鲜度权重介于长期和短期记忆之间
return { memory, score }
})
// 按综合分数降序排序
scoredMemories.sort((a, b) => b.score - a.score)
// 提取排序后的记忆
assistantSpecificMemories = scoredMemories.map((item) => item.memory)
// 限制数量,避免提示词过长
if (assistantSpecificMemories.length > 10) {
assistantSpecificMemories = assistantSpecificMemories.slice(0, 10)
}
}
if (assistantSpecificMemories.length > 0) {
// 按重要性排序
assistantSpecificMemories.sort((a, b) => {
const importanceA = a.importance || 0.5
const importanceB = b.importance || 0.5
return importanceB - importanceA
})
// 构建助手记忆提示词
const memoryItems = assistantSpecificMemories.map((memory) => `- ${memory.content}`).join('\n')
const assistantMemoryPrompt = `作为当前助手,请记住以下重要信息:\n\n${memoryItems}`
console.log('[Memory] Assistant memory prompt:', assistantMemoryPrompt)
// 添加助手记忆到提示词
result = `${result}\n\n${assistantMemoryPrompt}`
hasContent = true
}
}
// 处理短记忆 // 处理短记忆
if (shortMemoryActive && shortMemories && shortMemories.length > 0 && currentTopicId) { if (shortMemoryActive && shortMemories && shortMemories.length > 0 && currentTopicId) {
// 获取当前话题的短记忆 // 获取当前话题的短记忆

View File

@ -18,9 +18,10 @@ class TTSService {
* *
* @param text * @param text
* @param segmented 使 * @param segmented 使
* @param messageId ID
*/ */
speak = async (text: string, segmented: boolean = false): Promise<void> => { speak = async (text: string, segmented: boolean = false, messageId?: string): Promise<void> => {
await this.service.speak(text, segmented) await this.service.speak(text, segmented, messageId)
} }
/** /**

View File

@ -160,9 +160,10 @@ export class TTSService {
* *
* @param text * @param text
* @param segmented 使 * @param segmented 使
* @param messageId ID
* @returns * @returns
*/ */
public async speak(text: string, segmented: boolean = false): Promise<boolean> { public async speak(text: string, segmented: boolean = false, messageId?: string): Promise<boolean> {
try { try {
// 检查TTS是否启用 // 检查TTS是否启用
const settings = store.getState().settings const settings = store.getState().settings
@ -197,6 +198,15 @@ export class TTSService {
// 设置分段播放模式 // 设置分段播放模式
this.isSegmentedPlayback = segmented this.isSegmentedPlayback = segmented
// 如果提供了messageId则设置playingMessageId
if (messageId) {
this.playingMessageId = messageId
// 更新最后播放的消息ID
const dispatch = store.dispatch
dispatch(setLastPlayedMessageId(messageId))
console.log('更新最后播放的消息ID:', messageId)
}
if (segmented) { if (segmented) {
// 分段播放模式 // 分段播放模式
return await this.speakSegmented(text, serviceType, latestSettings) return await this.speakSegmented(text, serviceType, latestSettings)
@ -305,8 +315,11 @@ export class TTSService {
// 停止进度更新 // 停止进度更新
this.stopProgressUpdates() this.stopProgressUpdates()
// 更新状态并触发事件 // 直接设置isPlaying为false并触发事件确保无论当前状态如何都会触发事件
this.updatePlayingState(false) this.isPlaying = false
console.log('TTS播放状态更新: 停止播放')
const event = new CustomEvent('tts-state-change', { detail: { isPlaying: false } })
window.dispatchEvent(event)
// 清除正在播放的消息ID // 清除正在播放的消息ID
this.playingMessageId = null this.playingMessageId = null

View File

@ -51,6 +51,24 @@ export interface ShortMemory {
freshness?: number // 记忆鲜度评分0-1基于创建时间和最后访问时间 freshness?: number // 记忆鲜度评分0-1基于创建时间和最后访问时间
} }
// 助手记忆项接口
export interface AssistantMemory {
id: string
content: string
createdAt: string
assistantId: string // 关联的助手ID
analyzedMessageIds?: string[] // 记录该记忆是从哪些消息中分析出来的
lastMessageId?: string // 分析时的最后一条消息的ID用于跟踪分析进度
vector?: number[] // 记忆的向量表示,用于语义搜索
entities?: string[] // 记忆中提取的实体
keywords?: string[] // 记忆中提取的关键词
importance?: number // 记忆的重要性评分0-1
accessCount?: number // 记忆被访问的次数
lastAccessedAt?: string // 记忆最后被访问的时间
decayFactor?: number // 记忆衰减因子0-1值越小衰减越快
freshness?: number // 记忆鲜度评分0-1基于创建时间和最后访问时间
}
// 分析统计数据接口 // 分析统计数据接口
export interface AnalysisStats { export interface AnalysisStats {
totalAnalyses: number // 总分析次数 totalAnalyses: number // 总分析次数
@ -80,7 +98,7 @@ export interface UserInterest {
export interface MemoryRecommendation { export interface MemoryRecommendation {
memoryId: string memoryId: string
relevanceScore: number relevanceScore: number
source: 'long-term' | 'short-term' source: 'long-term' | 'short-term' | 'assistant'
matchReason?: string matchReason?: string
} }
@ -88,13 +106,16 @@ export interface MemoryState {
memoryLists: MemoryList[] // 记忆列表 memoryLists: MemoryList[] // 记忆列表
memories: Memory[] // 所有记忆项 memories: Memory[] // 所有记忆项
shortMemories: ShortMemory[] // 短记忆项 shortMemories: ShortMemory[] // 短记忆项
assistantMemories: AssistantMemory[] // 助手记忆项
currentListId: string | null // 当前选中的记忆列表ID currentListId: string | null // 当前选中的记忆列表ID
isActive: boolean // 记忆功能是否激活 isActive: boolean // 记忆功能是否激活
shortMemoryActive: boolean // 短记忆功能是否激活 shortMemoryActive: boolean // 短记忆功能是否激活
assistantMemoryActive: boolean // 助手记忆功能是否激活
autoAnalyze: boolean // 是否自动分析 autoAnalyze: boolean // 是否自动分析
filterSensitiveInfo: boolean // 是否过滤敏感信息 filterSensitiveInfo: boolean // 是否过滤敏感信息
analyzeModel: string | null // 用于长期记忆分析的模型ID analyzeModel: string | null // 用于长期记忆分析的模型ID
shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID shortMemoryAnalyzeModel: string | null // 用于短期记忆分析的模型ID
assistantMemoryAnalyzeModel: string | null // 用于助手记忆分析的模型ID
historicalContextAnalyzeModel: string | null // 用于历史对话上下文分析的模型ID historicalContextAnalyzeModel: string | null // 用于历史对话上下文分析的模型ID
vectorizeModel: string | null // 用于向量化的模型ID vectorizeModel: string | null // 用于向量化的模型ID
lastAnalyzeTime: number | null // 上次分析时间 lastAnalyzeTime: number | null // 上次分析时间
@ -144,13 +165,16 @@ const initialState: MemoryState = {
memoryLists: [defaultList], memoryLists: [defaultList],
memories: [], memories: [],
shortMemories: [], // 初始化空的短记忆数组 shortMemories: [], // 初始化空的短记忆数组
assistantMemories: [], // 初始化空的助手记忆数组
currentListId: defaultList.id, currentListId: defaultList.id,
isActive: true, isActive: true,
shortMemoryActive: true, // 默认启用短记忆功能 shortMemoryActive: true, // 默认启用短记忆功能
assistantMemoryActive: true, // 默认启用助手记忆功能
autoAnalyze: true, autoAnalyze: true,
filterSensitiveInfo: true, // 默认启用敏感信息过滤 filterSensitiveInfo: true, // 默认启用敏感信息过滤
analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型 analyzeModel: 'gpt-3.5-turbo', // 设置默认长期记忆分析模型
shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型 shortMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认短期记忆分析模型
assistantMemoryAnalyzeModel: 'gpt-3.5-turbo', // 设置默认助手记忆分析模型
historicalContextAnalyzeModel: 'gpt-3.5-turbo', // 设置默认历史对话上下文分析模型 historicalContextAnalyzeModel: 'gpt-3.5-turbo', // 设置默认历史对话上下文分析模型
vectorizeModel: 'gpt-3.5-turbo', // 设置默认向量化模型 vectorizeModel: 'gpt-3.5-turbo', // 设置默认向量化模型
lastAnalyzeTime: null, lastAnalyzeTime: null,
@ -304,6 +328,11 @@ const memorySlice = createSlice({
state.shortMemoryAnalyzeModel = action.payload state.shortMemoryAnalyzeModel = action.payload
}, },
// 设置助手记忆分析模型
setAssistantMemoryAnalyzeModel: (state, action: PayloadAction<string | null>) => {
state.assistantMemoryAnalyzeModel = action.payload
},
// 设置历史对话上下文分析模型 // 设置历史对话上下文分析模型
setHistoricalContextAnalyzeModel: (state, action: PayloadAction<string | null>) => { setHistoricalContextAnalyzeModel: (state, action: PayloadAction<string | null>) => {
state.historicalContextAnalyzeModel = action.payload state.historicalContextAnalyzeModel = action.payload
@ -482,6 +511,37 @@ const memorySlice = createSlice({
state.shortMemories.push(newShortMemory) state.shortMemories.push(newShortMemory)
}, },
// 添加助手记忆
addAssistantMemory: (
state,
action: PayloadAction<{
content: string
assistantId: string
analyzedMessageIds?: string[]
lastMessageId?: string
importance?: number // 重要性评分
keywords?: string[] // 关键词
}>
) => {
const newAssistantMemory: AssistantMemory = {
id: nanoid(),
content: action.payload.content,
createdAt: new Date().toISOString(),
assistantId: action.payload.assistantId,
analyzedMessageIds: action.payload.analyzedMessageIds,
lastMessageId: action.payload.lastMessageId,
importance: action.payload.importance,
keywords: action.payload.keywords
}
// 确保 assistantMemories 存在
if (!state.assistantMemories) {
state.assistantMemories = []
}
state.assistantMemories.push(newAssistantMemory)
},
// 删除短记忆 // 删除短记忆
deleteShortMemory: (state, action: PayloadAction<string>) => { deleteShortMemory: (state, action: PayloadAction<string>) => {
// 确保 shortMemories 存在 // 确保 shortMemories 存在
@ -555,11 +615,56 @@ const memorySlice = createSlice({
} }
}, },
// 删除助手记忆
deleteAssistantMemory: (state, action: PayloadAction<string>) => {
// 确保 assistantMemories 存在
if (!state.assistantMemories) {
state.assistantMemories = []
return
}
// 找到要删除的记忆
const memoryToDelete = state.assistantMemories.find((memory) => memory.id === action.payload)
// 如果找到了要删除的记忆并且它有分析过的消息ID
if (memoryToDelete && memoryToDelete.analyzedMessageIds && memoryToDelete.analyzedMessageIds.length > 0) {
// 记录日志,方便调试
console.log(`[Memory] Deleting assistant memory with ${memoryToDelete.analyzedMessageIds.length} analyzed message IDs`)
}
// 删除记忆
state.assistantMemories = state.assistantMemories.filter((memory) => memory.id !== action.payload)
},
// 清空指定助手的记忆
clearAssistantMemories: (state, action: PayloadAction<string | undefined>) => {
// 确保 assistantMemories 存在
if (!state.assistantMemories) {
state.assistantMemories = []
return
}
const assistantId = action.payload
if (assistantId) {
// 清空指定助手的记忆
state.assistantMemories = state.assistantMemories.filter((memory) => memory.assistantId !== assistantId)
} else {
// 清空所有助手记忆
state.assistantMemories = []
}
},
// 设置短记忆功能是否激活 // 设置短记忆功能是否激活
setShortMemoryActive: (state, action: PayloadAction<boolean>) => { setShortMemoryActive: (state, action: PayloadAction<boolean>) => {
state.shortMemoryActive = action.payload state.shortMemoryActive = action.payload
}, },
// 设置助手记忆功能是否激活
setAssistantMemoryActive: (state, action: PayloadAction<boolean>) => {
state.assistantMemoryActive = action.payload
},
// 自适应分析相关的reducer // 自适应分析相关的reducer
setAdaptiveAnalysisEnabled: (state, action: PayloadAction<boolean>) => { setAdaptiveAnalysisEnabled: (state, action: PayloadAction<boolean>) => {
state.adaptiveAnalysisEnabled = action.payload state.adaptiveAnalysisEnabled = action.payload
@ -717,6 +822,33 @@ const memorySlice = createSlice({
}) })
} }
// 更新助手记忆优先级
if (state.assistantMemories && state.assistantMemories.length > 0) {
state.assistantMemories.forEach((memory) => {
// 计算时间衰减因子
if (state.decayEnabled && memory.lastAccessedAt) {
const daysSinceLastAccess = (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24)
const decayFactor = Math.max(0, 1 - daysSinceLastAccess * state.decayRate * 2) // 助手记忆衰减速度介于长期和短期记忆之间
memory.decayFactor = decayFactor
} else {
memory.decayFactor = 1 // 无衰减
}
// 计算鲜度评分
if (state.freshnessEnabled) {
const daysSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60 * 24)
const lastAccessDays = memory.lastAccessedAt
? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24)
: daysSinceCreation
// 助手记忆的鲜度评分
const creationFreshness = Math.max(0, 1 - daysSinceCreation / 15) // 15天内创建的记忆较新
const accessFreshness = Math.max(0, 1 - lastAccessDays / 3) // 3天内访问的记忆较新
memory.freshness = creationFreshness * 0.3 + accessFreshness * 0.7 // 加权平均
}
})
}
state.lastPriorityUpdate = now state.lastPriorityUpdate = now
}, },
@ -753,11 +885,25 @@ const memorySlice = createSlice({
memory.freshness = creationFreshness * 0.2 + accessFreshness * 0.8 memory.freshness = creationFreshness * 0.2 + accessFreshness * 0.8
}) })
} }
// 更新助手记忆鲜度
if (state.assistantMemories && state.assistantMemories.length > 0) {
state.assistantMemories.forEach((memory) => {
const daysSinceCreation = (now - new Date(memory.createdAt).getTime()) / (1000 * 60 * 60 * 24)
const lastAccessDays = memory.lastAccessedAt
? (now - new Date(memory.lastAccessedAt).getTime()) / (1000 * 60 * 60 * 24)
: daysSinceCreation
const creationFreshness = Math.max(0, 1 - daysSinceCreation / 15)
const accessFreshness = Math.max(0, 1 - lastAccessDays / 3)
memory.freshness = creationFreshness * 0.3 + accessFreshness * 0.7
})
}
}, },
// 记录记忆访问 // 记录记忆访问
accessMemory: (state, action: PayloadAction<{ id: string; isShortMemory?: boolean }>) => { accessMemory: (state, action: PayloadAction<{ id: string; isShortMemory?: boolean; isAssistantMemory?: boolean }>) => {
const { id, isShortMemory } = action.payload const { id, isShortMemory, isAssistantMemory } = action.payload
const now = new Date().toISOString() const now = new Date().toISOString()
if (isShortMemory) { if (isShortMemory) {
@ -767,6 +913,13 @@ const memorySlice = createSlice({
memory.accessCount = (memory.accessCount || 0) + 1 memory.accessCount = (memory.accessCount || 0) + 1
memory.lastAccessedAt = now memory.lastAccessedAt = now
} }
} else if (isAssistantMemory) {
// 更新助手记忆访问信息
const memory = state.assistantMemories?.find((m) => m.id === id)
if (memory) {
memory.accessCount = (memory.accessCount || 0) + 1
memory.lastAccessedAt = now
}
} else { } else {
// 更新长期记忆访问信息 // 更新长期记忆访问信息
const memory = state.memories?.find((m) => m.id === id) const memory = state.memories?.find((m) => m.id === id)
@ -821,6 +974,18 @@ const memorySlice = createSlice({
state.memoryLists = action.payload.memoryLists || state.memoryLists state.memoryLists = action.payload.memoryLists || state.memoryLists
state.shortMemories = action.payload.shortMemories || state.shortMemories state.shortMemories = action.payload.shortMemories || state.shortMemories
// 助手记忆数据
if (action.payload.assistantMemories) {
state.assistantMemories = action.payload.assistantMemories
console.log('[Memory Reducer] Loaded assistant memories:', action.payload.assistantMemories.length)
}
// 助手记忆功能状态
if (action.payload.assistantMemoryActive !== undefined) {
state.assistantMemoryActive = action.payload.assistantMemoryActive
console.log('[Memory Reducer] Loaded assistant memory active state:', action.payload.assistantMemoryActive)
}
// 更新模型选择 // 更新模型选择
if (action.payload.analyzeModel) { if (action.payload.analyzeModel) {
state.analyzeModel = action.payload.analyzeModel state.analyzeModel = action.payload.analyzeModel
@ -832,6 +997,12 @@ const memorySlice = createSlice({
console.log('[Memory Reducer] Loaded short memory analyze model:', action.payload.shortMemoryAnalyzeModel) console.log('[Memory Reducer] Loaded short memory analyze model:', action.payload.shortMemoryAnalyzeModel)
} }
// 助手记忆分析模型
if (action.payload.assistantMemoryAnalyzeModel) {
state.assistantMemoryAnalyzeModel = action.payload.assistantMemoryAnalyzeModel
console.log('[Memory Reducer] Loaded assistant memory analyze model:', action.payload.assistantMemoryAnalyzeModel)
}
console.log('Short-term memory data loaded into state') console.log('Short-term memory data loaded into state')
} }
}) })
@ -898,6 +1069,7 @@ export const {
setFilterSensitiveInfo, setFilterSensitiveInfo,
setAnalyzeModel, setAnalyzeModel,
setShortMemoryAnalyzeModel, setShortMemoryAnalyzeModel,
setAssistantMemoryAnalyzeModel,
setHistoricalContextAnalyzeModel, setHistoricalContextAnalyzeModel,
setVectorizeModel, setVectorizeModel,
setAnalyzing, setAnalyzing,
@ -914,6 +1086,11 @@ export const {
deleteShortMemory, deleteShortMemory,
clearShortMemories, clearShortMemories,
setShortMemoryActive, setShortMemoryActive,
// 助手记忆相关的action
addAssistantMemory,
deleteAssistantMemory,
clearAssistantMemories,
setAssistantMemoryActive,
// 自适应分析相关的action // 自适应分析相关的action
setAdaptiveAnalysisEnabled, setAdaptiveAnalysisEnabled,
@ -998,12 +1175,14 @@ export const saveMemoryData = createAsyncThunk(
// 模型选择 // 模型选择
analyzeModel: memoryData.analyzeModel || state.analyzeModel, analyzeModel: memoryData.analyzeModel || state.analyzeModel,
shortMemoryAnalyzeModel: memoryData.shortMemoryAnalyzeModel || state.shortMemoryAnalyzeModel, shortMemoryAnalyzeModel: memoryData.shortMemoryAnalyzeModel || state.shortMemoryAnalyzeModel,
assistantMemoryAnalyzeModel: memoryData.assistantMemoryAnalyzeModel || state.assistantMemoryAnalyzeModel,
historicalContextAnalyzeModel: memoryData.historicalContextAnalyzeModel || state.historicalContextAnalyzeModel, historicalContextAnalyzeModel: memoryData.historicalContextAnalyzeModel || state.historicalContextAnalyzeModel,
vectorizeModel: memoryData.vectorizeModel || state.vectorizeModel, vectorizeModel: memoryData.vectorizeModel || state.vectorizeModel,
// 记忆数据 // 记忆数据
memoryLists: memoryData.memoryLists || state.memoryLists, memoryLists: memoryData.memoryLists || state.memoryLists,
shortMemories: memoryData.shortMemories || state.shortMemories, shortMemories: memoryData.shortMemories || state.shortMemories,
assistantMemories: memoryData.assistantMemories || state.assistantMemories,
currentListId: memoryData.currentListId || state.currentListId, currentListId: memoryData.currentListId || state.currentListId,
// 自适应分析相关 // 自适应分析相关
@ -1173,14 +1352,19 @@ export const saveAllMemorySettings = createAsyncThunk('memory/saveAllSettings',
// 基本设置 // 基本设置
isActive: state.isActive, isActive: state.isActive,
shortMemoryActive: state.shortMemoryActive, shortMemoryActive: state.shortMemoryActive,
assistantMemoryActive: state.assistantMemoryActive,
autoAnalyze: state.autoAnalyze, autoAnalyze: state.autoAnalyze,
// 模型选择 // 模型选择
analyzeModel: state.analyzeModel, analyzeModel: state.analyzeModel,
shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel, shortMemoryAnalyzeModel: state.shortMemoryAnalyzeModel,
assistantMemoryAnalyzeModel: state.assistantMemoryAnalyzeModel,
historicalContextAnalyzeModel: state.historicalContextAnalyzeModel, historicalContextAnalyzeModel: state.historicalContextAnalyzeModel,
vectorizeModel: state.vectorizeModel, vectorizeModel: state.vectorizeModel,
// 记忆数据
assistantMemories: state.assistantMemories,
// 自适应分析相关 // 自适应分析相关
adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled, adaptiveAnalysisEnabled: state.adaptiveAnalysisEnabled,
analysisFrequency: state.analysisFrequency, analysisFrequency: state.analysisFrequency,

View File

@ -4,7 +4,7 @@ import { autoRenameTopic, TopicManager } from '@renderer/hooks/useTopic'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { fetchChatCompletion } from '@renderer/services/ApiService' import { fetchChatCompletion } from '@renderer/services/ApiService'
import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService' import { getAssistantMessage, resetAssistantMessage } from '@renderer/services/MessagesService'
import type { AppDispatch, RootState } from '@renderer/store' import store, { type AppDispatch, type RootState } from '@renderer/store'
import type { Assistant, Message, Topic } from '@renderer/types' import type { Assistant, Message, Topic } from '@renderer/types'
import type { Model } from '@renderer/types' import type { Model } from '@renderer/types'
import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue' import { clearTopicQueue, getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
@ -14,6 +14,7 @@ export interface MessagesState {
messagesByTopic: Record<string, Message[]> messagesByTopic: Record<string, Message[]>
streamMessagesByTopic: Record<string, Record<string, Message | null>> streamMessagesByTopic: Record<string, Record<string, Message | null>>
currentTopic: Topic | null currentTopic: Topic | null
currentAssistant: Assistant | null
loadingByTopic: Record<string, boolean> // 每个会话独立的loading状态 loadingByTopic: Record<string, boolean> // 每个会话独立的loading状态
displayCount: number displayCount: number
error: string | null error: string | null
@ -23,6 +24,7 @@ const initialState: MessagesState = {
messagesByTopic: {}, messagesByTopic: {},
streamMessagesByTopic: {}, streamMessagesByTopic: {},
currentTopic: null, currentTopic: null,
currentAssistant: null,
loadingByTopic: {}, loadingByTopic: {},
displayCount: 20, displayCount: 20,
error: null error: null
@ -142,6 +144,9 @@ const messagesSlice = createSlice({
setCurrentTopic: (state, action: PayloadAction<Topic | null>) => { setCurrentTopic: (state, action: PayloadAction<Topic | null>) => {
state.currentTopic = action.payload state.currentTopic = action.payload
}, },
setCurrentAssistant: (state, action: PayloadAction<Assistant | null>) => {
state.currentAssistant = action.payload
},
clearTopicMessages: (state, action: PayloadAction<string>) => { clearTopicMessages: (state, action: PayloadAction<string>) => {
const topicId = action.payload const topicId = action.payload
state.messagesByTopic[topicId] = [] state.messagesByTopic[topicId] = []
@ -218,6 +223,7 @@ const handleResponseMessageUpdate = (
dispatch: AppDispatch, dispatch: AppDispatch,
getState: () => RootState getState: () => RootState
) => { ) => {
setTimeout(() => {
dispatch(setStreamMessage({ topicId, message })) dispatch(setStreamMessage({ topicId, message }))
if (message.status !== 'pending') { if (message.status !== 'pending') {
// When message is complete, commit to messages and sync with DB // When message is complete, commit to messages and sync with DB
@ -234,6 +240,7 @@ const handleResponseMessageUpdate = (
} }
} }
} }
}, 0)
} }
// Helper function to sync messages with database // Helper function to sync messages with database
@ -379,9 +386,8 @@ export const sendMessage =
: topic.prompt : topic.prompt
} }
// 节流 // 节流,降低到 50ms因为已经在handleResponseMessageUpdate内保证react能调度。
const throttledDispatch = throttle(handleResponseMessageUpdate, 100, { trailing: true }) // 100ms的节流时间应足够平衡用户体验和性能 const throttledDispatch = throttle(handleResponseMessageUpdate, 50, { trailing: true })
// 寻找当前正在处理的消息在消息列表中的位置
// const messageIndex = messages.findIndex((m) => m.id === assistantMessage.id) // const messageIndex = messages.findIndex((m) => m.id === assistantMessage.id)
const handleMessages = (): Message[] => { const handleMessages = (): Message[] => {
// 找到对应的用户消息位置 // 找到对应的用户消息位置
@ -517,19 +523,29 @@ export const resendMessage =
} }
} }
// Modified loadTopicMessages thunk // Modified loadTopicMessages thunk - 优化性能,减少日志输出
export const loadTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => { export const loadTopicMessagesThunk = (topic: Topic) => async (dispatch: AppDispatch) => {
// 设置会话的loading状态 // 设置会话的loading状态
dispatch(setTopicLoading({ topicId: topic.id, loading: true })) dispatch(setTopicLoading({ topicId: topic.id, loading: true }))
// 如果已经有消息,不需要再次加载
const state = store.getState()
if (state.messages.messagesByTopic[topic.id]?.length > 0) {
dispatch(setCurrentTopic(topic)) dispatch(setCurrentTopic(topic))
dispatch(setTopicLoading({ topicId: topic.id, loading: false }))
return
}
try { try {
// 使用 getTopic 获取会话对象 // 使用 getTopic 获取会话对象,使用缓存减少数据库访问
const topicWithDB = await TopicManager.getTopic(topic.id) const topicWithDB = await TopicManager.getTopic(topic.id)
if (topicWithDB) { if (topicWithDB) {
// 如果数据库中有会话,加载消息,保存会话 // 如果数据库中有会话,加载消息
dispatch(loadTopicMessages({ topicId: topic.id, messages: topicWithDB.messages })) dispatch(loadTopicMessages({ topicId: topic.id, messages: topicWithDB.messages }))
} }
dispatch(setCurrentTopic(topic))
} catch (error) { } catch (error) {
// 静默处理错误,减少日志输出
dispatch(setError(error instanceof Error ? error.message : 'Failed to load messages')) dispatch(setError(error instanceof Error ? error.message : 'Failed to load messages'))
} finally { } finally {
// 清除会话的loading状态 // 清除会话的loading状态
@ -645,6 +661,7 @@ export const {
addMessage, addMessage,
updateMessage, updateMessage,
setCurrentTopic, setCurrentTopic,
setCurrentAssistant,
clearTopicMessages, clearTopicMessages,
loadTopicMessages, loadTopicMessages,
setStreamMessage, setStreamMessage,

View File

@ -123,6 +123,7 @@ export interface SettingsState {
ttsCustomVoices: string[] ttsCustomVoices: string[]
ttsCustomModels: string[] ttsCustomModels: string[]
showTTSProgressBar: boolean // 是否显示TTS进度条 showTTSProgressBar: boolean // 是否显示TTS进度条
autoPlayTTSOutsideVoiceCall: boolean // 是否在语音通话模式之外自动播放TTS
// 浏览器 TTS配置 // 浏览器 TTS配置
ttsEdgeVoice: string ttsEdgeVoice: string
// 硅基流动 TTS配置 // 硅基流动 TTS配置
@ -266,6 +267,7 @@ export const initialState: SettingsState = {
ttsEnabled: false, ttsEnabled: false,
ttsServiceType: 'openai', // 默认使用 OpenAI TTS ttsServiceType: 'openai', // 默认使用 OpenAI TTS
ttsApiKey: '', ttsApiKey: '',
autoPlayTTSOutsideVoiceCall: false, // 默认不在语音通话模式之外自动播放TTS
ttsApiUrl: 'https://api.openai.com/v1/audio/speech', ttsApiUrl: 'https://api.openai.com/v1/audio/speech',
ttsVoice: '', ttsVoice: '',
ttsModel: '', ttsModel: '',
@ -722,6 +724,9 @@ const settingsSlice = createSlice({
setShowTTSProgressBar: (state, action: PayloadAction<boolean>) => { setShowTTSProgressBar: (state, action: PayloadAction<boolean>) => {
state.showTTSProgressBar = action.payload state.showTTSProgressBar = action.payload
}, },
setAutoPlayTTSOutsideVoiceCall: (state, action: PayloadAction<boolean>) => {
state.autoPlayTTSOutsideVoiceCall = action.payload
},
// ASR相关的action // ASR相关的action
setAsrEnabled: (state, action: PayloadAction<boolean>) => { setAsrEnabled: (state, action: PayloadAction<boolean>) => {
state.asrEnabled = action.payload state.asrEnabled = action.payload
@ -884,6 +889,7 @@ export const {
removeTtsCustomModel, removeTtsCustomModel,
setTtsFilterOptions, setTtsFilterOptions,
setShowTTSProgressBar, setShowTTSProgressBar,
setAutoPlayTTSOutsideVoiceCall,
setAsrEnabled, setAsrEnabled,
setAsrServiceType, setAsrServiceType,
setAsrApiKey, setAsrApiKey,

View File

@ -1,5 +1,5 @@
interface ASRServerAPI { interface ASRServerAPI {
startServer: () => Promise<{ success: boolean; pid?: number; error?: string }> startServer: () => Promise<{ success: boolean; pid?: number; port?: number; error?: string }>
stopServer: (pid: number) => Promise<{ success: boolean; error?: string }> stopServer: (pid: number) => Promise<{ success: boolean; error?: string }>
} }

View File

@ -143,7 +143,7 @@ export type Provider = {
isNotSupportArrayContent?: boolean isNotSupportArrayContent?: boolean
} }
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai' export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai' | 'deepclaude'
export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search' export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search'

View File

@ -0,0 +1,12 @@
// 从OpenAI库导出的类型定义
// 直接从资源中导入需要的类型而不是导入整个OpenAI模块
// 定义ChatCompletionRequestMessage类型
export type ChatCompletionRequestMessage = {
role: 'system' | 'user' | 'assistant'
content: string
name?: string
}
// 导出其他可能需要的OpenAI类型
export type { ChatCompletionContentPart } from 'openai/resources'

View File

@ -0,0 +1,143 @@
import { Model, Provider } from '@renderer/types'
export interface ThinkingLibrary {
id: string
name: string
description: string
prompt: string
category: string
}
interface ModelCombination {
id: string
name: string
reasonerModel: any
targetModel: any
isActive: boolean
thinkingLibraryId?: string
}
// 检查localStorage中的模型组合数据
export function checkModelCombinationsInLocalStorage() {
try {
const savedCombinations = localStorage.getItem('modelCombinations')
if (!savedCombinations) {
console.log('[checkModelCombinationsInLocalStorage] localStorage中没有模型组合数据')
return
}
const combinations = JSON.parse(savedCombinations)
console.log('[checkModelCombinationsInLocalStorage] localStorage中的模型组合数据:',
JSON.stringify(combinations, null, 2))
} catch (e) {
console.error('[checkModelCombinationsInLocalStorage] 解析localStorage中的模型组合数据失败:', e)
}
}
/**
* DeepClaude提供商
* @param combination
* @returns DeepClaude提供商
*/
// 创建模型对象用于添加到DeepClaude提供商中
export function createDeepClaudeModel(combination: ModelCombination): Model {
console.log('[createDeepClaudeModel] 创建DeepClaude模型组合ID:', combination.id,
'组合名称:', combination.name,
'推理模型:', combination.reasonerModel?.id, combination.reasonerModel?.name,
'目标模型:', combination.targetModel?.id, combination.targetModel?.name)
// 使用组合ID作为模型ID
console.log('[createDeepClaudeModel] 使用组合ID作为模型ID:', combination.id)
// 创建符合Model类型的对象
const model: Model = {
id: combination.id, // 使用组合ID作为模型ID而不是生成新的UUID
provider: 'deepclaude',
name: combination.name,
group: 'DeepClaude',
type: ['text'], // 指定为文本模型,而非嵌入模型
description: `${combination.reasonerModel?.name} + ${combination.targetModel?.name}`
}
return model
}
// 创建DeepClaude提供商
export function createDeepClaudeProvider(combinations: ModelCombination[]): Provider {
console.log('[createDeepClaudeProvider] 创建DeepClaude提供商组合数量:', combinations.length)
// 为每个组合创建一个模型
const models = combinations.map(createDeepClaudeModel)
const provider: Provider = {
id: 'deepclaude',
name: 'DeepClaude',
type: 'deepclaude',
apiKey: '', // 不需要API密钥使用组合模型的API密钥
apiHost: '', // 不需要API地址使用组合模型的API地址
models: models,
enabled: true,
isSystem: false
}
console.log('[createDeepClaudeProvider] 创建的提供商详情:',
{ id: provider.id, name: provider.name, type: provider.type,
models: provider.models.map(m => ({ id: m.id, name: m.name, provider: m.provider })) })
return provider
}
/**
* localStorage获取所有激活的模型组合
* @returns
*/
export function getActiveModelCombinations(): ModelCombination[] {
try {
const savedCombinations = localStorage.getItem('modelCombinations')
if (!savedCombinations) {
console.log('[getActiveModelCombinations] 未找到模型组合配置')
return []
}
const combinations = JSON.parse(savedCombinations) as ModelCombination[]
const activeCombinations = combinations.filter(c => c.isActive)
console.log('[getActiveModelCombinations] 找到激活的模型组合数量:', activeCombinations.length)
console.log('[getActiveModelCombinations] 激活的模型组合详情:',
activeCombinations.map(c => ({
id: c.id,
name: c.name,
reasonerModel: {
id: c.reasonerModel?.id,
name: c.reasonerModel?.name,
provider: c.reasonerModel?.provider
},
targetModel: {
id: c.targetModel?.id,
name: c.targetModel?.name,
provider: c.targetModel?.provider
}
})))
return activeCombinations
} catch (e) {
console.error('[getActiveModelCombinations] Failed to parse model combinations:', e)
return []
}
}
/**
* DeepClaude提供商
* @returns DeepClaude提供商列表
*/
export function createAllDeepClaudeProviders(): Provider[] {
const activeCombinations = getActiveModelCombinations()
console.log('[createAllDeepClaudeProviders] 创建所有DeepClaude提供商激活的模型组合数量:', activeCombinations.length)
if (activeCombinations.length === 0) {
return []
}
// 创建一个单一的DeepClaude提供商
const provider = createDeepClaudeProvider(activeCombinations)
console.log('[createAllDeepClaudeProviders] 创建的DeepClaude提供商:', provider.id, provider.name, provider.models.length)
return [provider]
}

View File

@ -37,6 +37,27 @@ export function parseJSON(str: string) {
} }
} }
// 获取模型的唯一ID确保是字符串格式
export function getModelUniqId(model: any) {
if (!model) return null
// 如果已经是字符串,直接返回
if (typeof model === 'string') return model
// 如果是对象转换为JSON字符串
if (typeof model === 'object') {
if (model.id) {
return JSON.stringify({
id: model.id,
provider: model.provider || ''
})
}
return JSON.stringify(model)
}
return String(model)
}
export const delay = (seconds: number) => { export const delay = (seconds: number) => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {

View File

@ -0,0 +1,625 @@
import { ThinkingLibrary } from './createDeepClaudeProvider'
// 默认思考库列表
export const DEFAULT_THINKING_LIBRARIES: ThinkingLibrary[] = [
{
id: 'general_structured', // 建议修改ID以区分或者直接替换 'general'
name: '结构化通用思考', // 名称可以调整,反映其结构性
description: '采用结构化框架全面分析问题,探索不同维度和深层含义。', // 更新描述
category: '通用',
prompt: `你是一个深度思考模型。你的任务是对以下问题进行全面、多维度的思考探索,生成一个详细的结构化思考过程记录,为后续的 AI 处理提供丰富的上下文和分析。
1. ** (Deconstruction & Definition):**
*
*
*
*
2. ** (Multi-dimensional Analysis):**
* **:** //
* **:**
* **:**
3. ** (Connections & Reasoning):**
*
*
*
* /
4. ** (Challenges & Unknowns):**
*
*
*
5. ** (Synthesis & Insights):**
*
*
*
*
<think> </think> AI
: {question}`
},
{
id: 'scientific_rigorous',
name: '严谨科学分析',
description: '运用系统化的科学方法论,对问题进行深入、严谨的分析、假设检验与评估。', // 更新描述
category: '专业',
prompt: `你是一位严谨的科学分析师,遵循科学方法论对问题进行系统性探究。请对以下问题,按照结构化的科学探究过程进行深入思考和分析:
1. ** (Problem Definition & Background Research):**
* **:**
* **:**
* **:**
2. ** (Hypothesis Formulation & Prediction):**
* **:** TestableFalsifiable
* **:**
* **:**
3. **/ (Research Design / Evidence Strategy):**
* **:**
* **:**
* **:** 使
4. ** (Evidence Analysis & Interpretation - **):**
* **:** /
* **:** AX意味着什么B
* **:**
5. ** (Evaluation & Conclusion):**
* **:**
* **:**
* **:**
6. ** (Limitations & Future Directions):**
* **:** /
* **:**
<think> </think>
: {question}`
},
{
id: 'creative_structured', // Suggest changing ID or replacing 'creative'
name: '结构化创意思考', // Updated name
description: '运用结构化创意方法,激发非凡想法并发展成可行方案。', // Updated description
category: '创意',
prompt: `你是一位富有想象力的创意催化剂。你的任务是运用多种创意激发技巧,对以下问题进行深度、发散性的思考,并最终收敛到几个新颖且有潜力的解决方案。
1. ** (Problem Reframing & Opportunity Seeking):**
* **:**
* **:**
* **:**
2. ** (Divergent Idea Generation):**
* **/:**
* **/:**
* ** (Perspective Shifting):** ////
3. ** (Idea Expansion & Combination):**
* **SCAMPER :** Substitute , Combine , Adapt , Modify/Magnify/Minify //, Put to another use , Eliminate , Reverse/Rearrange /
* **:** /
* ** (Analogical Thinking):**
4. ** (Idea Screening & Focusing):**
* **:**
* **:**
* **:** 2-3
5. ** (Innovative Solution Articulation & Initial Assessment):**
* **:** 1-2
* **:**
* **:**
<think> </think>
: {question}`
},
{
id: 'logical_rigorous', // Suggest changing ID or replacing 'logical'
name: '严谨逻辑推理', // Updated name
description: '运用严密的逻辑分析工具,系统性地解构论证、评估有效性并得出可靠结论。', // Updated description
category: '专业',
prompt: `你是一位逻辑分析专家。你的任务是对以下问题或论证进行严密、系统的逻辑分析评估其有效性Validity和可靠性Soundness/Cogency并识别任何潜在的逻辑缺陷。
1. **/ (Problem/Argument Definition):**
* **:** Argument
* **:** Conclusion
2. ** (Premise Identification & Organization):**
* **:** Premises
* **:** Implicit Premise
* **:**
3. ** (Argument Structure & Type Analysis):**
* **:** DeductiveInductive使
* ** ():**
4. **/ (Validity/Strength Evaluation):**
* **:** **
* **:**
5. **/ (Soundness/Cogency Evaluation):**
* **/:** /
* **:** SoundCogent/
6. ** (Fallacy Check):**
* **:** Formal FallaciesInformal Fallacies
7. ** (Counterarguments & Limitations):**
* **:**
* **:** 广
8. ** (Final Conclusion & Justification):**
* **:** /
* **:**
<think> </think>
: {question}`
},
{
id: 'programming_detailed', // Suggest changing ID or replacing 'programming'
name: '详细编程思考', // Updated name
description: '系统化地分析编程问题,设计算法、数据结构,并规划实现与测试。', // Updated description
category: '专业',
prompt: `你是一位细致的程序员和算法设计师。你的任务是针对给定的编程问题,进行一步步的分析、设计和规划,产出清晰的解决思路。
1. ** (Problem Understanding & Requirement Clarification):**
* **:**
* **:** //
* **:**
2. ** (Initial Approach & Data Structure Selection):**
* **:** 1-2
* **:** 使
* **:**
3. ** (Algorithm Design & Pseudocode):**
* **:**
* **/:** 使
* **:** 使O表示法
4. ** (Implementation Details):**
* **/:**
* **:**
* **/:**
5. ** (Error Handling & Robustness):**
* **:** API调用失败
* **:**
* **:**
6. ** (Test Case Design):**
* **:**
* **:**
* **:**
* ** ():**
7. ** (Reflection & Optimization):**
* **:**
* **/:**
* **:**
<think> </think>
: {question}`
},
{
id: 'software_development_advanced', // Suggest changing ID or replacing 'software_development'
name: '高级软件开发设计', // Updated name
description: '针对复杂软件功能,进行全面的架构设计、实现规划和质量保障考量。', // Updated description
category: '专业',
prompt: `你是一位经验丰富的全栈软件架构师和工程师,擅长设计和领导开发复杂、高可用、可扩展且安全的功能。请对以下软件开发问题,进行深入的端到端分析、设计与规划:
1. ** (In-depth Requirement Analysis):**
* **:** / (KPIs)
* ** (Functional):** 使 (Use Cases) (User Stories)
* ** (Non-functional):** /SLA要求
* **:**
2. ** (Architecture & Technology Selection):**
* **:**
* **:** / (API Design)
* **:** /NoSQL
* **:**
3. ** (Detailed Design & Implementation Planning):**
* **:**
* **:** API OpenAPI/Swagger
* **:**
* **:** //
4. ** (Coding & Quality Assurance):**
* **:**
* **:**
* **:**
5. ** (Deployment & Operations):**
* **:** 绿
* **:**
* **:**
6. ** (Risk Assessment & Iteration Plan):**
* **:**
* **:** 线
<think> </think>
: {question}`
},
{
id: 'scientific_research_advanced', // Suggest changing ID or replacing 'scientific_research'
name: '深度科学研究规划', // Updated name
description: '系统地识别研究空白,设计严谨的研究方案,并评估其潜在影响与可行性。', // Updated description
category: '专业',
prompt: `你是一位具有深厚领域知识和敏锐洞察力的研究科学家。你的任务是基于对现有文献的批判性评估,识别出有价值的研究空白,并构思一个新颖、严谨且可行的研究方案来填补这一空白。
1. ** (Literature Review & Gap Identification):**
* **:** 沿
* **/:**
* **:** (Research Questions)
2. ** (Theoretical Framework & Hypothesis Formulation):**
* **:**
* ** ():**
* **:** TestableFalsifiable (Hypotheses)
3. ** (Research Design & Methodology):**
* **:** 访
* **/:** 访
* **:** (Reliability) (Validity)
* **:**
4. ** (Expected Contributions & Potential Impact):**
* **:**
* **/:**
* **:**
5. ** (Feasibility & Ethical Considerations):**
* **:**
* **:**
6. ** (Limitations & Alternatives):**
* **:**
* **:**
<think> </think>
: {question}` // Note: The {question} here should ideally frame a research area or topic, not just a simple question.
},
{
id: 'creative_writing_3act_detailed', // Suggest changing ID or replacing 'creative_writing'
name: '三幕剧深度情节构建', // Updated name
description: '运用三幕剧结构,深入构思情节转折、角色发展和主题呈现。', // Updated description
category: '创意',
prompt: `你是一位经验丰富的小说家和叙事设计师,精通运用经典的三幕剧结构来编织引人入胜、情感饱满的故事。请对以下创意写作需求(可能是一个主题、一个角色、一个核心冲突或一个简单的想法),进行深入的情节大纲构建:
**:**
* ** (Logline):**
* **:**
* **/:** /
** (Setup - Approx. 25%)**
* **:**
* ** (Inciting Incident):** 使//
* **/:**
* ** (Plot Point 1 / Lock In):** /
** (Confrontation - Approx. 50%)**
* **/:** /
* **/:** 线
* ** (Midpoint):** //
* **/:** /
* ** (Plot Point 2 / All Is Lost / Dark Night of the Soul):** /
** (Resolution - Approx. 25%)**
* **/:**
* ** (Climax):** //
* **/:**
* **/:**
**:**
* **:**
* **:**
* **:** 穿
<think> </think>
: {question}` // Note: The {question} here should provide a starting point for the story idea.
},
{
id: 'business_strategy_swot_actionable', // Suggest changing ID or replacing 'business_strategy'
name: 'SWOT分析与行动战略', // Updated name
description: '进行深入的SWOT分析并据此制定具体、可操作且优先排序的战略建议。', // Updated description
category: '专业',
prompt: `你是一位经验丰富、注重实效的商业战略顾问。你的任务是针对给定的商业情境或问题进行深入、富有洞察力的SWOT分析并从中推导出具体、可操作、优先排序的战略行动方案。
SWOT分析与战略制定流程
1. ** (Context Understanding & Objective Setting):**
* **/:**
* **:** SWOT分析聚焦的具体业务单元线
2. ** (Internal Factor Analysis):**
* ** (Strengths) :**
*
* **:**
* ** (Weaknesses) :**
*
* **:**
3. ** (External Factor Analysis):**
* ** (Opportunities) :**
*
* **:**
* ** (Threats) :**
* 退
* **:**
4. **SWOT矩阵综合分析 (SWOT Matrix Synthesis):**
* S, W, O, T SWOT矩阵
*
* SO
* ST
* WO
* WT
5. ** (Strategy Formulation):**
* **SO () :** -
* **ST (/) :** -
* **WO () :** -
* **WT (/) :** -
* **:**
6. ** (Strategy Evaluation & Prioritization):**
* **:** 使/SMART原则
* **:**
* **:**
7. ** (Key Success Factors & Next Steps):**
* **:**
* **:** KPIs
* **:**
SWOT分析与战略规划过程严格按照上述结构 <think> </think>
: {question}` // Note: The {question} should define the business context or problem to analyze.
},
{
id: 'mathematical_logic_reasoning', // 新的 ID
name: '数学逻辑推理', // 新的名称
description: '分析数学陈述,构建严谨证明或寻找反例,侧重逻辑结构和有效性。', // 新的描述
category: '专业', // 或 '逻辑'/'数学'
prompt: `你是一位严谨的数学逻辑分析器。你的任务是分析给定的数学陈述或问题,评估其逻辑结构,构建严谨的证明,或者寻找有效的反例。
1. ** (Statement Understanding & Formalization):**
* **:** /
* ** ():** ,
* **:** P Q P / P x
2. ** (Identifying Premises, Assumptions & Givens):**
* **:**
* **:**
* **:**
3. **/ (Proof/Disproof Strategy Selection):**
* **:**
* (Direct Proof)
* (Proof by Contradiction)
* (Proof by Induction - /)
* (Proof by Construction)
* (Proof by Cases)
* (Disproof by Counterexample)
* **:**
4. ** (Logical Deduction & Step Construction):**
* **:**
* **:**
* **:** /
* **():** (Base Case) (Inductive Step) (Inductive Hypothesis) 使
* **():** (Negation of the conclusion)
* **():**
5. ** (Validity & Rigor Check):**
* **:**
* **使:**
* ** (使):**
6. ** (Conclusion & Summary):**
* **:** Q.E.D., /,
* ** ():** /
<think> </think>
: {question}` // Note: The {question} should be a mathematical statement to prove/disprove, or a problem requiring logical deduction.
},
{
id: 'data_analysis_insightful', // Or keep 'data_analysis' if replacing
name: '洞察驱动的数据分析',
description: '执行系统化的探索性数据分析EDA发现模式、异常并提炼可行动的见解。',
category: '专业',
prompt: `你是一位经验丰富、注重细节的数据科学家,精通使用 Python (pandas, numpy, matplotlib, seaborn, scipy等库) 进行探索性数据分析EDA。你的目标是从原始数据中挖掘出深刻的模式、识别异常、验证假设并为后续的建模或决策提供清晰的、数据驱动的见解。
/EDA规划与思考
EDA的核心目标是什么({question} ) dtypes (\`.head()\`, \`.tail()\`)。使用 \`.info()\`\`.describe()\` 获取基本摘要。
///
使 \`plt.hist\`/\`sns.histplot\`、核密度估计 \`sns.kdeplot\`、箱线图 \`plt.boxplot\`/\`sns.boxplot\`)来理解其分布、中心趋势、离散程度和偏度?初步解释每个关键数值变量的分布特征。
(\`.value_counts()\`)。计划使用何种可视化(如条形图 \`plt.bar\`/\`sns.countplot\`)来展示类别分布?初步解释关键类别变量的分布情况。
vs \`.corr()\`,可视化使用散点图 \`plt.scatter\`/\`sns.scatterplot\` 或热力图 \`sns.heatmap\`)。解释关键相关性。
vs 使线 \`sns.violinplot\`、分组条形图表示均值/中位数)。解释观察到的差异。
vs \`pd.crosstab\`,可视化使用堆叠/分组条形图)。解释关键关联。
(): 使 (\`sns.pairplot\`) 或根据业务理解探索特定多变量交互作用(例如,通过颜色/大小/形状在散点图上表示第三个变量)。
使线 IQR Z-score
/Winsorization
///
/EDA结果
EDA所用方法和覆盖范围的局限性
<think></think>
: {question}`
}
]
// 获取思考库列表
export function getThinkingLibraries(): ThinkingLibrary[] {
try {
const savedLibraries = localStorage.getItem('thinkingLibraries')
console.log('[ThinkingLibrary] 从localStorage获取思考库:', savedLibraries ? '成功' : '未找到')
if (savedLibraries) {
const parsed = JSON.parse(savedLibraries) as ThinkingLibrary[]
console.log('[ThinkingLibrary] 解析思考库数量:', parsed.length)
if (parsed.length < DEFAULT_THINKING_LIBRARIES.length || !parsed.every(lib => DEFAULT_THINKING_LIBRARIES.some(defLib => defLib.id === lib.id))) {
console.log('[ThinkingLibrary] 存储的思考库需要更新,与默认库合并')
const librariesToMerge = DEFAULT_THINKING_LIBRARIES.map(defaultLib => {
const existingLib = parsed.find(lib => lib.id === defaultLib.id);
return existingLib || defaultLib;
});
const customLibraries = parsed.filter(lib => !DEFAULT_THINKING_LIBRARIES.some(defLib => defLib.id === lib.id));
const updatedLibraries = [...librariesToMerge, ...customLibraries];
console.log('[ThinkingLibrary] 更新后思考库数量:', updatedLibraries.length);
saveThinkingLibraries(updatedLibraries);
return updatedLibraries;
}
return parsed
}
} catch (e) {
console.error('[ThinkingLibrary] 解析思考库失败:', e)
}
console.log('[ThinkingLibrary] 使用默认思考库')
saveThinkingLibraries(DEFAULT_THINKING_LIBRARIES)
return DEFAULT_THINKING_LIBRARIES
}
// 保存思考库列表
export function saveThinkingLibraries(libraries: ThinkingLibrary[]): void {
try {
console.log('[ThinkingLibrary] 保存思考库数量:', libraries.length)
const jsonString = JSON.stringify(libraries, null, 2)
localStorage.setItem('thinkingLibraries', jsonString)
console.log('[ThinkingLibrary] 思考库保存成功')
const savedLibraries = localStorage.getItem('thinkingLibraries')
if (savedLibraries) {
console.log('[ThinkingLibrary] 验证保存结果 - 数据已写入localStorage');
} else {
console.warn('[ThinkingLibrary] 验证保存结果 - 未在localStorage中找到数据');
}
} catch (e) {
console.error('[ThinkingLibrary] 保存思考库失败:', e)
}
}
// 根据ID获取思考库
export function getThinkingLibraryById(id: string | undefined): ThinkingLibrary | undefined {
if (!id) return undefined
const libraries = getThinkingLibraries()
return libraries.find(lib => lib.id === id)
}
// 调试函数:显示思考库数据
export function debugThinkingLibraries(): void {
try {
const savedLibraries = localStorage.getItem('thinkingLibraries')
console.log('[ThinkingLibrary] DEBUG - localStorage中的原始数据:', savedLibraries)
if (savedLibraries) {
try {
const parsed = JSON.parse(savedLibraries) as ThinkingLibrary[]
console.log('[ThinkingLibrary] DEBUG - 解析后的思考库数量:', parsed.length)
console.log('[ThinkingLibrary] DEBUG - 思考库列表详情:', JSON.stringify(parsed, null, 2));
} catch (e) {
console.error('[ThinkingLibrary] DEBUG - 解析思考库JSON失败:', e)
}
} else {
console.log('[ThinkingLibrary] DEBUG - localStorage中没有思考库数据')
}
} catch (e) {
console.error('[ThinkingLibrary] DEBUG - 访问localStorage失败:', e)
}
}
// 添加思考库
export function addThinkingLibrary(library: Omit<ThinkingLibrary, 'id'>): ThinkingLibrary {
console.log('[ThinkingLibrary] 添加新思考库:', library.name)
const libraries = getThinkingLibraries()
const newLibrary: ThinkingLibrary = {
...library,
id: `lib_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`
}
console.log('[ThinkingLibrary] 添加前思考库数量:', libraries.length)
const updatedLibraries = [...libraries, newLibrary];
console.log('[ThinkingLibrary] 添加后思考库数量:', updatedLibraries.length)
saveThinkingLibraries(updatedLibraries)
console.log('[ThinkingLibrary] 新增库ID:', newLibrary.id);
return newLibrary
}
// 更新思考库
export function updateThinkingLibrary(library: ThinkingLibrary): boolean {
console.log('[ThinkingLibrary] 更新思考库 ID:', library.id, '名称:', library.name);
const libraries = getThinkingLibraries()
const index = libraries.findIndex(lib => lib.id === library.id)
if (index !== -1) {
const updatedLibraries = [...libraries];
updatedLibraries[index] = library;
saveThinkingLibraries(updatedLibraries);
console.log('[ThinkingLibrary] 思考库更新成功');
return true;
} else {
console.warn('[ThinkingLibrary] 更新失败未找到ID为', library.id, '的思考库');
return false;
}
}
// 删除思考库
export function deleteThinkingLibrary(id: string): boolean {
console.log('[ThinkingLibrary] 删除思考库 ID:', id);
const libraries = getThinkingLibraries()
const initialLength = libraries.length;
const filteredLibraries = libraries.filter(lib => lib.id !== id)
if (filteredLibraries.length < initialLength) {
saveThinkingLibraries(filteredLibraries);
console.log('[ThinkingLibrary] 思考库删除成功,剩余数量:', filteredLibraries.length);
return true;
} else {
console.warn('[ThinkingLibrary] 删除失败未找到ID为', id, '的思考库');
return false;
}
}

BIN
vector_db/chroma.sqlite3 Normal file

Binary file not shown.

BIN
vector_store.db Normal file

Binary file not shown.

View File

@ -470,6 +470,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.0.0":
version: 7.27.0
resolution: "@babel/runtime@npm:7.27.0"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10c0/35091ea9de48bd7fd26fb177693d64f4d195eb58ab2b142b893b7f3fa0f1d7c677604d36499ae0621a3703f35ba0c6a8f6c572cc8f7dc0317213841e493cf663
languageName: node
linkType: hard
"@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.9.2": "@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.9.2":
version: 7.26.10 version: 7.26.10
resolution: "@babel/runtime@npm:7.26.10" resolution: "@babel/runtime@npm:7.26.10"
@ -1395,13 +1404,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@google/genai@npm:^0.4.0": "@google/genai@npm:^0.8.0":
version: 0.4.0 version: 0.8.0
resolution: "@google/genai@npm:0.4.0" resolution: "@google/genai@npm:0.8.0"
dependencies: dependencies:
google-auth-library: "npm:^9.14.2" google-auth-library: "npm:^9.14.2"
ws: "npm:^8.18.0" ws: "npm:^8.18.0"
checksum: 10c0/4feb837b373cdbe60a5388b880b2384b116ffa369ae17ec2562c4e9da0f90e315d5e30c413ee3a620b6d147c55e1e9165f0e143aba6d945f1dfbe61fa584fefc checksum: 10c0/8a26a7dd1ab26aeeef5b5610612965ab271142460912c31b12f201cf6e00f5a4965910b195033992bdee1a7ee2b88c55f55d3a2727e09e4cd8d30ecbd0d655d0
languageName: node languageName: node
linkType: hard linkType: hard
@ -2374,6 +2383,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@langchain/core@npm:^0.3.44":
version: 0.3.44
resolution: "@langchain/core@npm:0.3.44"
dependencies:
"@cfworker/json-schema": "npm:^4.0.2"
ansi-styles: "npm:^5.0.0"
camelcase: "npm:6"
decamelize: "npm:1.2.0"
js-tiktoken: "npm:^1.0.12"
langsmith: "npm:>=0.2.8 <0.4.0"
mustache: "npm:^4.2.0"
p-queue: "npm:^6.6.2"
p-retry: "npm:4"
uuid: "npm:^10.0.0"
zod: "npm:^3.22.4"
zod-to-json-schema: "npm:^3.22.3"
checksum: 10c0/fb8d7c5760419cc9d0a3ed4f04473e103c8a27031566ba0c89438879bbd66e3d8869349f943045e86ddb33c4e8db4ae59311a3aad45e832d273b0e7d7db3f939
languageName: node
linkType: hard
"@langchain/openai@npm:0.3.16": "@langchain/openai@npm:0.3.16":
version: 0.3.16 version: 0.3.16
resolution: "@langchain/openai@npm:0.3.16" resolution: "@langchain/openai@npm:0.3.16"
@ -3908,6 +3937,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react-window@npm:^1.8.8":
version: 1.8.8
resolution: "@types/react-window@npm:1.8.8"
dependencies:
"@types/react": "npm:*"
checksum: 10c0/2170a3957752603e8b994840c5d31b72ddf94c427c0f42b0175b343cc54f50fe66161d8871e11786ec7a59906bd33861945579a3a8f745455a3744268ec1069f
languageName: node
linkType: hard
"@types/react@npm:*, @types/react@npm:^19.0.12": "@types/react@npm:*, @types/react@npm:^19.0.12":
version: 19.0.12 version: 19.0.12
resolution: "@types/react@npm:19.0.12" resolution: "@types/react@npm:19.0.12"
@ -4207,11 +4245,12 @@ __metadata:
"@emotion/is-prop-valid": "npm:^1.3.1" "@emotion/is-prop-valid": "npm:^1.3.1"
"@eslint-react/eslint-plugin": "npm:^1.36.1" "@eslint-react/eslint-plugin": "npm:^1.36.1"
"@eslint/js": "npm:^9.22.0" "@eslint/js": "npm:^9.22.0"
"@google/genai": "npm:^0.4.0" "@google/genai": "npm:^0.8.0"
"@google/generative-ai": "npm:^0.24.0" "@google/generative-ai": "npm:^0.24.0"
"@hello-pangea/dnd": "npm:^16.6.0" "@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0" "@kangfenmao/keyv-storage": "npm:^0.1.0"
"@langchain/community": "npm:^0.3.36" "@langchain/community": "npm:^0.3.36"
"@langchain/core": "npm:^0.3.44"
"@modelcontextprotocol/sdk": "npm:^1.9.0" "@modelcontextprotocol/sdk": "npm:^1.9.0"
"@mozilla/readability": "npm:^0.6.0" "@mozilla/readability": "npm:^0.6.0"
"@notionhq/client": "npm:^2.2.15" "@notionhq/client": "npm:^2.2.15"
@ -4232,6 +4271,7 @@ __metadata:
"@types/react": "npm:^19.0.12" "@types/react": "npm:^19.0.12"
"@types/react-dom": "npm:^19.0.4" "@types/react-dom": "npm:^19.0.4"
"@types/react-infinite-scroll-component": "npm:^5.0.0" "@types/react-infinite-scroll-component": "npm:^5.0.0"
"@types/react-window": "npm:^1.8.8"
"@types/tinycolor2": "npm:^1" "@types/tinycolor2": "npm:^1"
"@vitejs/plugin-react": "npm:^4.3.4" "@vitejs/plugin-react": "npm:^4.3.4"
"@xyflow/react": "npm:^12.4.4" "@xyflow/react": "npm:^12.4.4"
@ -4275,6 +4315,7 @@ __metadata:
html-to-image: "npm:^1.11.13" html-to-image: "npm:^1.11.13"
husky: "npm:^9.1.7" husky: "npm:^9.1.7"
i18next: "npm:^23.11.5" i18next: "npm:^23.11.5"
js-tiktoken: "npm:^1.0.19"
js-yaml: "npm:^4.1.0" js-yaml: "npm:^4.1.0"
jsdom: "npm:^26.0.0" jsdom: "npm:^26.0.0"
lint-staged: "npm:^15.5.0" lint-staged: "npm:^15.5.0"
@ -4301,6 +4342,8 @@ __metadata:
react-router: "npm:6" react-router: "npm:6"
react-router-dom: "npm:6" react-router-dom: "npm:6"
react-spinners: "npm:^0.14.1" react-spinners: "npm:^0.14.1"
react-virtualized-auto-sizer: "npm:^1.0.26"
react-window: "npm:^1.8.11"
redux: "npm:^5.0.1" redux: "npm:^5.0.1"
redux-persist: "npm:^6.0.0" redux-persist: "npm:^6.0.0"
rehype-katex: "npm:^7.0.1" rehype-katex: "npm:^7.0.1"
@ -4326,6 +4369,7 @@ __metadata:
vite: "npm:^5.0.12" vite: "npm:^5.0.12"
webdav: "npm:^5.8.0" webdav: "npm:^5.8.0"
zipread: "npm:^1.3.3" zipread: "npm:^1.3.3"
zod: "npm:^3.24.2"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -4926,9 +4970,9 @@ __metadata:
linkType: hard linkType: hard
"bignumber.js@npm:^9.0.0": "bignumber.js@npm:^9.0.0":
version: 9.1.2 version: 9.2.1
resolution: "bignumber.js@npm:9.1.2" resolution: "bignumber.js@npm:9.2.1"
checksum: 10c0/e17786545433f3110b868725c449fa9625366a6e675cd70eb39b60938d6adbd0158cb4b3ad4f306ce817165d37e63f4aa3098ba4110db1d9a3b9f66abfbaf10d checksum: 10c0/f50b2f2d633382ac5ab86f8baa90437cf6f14adfa8bd47b7159f1b893d19777853429565c33dfe6f8f695c5361c1e3cd2aae5067b99093d5608d671683c56cb4
languageName: node languageName: node
linkType: hard linkType: hard
@ -10380,7 +10424,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"js-tiktoken@npm:^1.0.12, js-tiktoken@npm:^1.0.14": "js-tiktoken@npm:^1.0.12, js-tiktoken@npm:^1.0.14, js-tiktoken@npm:^1.0.19":
version: 1.0.19 version: 1.0.19
resolution: "js-tiktoken@npm:1.0.19" resolution: "js-tiktoken@npm:1.0.19"
dependencies: dependencies:
@ -11638,6 +11682,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"memoize-one@npm:>=3.1.1 <6":
version: 5.2.1
resolution: "memoize-one@npm:5.2.1"
checksum: 10c0/fd22dbe9a978a2b4f30d6a491fc02fb90792432ad0dab840dc96c1734d2bd7c9cdeb6a26130ec60507eb43230559523615873168bcbe8fafab221c30b11d54c1
languageName: node
linkType: hard
"memoize-one@npm:^6.0.0": "memoize-one@npm:^6.0.0":
version: 6.0.0 version: 6.0.0
resolution: "memoize-one@npm:6.0.0" resolution: "memoize-one@npm:6.0.0"
@ -14919,6 +14970,29 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-virtualized-auto-sizer@npm:^1.0.26":
version: 1.0.26
resolution: "react-virtualized-auto-sizer@npm:1.0.26"
peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/788b438c9cb55f94a0561ef07e6bb6e5051ad3d5ececd9b2131014324ffe773b507ac7060f965e44c84bd8d6aa85c686754ac944384878c97f7304c0473a7754
languageName: node
linkType: hard
"react-window@npm:^1.8.11":
version: 1.8.11
resolution: "react-window@npm:1.8.11"
dependencies:
"@babel/runtime": "npm:^7.0.0"
memoize-one: "npm:>=3.1.1 <6"
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/5ae8da1bc5c47d8f0a428b28a600256e2db511975573e52cb65a9b27ed1a0e5b9f7b3bee5a54fb0da93956d782c24010be434be451072f46ba5a89159d2b3944
languageName: node
linkType: hard
"react@npm:^19.0.0": "react@npm:^19.0.0":
version: 19.0.0 version: 19.0.0
resolution: "react@npm:19.0.0" resolution: "react@npm:19.0.0"
@ -18112,7 +18186,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"zod@npm:^3.22.3, zod@npm:^3.22.4, zod@npm:^3.23.8": "zod@npm:^3.22.3, zod@npm:^3.22.4, zod@npm:^3.23.8, zod@npm:^3.24.2":
version: 3.24.2 version: 3.24.2
resolution: "zod@npm:3.24.2" resolution: "zod@npm:3.24.2"
checksum: 10c0/c638c7220150847f13ad90635b3e7d0321b36cce36f3fc6050ed960689594c949c326dfe2c6fa87c14b126ee5d370ccdebd6efb304f41ef5557a4aaca2824565 checksum: 10c0/c638c7220150847f13ad90635b3e7d0321b36cce36f3fc6050ed960689594c949c326dfe2c6fa87c14b126ee5d370ccdebd6efb304f41ef5557a4aaca2824565

View File

@ -1,280 +0,0 @@
# 敏感信息过滤功能实现方案(修改版)
## 需求分析
用户希望增加一个按钮,控制记忆功能是否过滤密钥等安全敏感信息。当开启过滤功能时,分析模型会过滤掉密钥等敏感信息;关闭则不过滤。此功能对于保护用户隐私和敏感数据至关重要。
## 实现思路
1. 在Redux状态中添加一个新的状态属性`filterSensitiveInfo`
2. 在设置界面中添加一个开关按钮,默认为开启状态
3. 修改分析函数,根据`filterSensitiveInfo`状态添加过滤指令
4. 添加日志记录,跟踪过滤状态的变化
## 修改文件
### 1. 修改 src/renderer/src/store/memory.ts
```typescript
// 在 MemoryState 接口中添加
export interface MemoryState {
// 其他属性...
filterSensitiveInfo: boolean // 是否过滤敏感信息
}
// 在 initialState 中添加
const initialState: MemoryState = {
// 其他属性...
filterSensitiveInfo: true, // 默认启用敏感信息过滤
}
// 添加新的 action creator
setFilterSensitiveInfo: (state, action: PayloadAction<boolean>) => {
state.filterSensitiveInfo = action.payload
},
// 导出 action
export const {
// 其他 actions...
setFilterSensitiveInfo,
} = memorySlice.actions
// 修改 saveMemoryData 函数,确保 filterSensitiveInfo 设置也被保存
const completeData = {
// 基本设置
isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive,
shortMemoryActive: memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive,
autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze,
filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo,
// 其他属性...
}
// 同样修改 saveLongTermMemoryData 函数
const completeData = {
// 基本设置
isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive,
autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze,
filterSensitiveInfo: memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo,
// 其他属性...
}
```
### 2. 修改 src/renderer/src/pages/settings/MemorySettings/index.tsx
```typescript
// 导入 InfoCircleOutlined 图标
import {
AppstoreOutlined,
DeleteOutlined,
EditOutlined,
InfoCircleOutlined,
PlusOutlined,
SearchOutlined,
UnorderedListOutlined
} from '@ant-design/icons'
// 导入 setFilterSensitiveInfo action
import {
addMemory,
clearMemories,
deleteMemory,
editMemory,
setAnalyzeModel,
setAnalyzing,
setAutoAnalyze,
setFilterSensitiveInfo,
setMemoryActive,
setShortMemoryAnalyzeModel,
saveMemoryData,
saveLongTermMemoryData,
saveAllMemorySettings
} from '@renderer/store/memory'
// 从 Redux 获取 filterSensitiveInfo 状态
const filterSensitiveInfo = useAppSelector((state) => state.memory?.filterSensitiveInfo ?? true) // 默认启用敏感信息过滤
// 添加处理切换敏感信息过滤的函数
const handleToggleFilterSensitiveInfo = async (checked: boolean) => {
dispatch(setFilterSensitiveInfo(checked))
console.log('[Memory Settings] Filter sensitive info set:', checked)
// 使用Redux Thunk保存到JSON文件
try {
await dispatch(saveMemoryData({ filterSensitiveInfo: checked })).unwrap()
console.log('[Memory Settings] Filter sensitive info saved to file successfully:', checked)
} catch (error) {
console.error('[Memory Settings] Failed to save filter sensitive info to file:', error)
}
}
// 在短期记忆设置中添加开关按钮
<SettingRow>
<SettingRowTitle>
{t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'}
<Tooltip title={t('settings.memory.filterSensitiveInfoTip') || '启用后记忆功能将不会提取API密钥、密码等敏感信息'}>
<InfoCircleOutlined style={{ marginLeft: 8 }} />
</Tooltip>
</SettingRowTitle>
<Switch checked={filterSensitiveInfo} onChange={handleToggleFilterSensitiveInfo} disabled={!isActive} />
</SettingRow>
// 在长期记忆设置中也添加相同的开关按钮
<SettingRow>
<SettingRowTitle>
{t('settings.memory.filterSensitiveInfo') || '过滤敏感信息'}
<Tooltip title={t('settings.memory.filterSensitiveInfoTip') || '启用后记忆功能将不会提取API密钥、密码等敏感信息'}>
<InfoCircleOutlined style={{ marginLeft: 8 }} />
</Tooltip>
</SettingRowTitle>
<Switch checked={filterSensitiveInfo} onChange={handleToggleFilterSensitiveInfo} disabled={!isActive} />
</SettingRow>
```
### 3. 修改 src/renderer/src/services/MemoryService.ts
```typescript
// 修改 analyzeConversation 函数
const analyzeConversation = async (
conversation: string,
modelId: string,
customPrompt?: string
): Promise<Array<{ content: string; category: string }>> => {
try {
// 获取当前的过滤敏感信息设置
const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true
// 使用自定义提示词或默认提示词
let basePrompt =
customPrompt ||
`
请分析对话内容,提取出重要的用户偏好、习惯、需求和背景信息,这些信息在未来的对话中可能有用。
将每条信息分类并按以下格式返回:
类别: 信息内容
类别应该是以下几种之一:
- 用户偏好:用户喜好、喜欢的事物、风格等
- 技术需求:用户的技术相关需求、开发偏好等
- 个人信息:用户的背景、经历等个人信息
- 交互偏好:用户喜欢的交流方式、沟通风格等
- 其他:不属于以上类别的重要信息
请确保每条信息都是简洁、准确的。如果没有找到重要信息,请返回空字符串。
`
// 如果启用了敏感信息过滤,添加相关指令
if (filterSensitiveInfo) {
basePrompt += `
## 安全提示:
请注意不要提取任何敏感信息,包括但不限于:
- API密钥、访问令牌或其他凭证
- 密码或密码提示
- 私人联系方式(如电话号码、邮箱地址)
- 个人身份信息(如身份证号、社保号)
- 银行账户或支付信息
- 私密的个人或商业信息
如果发现此类信息,请完全忽略,不要以任何形式记录或提取。
`
}
// 其余代码保持不变...
}
}
// 修改 analyzeAndAddShortMemories 函数
export const analyzeAndAddShortMemories = async (topicId: string) => {
// 其他代码...
try {
// 获取当前的过滤敏感信息设置
const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true
// 构建短期记忆分析提示词
let prompt = `
请对以下对话内容进行非常详细的分析和总结提取对当前对话至关重要的上下文信息。请注意这个分析将用于生成短期记忆帮助AI理解当前对话的完整上下文。
分析要求:
1. 非常详细地总结用户的每一句话中表达的关键信息、需求和意图
2. 全面分析AI回复中的重要内容和对用户问题的解决方案
3. 详细记录对话中的重要事实、数据、代码示例和具体细节
4. 清晰捕捉对话的逻辑发展、转折点和关键决策
5. 提取对理解当前对话上下文必不可少的信息
6. 记录用户提出的具体问题和关注点
7. 捕捉用户在对话中表达的偏好、困惑和反馈
8. 记录对话中提到的文件、路径、变量名等具体技术细节
`
// 如果启用了敏感信息过滤,添加相关指令
if (filterSensitiveInfo) {
prompt += `
9. 请注意不要提取任何敏感信息,包括但不限于:
- API密钥、访问令牌或其他凭证
- 密码或密码提示
- 私人联系方式(如电话号码、邮箱地址)
- 个人身份信息(如身份证号、社保号)
- 银行账户或支付信息
- 私密的个人或商业信息
如果发现此类信息,请完全忽略,不要以任何形式记录或提取。
`
}
// 其余代码保持不变...
}
}
```
### 4. 修改 src/renderer/src/i18n/locales/zh-cn.json 和 en-us.json
```json
{
"settings": {
"memory": {
"filterSensitiveInfo": "过滤敏感信息",
"filterSensitiveInfoTip": "启用后记忆功能将不会提取API密钥、密码等敏感信息"
}
}
}
```
```json
{
"settings": {
"memory": {
"filterSensitiveInfo": "Filter Sensitive Information",
"filterSensitiveInfoTip": "When enabled, memory function will not extract API keys, passwords, or other sensitive information"
}
}
}
```
## 实现效果
这些修改后,用户将能够通过开关按钮控制记忆功能是否过滤敏感信息:
1. 当开启过滤功能时默认状态分析模型会被明确指示不要提取API密钥、密码等敏感信息
2. 当关闭过滤功能时,分析模型会正常提取所有信息,包括可能的敏感信息
开关按钮会出现在短期记忆和长期记忆设置中,用户可以根据需要随时切换。设置会被保存到配置文件中,确保应用重启后设置仍然生效。
## 思考过程
1. **状态管理**首先考虑如何在Redux中添加新的状态属性并确保它能够被正确保存和加载。
2. **UI设计**:在设置界面中添加开关按钮,并提供提示信息,帮助用户理解这个功能的作用。
3. **提示词修改**:根据开关状态修改分析提示词,添加不要提取敏感信息的指令。这是实现过滤功能的核心部分。
4. **国际化支持**:添加相关的翻译键值对,确保功能在不同语言环境下都能正常使用。
5. **持久化**:确保设置能够被正确保存到配置文件中,并在应用重启后加载。
## 注意事项
1. 这个功能只能在一定程度上防止敏感信息被提取但不能完全保证。如果用户在对话中明确提到了敏感信息AI模型可能仍然会提取部分内容。
2. 建议用户在讨论敏感信息时,最好暂时关闭记忆功能,或者在对话中避免提及敏感信息。
3. 这个功能只影响新分析的对话内容,已经存储的记忆不会受到影响。如果用户想要清除可能包含敏感信息的记忆,需要手动删除这些记忆。