Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc2e822ea9 | |||
| 53db607b8d | |||
| c346418d68 |
@@ -1345,10 +1345,20 @@ def get_frontend_config():
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取默认LLM配置
|
||||
cursor.execute('SELECT * FROM llm_configs WHERE is_default=1 AND is_active=1 LIMIT 1')
|
||||
# 先获取默认对话配置
|
||||
cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1')
|
||||
chat_config = cursor.fetchone()
|
||||
|
||||
# 根据对话配置中的 llm_config_id 获取对应的 LLM 配置
|
||||
llm_config_id = chat_config['llm_config_id'] if chat_config else 1
|
||||
cursor.execute('SELECT * FROM llm_configs WHERE id=?', (llm_config_id,))
|
||||
llm = cursor.fetchone()
|
||||
|
||||
# 如果找不到对应的LLM配置,使用默认的
|
||||
if not llm:
|
||||
cursor.execute('SELECT * FROM llm_configs WHERE is_default=1 LIMIT 1')
|
||||
llm = cursor.fetchone()
|
||||
|
||||
# 获取默认工具配置(搜索等)
|
||||
cursor.execute('SELECT * FROM tool_configs WHERE is_default=1 AND is_active=1')
|
||||
tools = [dict(row) for row in cursor.fetchall()]
|
||||
@@ -1361,10 +1371,6 @@ def get_frontend_config():
|
||||
cursor.execute('SELECT agent_id, name, avatar, category, description, system_prompt, heat, tags, enable_tools FROM agents WHERE is_online=1 AND is_active=1')
|
||||
agents = [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
# 获取默认对话配置
|
||||
cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1')
|
||||
chat_config = cursor.fetchone()
|
||||
|
||||
# 获取系统配置
|
||||
cursor.execute('SELECT key, value FROM system_configs')
|
||||
system = {row['key']: row['value'] for row in cursor.fetchall()}
|
||||
|
||||
252
www/app.js
252
www/app.js
@@ -53,9 +53,11 @@ let backendConfig = null; // 从API获取的配置
|
||||
let currentUser = null; // 当前登录用户 { username, password, registeredAt }
|
||||
|
||||
// TTS 语音播放状态
|
||||
let enableTTS = false; // 是否启用语音播放
|
||||
let enableTTS = false; // 是否启用语音播放(默认关闭)
|
||||
let currentPlayingAudio = null; // 当前播放的音频对象
|
||||
let ttsVoice = 'zh-CN-XiaoxiaoNeural'; // TTS 语音
|
||||
let ttsQueue = []; // TTS 待播放队列
|
||||
let isTTSPlaying = false; // 是否正在播放队列
|
||||
|
||||
// 每日使用统计(未登录用户)
|
||||
let dailyUsage = {
|
||||
@@ -3137,6 +3139,10 @@ async function openAgent(agentId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 重置 TTS 状态(新对话默认关闭)
|
||||
enableTTS = false;
|
||||
stopTTSQueue();
|
||||
|
||||
// 创建新对话并设置智能体
|
||||
const newConv = {
|
||||
id: backendId || Date.now().toString(),
|
||||
@@ -3159,6 +3165,9 @@ async function openAgent(agentId) {
|
||||
function showAgentChatPage() {
|
||||
if (!currentAgent || !currentConversation) return;
|
||||
|
||||
// 停止之前的 TTS 播放
|
||||
stopTTSQueue();
|
||||
|
||||
const chatHtml = `
|
||||
<div class="chat-page">
|
||||
<header class="chat-header">
|
||||
@@ -3616,6 +3625,10 @@ async function createNewConversation() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置 TTS 状态(新对话默认关闭)
|
||||
enableTTS = false;
|
||||
stopTTSQueue();
|
||||
|
||||
// 如果用户已登录,先在 backend 创建对话
|
||||
let backendId = null;
|
||||
if (currentUser && currentUser.id) {
|
||||
@@ -3653,6 +3666,9 @@ async function createNewConversation() {
|
||||
|
||||
// 打开对话
|
||||
function openConversation(id) {
|
||||
// 停止之前的 TTS 播放
|
||||
stopTTSQueue();
|
||||
|
||||
currentConversation = conversations.find(c => c.id === id);
|
||||
if (!currentConversation) {
|
||||
showConversationList();
|
||||
@@ -4088,6 +4104,8 @@ async function streamGenerate(userMsgIndex) {
|
||||
let buffer = '';
|
||||
let thinkingOutputStarted = false; // 正式内容是否开始输出
|
||||
let abortController = new AbortController(); // 用于中断流
|
||||
let ttsAccumulatedText = ''; // TTS 累计文本
|
||||
let ttsLastPlayedIndex = 0; // 上次播放的文本位置
|
||||
|
||||
// 绑定停止按钮事件
|
||||
const stopBtn = document.getElementById('stopGenerateBtn');
|
||||
@@ -4157,6 +4175,32 @@ async function streamGenerate(userMsgIndex) {
|
||||
|
||||
currentConversation.messages[aiMessageIndex].content += delta.content;
|
||||
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
|
||||
|
||||
// 流式TTS播放:积累文本并播放
|
||||
if (enableTTS) {
|
||||
ttsAccumulatedText += delta.content;
|
||||
// 每80字符播放一段(避免太短频繁请求)
|
||||
if (ttsAccumulatedText.length >= 80) {
|
||||
// 找到合适的断句点(句子结束)
|
||||
const breakPoints = ['。', '!', '?', ';', '.', '!', '?', ';', '\n'];
|
||||
let breakIndex = -1;
|
||||
for (let i = ttsAccumulatedText.length - 1; i >= ttsLastPlayedIndex + 40; i--) {
|
||||
if (breakPoints.includes(ttsAccumulatedText[i])) {
|
||||
breakIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果找到断句点,或者积累超过120字符强制播放
|
||||
if (breakIndex >= 0 || ttsAccumulatedText.length - ttsLastPlayedIndex >= 120) {
|
||||
const textToPlay = ttsAccumulatedText.slice(ttsLastPlayedIndex, breakIndex >= 0 ? breakIndex + 1 : ttsAccumulatedText.length);
|
||||
if (textToPlay.trim()) {
|
||||
addToTTSQueue(textToPlay.trim());
|
||||
}
|
||||
ttsLastPlayedIndex = breakIndex >= 0 ? breakIndex + 1 : ttsAccumulatedText.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
@@ -4170,11 +4214,21 @@ async function streamGenerate(userMsgIndex) {
|
||||
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
|
||||
}
|
||||
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
|
||||
|
||||
// 播放剩余未播放的文本(流式TTS)
|
||||
if (enableTTS && ttsAccumulatedText && ttsLastPlayedIndex < ttsAccumulatedText.length) {
|
||||
const remainingText = ttsAccumulatedText.slice(ttsLastPlayedIndex).trim();
|
||||
if (remainingText) {
|
||||
addToTTSQueue(remainingText);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
|
||||
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
|
||||
// 出错时停止TTS队列
|
||||
stopTTSQueue();
|
||||
} finally {
|
||||
isLoading = false;
|
||||
sendBtn.disabled = false;
|
||||
@@ -4187,12 +4241,6 @@ async function streamGenerate(userMsgIndex) {
|
||||
// 记录统计到 backend
|
||||
logStatsToBackend('llm_call', currentConversation.agentId || 'chat', 1);
|
||||
|
||||
// 自动播放 TTS
|
||||
const lastMsg = currentConversation.messages[aiMessageIndex];
|
||||
if (enableTTS && lastMsg && lastMsg.content) {
|
||||
autoPlayTTS(lastMsg.content);
|
||||
}
|
||||
|
||||
// 自动总结标题:第一次对话和每隔5次对话
|
||||
const totalMessages = currentConversation.messages.length;
|
||||
// 第一次对话(用户+AI=2条)或每5次对话(10条)
|
||||
@@ -4740,23 +4788,162 @@ function renderMessages() {
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// TTS 语音播放
|
||||
// ==================== TTS 队列播放 ====================
|
||||
|
||||
// 清理 TTS 文本(过滤Markdown特殊字符和表情)
|
||||
function cleanTTSText(text) {
|
||||
if (!text) return '';
|
||||
|
||||
let cleaned = text;
|
||||
|
||||
// 移除代码块(```code```)
|
||||
cleaned = cleaned.replace(/```[\s\S]*?```/g, '');
|
||||
|
||||
// 移除行内代码(`code`)
|
||||
cleaned = cleaned.replace(/`[^`]+`/g, '');
|
||||
|
||||
// 移除标题符号(#、##、###等)
|
||||
cleaned = cleaned.replace(/^#{1,6}\s*/gm, '');
|
||||
|
||||
// 移除列表符号(-、*、+)
|
||||
cleaned = cleaned.replace(/^[\-\*\+]\s+/gm, '');
|
||||
|
||||
// 移除数字列表(1.、2.等)
|
||||
cleaned = cleaned.replace(/^\d+\.\s+/gm, '');
|
||||
|
||||
// 处理链接 [text](url) -> 只保留text
|
||||
cleaned = cleaned.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
||||
|
||||
// 移除粗体/斜体符号(**text**、*text*、__text__、_text_)
|
||||
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1');
|
||||
cleaned = cleaned.replace(/\*([^*]+)\*/g, '$1');
|
||||
cleaned = cleaned.replace(/__([^_]+)__/g, '$1');
|
||||
cleaned = cleaned.replace(/_([^_]+)_/g, '$1');
|
||||
|
||||
// 移除引用符号(>)
|
||||
cleaned = cleaned.replace(/^>\s*/gm, '');
|
||||
|
||||
// 移除分割线(---、***)
|
||||
cleaned = cleaned.replace(/^[\-\*]{3,}$/gm, '');
|
||||
|
||||
// 移除表情符号(常见emoji范围)
|
||||
cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}]/gu, ''); // 表情
|
||||
cleaned = cleaned.replace(/[\u{1F300}-\u{1F5FF}]/gu, ''); // 符号和图形
|
||||
cleaned = cleaned.replace(/[\u{1F680}-\u{1F6FF}]/gu, ''); // 交通和地图
|
||||
cleaned = cleaned.replace(/[\u{1F700}-\u{1F77F}]/gu, ''); // 占星术
|
||||
cleaned = cleaned.replace(/[\u{1F780}-\u{1F7FF}]/gu, ''); // 几何图形
|
||||
cleaned = cleaned.replace(/[\u{1F800}-\u{1F8FF}]/gu, ''); // 补充箭头
|
||||
cleaned = cleaned.replace(/[\u{1F900}-\u{1F9FF}]/gu, ''); // 补充符号
|
||||
cleaned = cleaned.replace(/[\u{1FA00}-\u{1FA6F}]/gu, ''); // 游戏符号
|
||||
cleaned = cleaned.replace(/[\u{1FA70}-\u{1FAFF}]/gu, ''); // 补充符号B
|
||||
cleaned = cleaned.replace(/[\u{2600}-\u{26FF}]/gu, ''); // 杂项符号
|
||||
cleaned = cleaned.replace(/[\u{2700}-\u{27BF}]/gu, ''); // 装饰符号
|
||||
cleaned = cleaned.replace(/[\u{FE00}-\u{FE0F}]/gu, ''); // 变体选择符
|
||||
cleaned = cleaned.replace(/[\u{1F1E0}-\u{1F1FF}]/gu, ''); // 旗帜
|
||||
|
||||
// 移除HTML实体
|
||||
cleaned = cleaned.replace(/&[a-zA-Z]+;/g, '');
|
||||
|
||||
// 清理多余空白
|
||||
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
// 添加文本到 TTS 队列
|
||||
async function addToTTSQueue(text) {
|
||||
if (!text || !enableTTS) return;
|
||||
|
||||
ttsQueue.push(text);
|
||||
|
||||
// 如果没有在播放,开始播放队列
|
||||
if (!isTTSPlaying) {
|
||||
playTTSQueue();
|
||||
}
|
||||
}
|
||||
|
||||
// 播放 TTS 队列
|
||||
async function playTTSQueue() {
|
||||
if (ttsQueue.length === 0) {
|
||||
isTTSPlaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isTTSPlaying = true;
|
||||
|
||||
while (ttsQueue.length > 0 && enableTTS) {
|
||||
const rawText = ttsQueue.shift();
|
||||
const text = cleanTTSText(rawText); // 清理文本
|
||||
|
||||
if (!text) continue; // 跳过空文本
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/tts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, voice: ttsVoice })
|
||||
});
|
||||
|
||||
if (!response.ok || !enableTTS) break;
|
||||
|
||||
const audioBlob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
|
||||
// 播放音频
|
||||
currentPlayingAudio = new Audio(audioUrl);
|
||||
await currentPlayingAudio.play();
|
||||
|
||||
// 等待播放完成
|
||||
await new Promise(resolve => {
|
||||
currentPlayingAudio.onended = resolve;
|
||||
currentPlayingAudio.onerror = resolve;
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(audioUrl);
|
||||
currentPlayingAudio = null;
|
||||
|
||||
} catch (e) {
|
||||
console.error('TTS播放失败:', e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isTTSPlaying = false;
|
||||
ttsQueue = []; // 清空队列
|
||||
}
|
||||
|
||||
// 停止 TTS 队列播放
|
||||
function stopTTSQueue() {
|
||||
ttsQueue = [];
|
||||
isTTSPlaying = false;
|
||||
if (currentPlayingAudio) {
|
||||
currentPlayingAudio.pause();
|
||||
currentPlayingAudio = null;
|
||||
}
|
||||
}
|
||||
|
||||
// TTS 手动播放(点击消息上的播放按钮)
|
||||
async function playTTS(index) {
|
||||
if (!currentConversation || index < 0) return;
|
||||
|
||||
const msg = currentConversation.messages[index];
|
||||
if (!msg || msg.role !== 'assistant') return;
|
||||
|
||||
const text = msg.content;
|
||||
const rawText = msg.content;
|
||||
if (!rawText) {
|
||||
showToast('没有可播放的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
const text = cleanTTSText(rawText); // 清理文本
|
||||
if (!text) {
|
||||
showToast('没有可播放的内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果正在播放,停止播放
|
||||
if (currentPlayingAudio) {
|
||||
currentPlayingAudio.pause();
|
||||
currentPlayingAudio = null;
|
||||
// 如果正在播放或队列中有内容,停止
|
||||
if (currentPlayingAudio || isTTSPlaying) {
|
||||
stopTTSQueue();
|
||||
showToast('已停止播放');
|
||||
return;
|
||||
}
|
||||
@@ -4783,7 +4970,7 @@ async function playTTS(index) {
|
||||
|
||||
// 播放音频
|
||||
currentPlayingAudio = new Audio(audioUrl);
|
||||
currentPlayingAudio.play();
|
||||
await currentPlayingAudio.play();
|
||||
|
||||
showToast('开始播放');
|
||||
|
||||
@@ -4799,43 +4986,6 @@ async function playTTS(index) {
|
||||
}
|
||||
}
|
||||
|
||||
// 自动播放 TTS(AI回复完成后)
|
||||
function autoPlayTTS(text) {
|
||||
if (!enableTTS || !text) return;
|
||||
|
||||
// 延迟一点时间,让用户先看到内容
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, voice: ttsVoice })
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
|
||||
const audioBlob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
|
||||
// 停止之前的播放
|
||||
if (currentPlayingAudio) {
|
||||
currentPlayingAudio.pause();
|
||||
}
|
||||
|
||||
currentPlayingAudio = new Audio(audioUrl);
|
||||
currentPlayingAudio.play();
|
||||
|
||||
currentPlayingAudio.onended = () => {
|
||||
URL.revokeObjectURL(audioUrl);
|
||||
currentPlayingAudio = null;
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error('自动TTS播放失败:', e);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 折叠/展开思考内容
|
||||
function toggleThinking(block) {
|
||||
block.classList.toggle('expanded');
|
||||
|
||||
Reference in New Issue
Block a user