mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 23:22:05 +08:00
添加了 TTS 相关服务并更新了设置
This commit is contained in:
parent
877beeab43
commit
53708a973f
@ -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
69
asr-server/test.js
Normal 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
64
check_json.js
Normal 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
1073
checkpoint.py
Normal file
File diff suppressed because it is too large
Load Diff
20
checkpoint_config.json
Normal file
20
checkpoint_config.json
Normal 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
343
const extractedData = {.md
Normal 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
14
create_empty_agents.js
Normal 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文件,内容为空数组');
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
9112
resources/data/agents.json.bak
Normal file
9112
resources/data/agents.json.bak
Normal file
File diff suppressed because one or more lines are too long
9112
resources/data/agents_clean.json
Normal file
9112
resources/data/agents_clean.json
Normal file
File diff suppressed because one or more lines are too long
9112
resources/data/agents_fixed.json
Normal file
9112
resources/data/agents_fixed.json
Normal file
File diff suppressed because one or more lines are too long
@ -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 如果端口可用则返回true,否则返回false
|
||||||
*/
|
*/
|
||||||
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();
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: '',
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
222
src/renderer/src/components/AssistantMemoryPopup.tsx
Normal file
222
src/renderer/src/components/AssistantMemoryPopup.tsx
Normal 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
|
||||||
78
src/renderer/src/components/DeepClaudeProvider.tsx
Normal file
78
src/renderer/src/components/DeepClaudeProvider.tsx
Normal 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
|
||||||
250
src/renderer/src/components/Popups/AssistantMemoryPopup.tsx
Normal file
250
src/renderer/src/components/Popups/AssistantMemoryPopup.tsx
Normal 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
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
257
src/renderer/src/components/Popups/ModelEditPopup.tsx
Normal file
257
src/renderer/src/components/Popups/ModelEditPopup.tsx
Normal 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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/renderer/src/components/Popups/ModelSettingsButton.tsx
Normal file
64
src/renderer/src/components/Popups/ModelSettingsButton.tsx
Normal 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
|
||||||
@ -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')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "添加提供商",
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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 }) => {
|
|||||||
'>': '>',
|
'>': '>',
|
||||||
'"': '"',
|
'"': '"',
|
||||||
"'": '''
|
"'": '''
|
||||||
}
|
};
|
||||||
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);
|
||||||
@ -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={{
|
||||||
|
|||||||
@ -172,6 +172,6 @@ const NarrowIcon = styled(NavbarIcon)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// AnalyzeButton组件已移动到话题右键工具栏中
|
||||||
|
|
||||||
export default HeaderNavbar
|
export default HeaderNavbar
|
||||||
|
|||||||
556
src/renderer/src/pages/settings/DeepClaudeSettings/index.tsx
Normal file
556
src/renderer/src/pages/settings/DeepClaudeSettings/index.tsx
Normal 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
|
||||||
@ -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
|
||||||
@ -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 }}
|
||||||
|
|||||||
@ -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
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
@ -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}>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 />} />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
{/* 重置按钮 */}
|
{/* 重置按钮 */}
|
||||||
|
|||||||
494
src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts
Normal file
494
src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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('浏览器尚未准备好')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
278
src/renderer/src/services/AssistantMemoryService.ts
Normal file
278
src/renderer/src/services/AssistantMemoryService.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
// 获取当前话题的短记忆
|
// 获取当前话题的短记忆
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
2
src/renderer/src/types/asr.d.ts
vendored
2
src/renderer/src/types/asr.d.ts
vendored
@ -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 }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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'
|
||||||
|
|
||||||
|
|||||||
12
src/renderer/src/types/openai.ts
Normal file
12
src/renderer/src/types/openai.ts
Normal 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'
|
||||||
143
src/renderer/src/utils/createDeepClaudeProvider.ts
Normal file
143
src/renderer/src/utils/createDeepClaudeProvider.ts
Normal 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]
|
||||||
|
}
|
||||||
@ -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(() => {
|
||||||
|
|||||||
625
src/renderer/src/utils/thinkingLibrary.ts
Normal file
625
src/renderer/src/utils/thinkingLibrary.ts
Normal 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):**
|
||||||
|
* **提出假设:** 基于背景研究和初步理解,提出一个或多个具体的、可检验的(Testable)、可证伪的(Falsifiable)科学假设来解释现象或回答问题。
|
||||||
|
* **逻辑依据:** 说明每个假设提出的逻辑基础或理论依据。
|
||||||
|
* **具体预测:** 对于每个假设,如果它是正确的,我们预期会观察到什么具体的结果或数据模式?
|
||||||
|
|
||||||
|
3. **研究设计/证据策略 (Research Design / Evidence Strategy):**
|
||||||
|
* **方法论选择:** 提出收集证据以检验假设的最佳方法。是实验研究、观察研究、模型模拟、文献分析还是其他?说明选择理由。
|
||||||
|
* **数据需求:** 需要哪些具体的数据或证据类型来支持或反驳假设?数据的来源是什么?
|
||||||
|
* **分析计划:** 计划使用哪些分析方法(例如统计检验、模式识别、比较分析)来处理预期收集到的数据?
|
||||||
|
|
||||||
|
4. **证据分析与解释 (Evidence Analysis & Interpretation - *可基于预期或已有信息*):**
|
||||||
|
* **系统评估:** (如果已有数据)系统地分析可用证据/数据,寻找支持或反对各个假设的模式、趋势或统计显著性。
|
||||||
|
* **结果阐释:** (或基于预期)如果观察到了预测A,它对假设X意味着什么?如果观察到了预测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):**
|
||||||
|
* **推理类型:** 这是演绎(Deductive)论证(旨在保证结论为真)还是归纳(Inductive)论证(旨在使结论很可能为真)?或是其他类型(如类比推理、因果推理)?
|
||||||
|
* **结构图示 (可选):** 尝试用逻辑符号或图示(如流程图)来表示论证的结构。
|
||||||
|
|
||||||
|
4. **有效性/强度评估 (Validity/Strength Evaluation):**
|
||||||
|
* **演绎有效性:** 如果是演绎论证,假设前提都为真,结论是否*必然*为真?论证形式是否有效?
|
||||||
|
* **归纳强度:** 如果是归纳论证,前提在多大程度上支持结论?证据是否充分、相关、具有代表性?
|
||||||
|
|
||||||
|
5. **可靠性/可信度评估 (Soundness/Cogency Evaluation):**
|
||||||
|
* **前提真实性/可接受性:** (基于演绎/归纳评估)前提本身是否真实或可被接受?有无证据支持?
|
||||||
|
* **论证可靠性:** 对于有效的演绎论证,如果前提都为真,则论证是可靠的(Sound)。对于强的归纳论证,如果前提都为真,则论证是可信的(Cogent)。评估整体可靠性/可信度。
|
||||||
|
|
||||||
|
6. **逻辑谬误审查 (Fallacy Check):**
|
||||||
|
* **系统排查:** 主动检查是否存在形式谬误(Formal Fallacies,如肯定后件)或非形式谬误(Informal 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):**
|
||||||
|
* **理论基础:** 选择或构建一个合适的理论框架来指导研究。该框架如何帮助理解研究问题?
|
||||||
|
* **概念模型 (可选):** 绘制概念模型图,展示关键变量及其预期的相互关系。
|
||||||
|
* **核心假设:** 基于理论框架和研究问题,提出具体的、可检验的(Testable)、可证伪的(Falsifiable)研究假设 (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矩阵。
|
||||||
|
* 着重思考因素之间的交叉影响,例如:
|
||||||
|
* 如何用优势(S)抓住机会(O)?
|
||||||
|
* 如何用优势(S)规避威胁(T)?
|
||||||
|
* 如何克服劣势(W)抓住机会(O)?
|
||||||
|
* 如何最大限度地减少劣势(W)和威胁(T)的负面影响?
|
||||||
|
|
||||||
|
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
BIN
vector_db/chroma.sqlite3
Normal file
Binary file not shown.
BIN
vector_store.db
Normal file
BIN
vector_store.db
Normal file
Binary file not shown.
94
yarn.lock
94
yarn.lock
@ -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
|
||||||
|
|||||||
280
敏感信息过滤功能实现方案.txt
280
敏感信息过滤功能实现方案.txt
@ -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. 这个功能只影响新分析的对话内容,已经存储的记忆不会受到影响。如果用户想要清除可能包含敏感信息的记忆,需要手动删除这些记忆。
|
|
||||||
Loading…
Reference in New Issue
Block a user