From c181e500bc9cc994883c97ad2c428c430c687bc0 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Wed, 20 May 2026 20:59:04 +0800 Subject: [PATCH 01/46] feat(web): redesign multi-models console Overhauls the Models tab in the Web Console with a vendor-first layout and ships a runtime-accurate dispatcher view for vision and image generation. --- channel/web/chat.html | 113 ++- channel/web/static/css/console.css | 123 +++ channel/web/static/js/console.js | 989 ++++++++++++++++++++++++- channel/web/static/logos/claudeAPI.svg | 1 + channel/web/static/logos/custom.svg | 10 + channel/web/static/logos/dashscope.svg | 1 + channel/web/static/logos/deepseek.svg | 1 + channel/web/static/logos/doubao.svg | 1 + channel/web/static/logos/gemini.svg | 1 + channel/web/static/logos/linkai.svg | 1 + channel/web/static/logos/minimax.svg | 1 + channel/web/static/logos/moonshot.svg | 1 + channel/web/static/logos/openai.svg | 1 + channel/web/static/logos/qianfan.svg | 1 + channel/web/static/logos/zhipu.svg | 1 + channel/web/web_channel.py | 754 ++++++++++++++++++- common/const.py | 2 + 17 files changed, 1995 insertions(+), 7 deletions(-) create mode 100644 channel/web/static/logos/claudeAPI.svg create mode 100644 channel/web/static/logos/custom.svg create mode 100644 channel/web/static/logos/dashscope.svg create mode 100644 channel/web/static/logos/deepseek.svg create mode 100644 channel/web/static/logos/doubao.svg create mode 100644 channel/web/static/logos/gemini.svg create mode 100644 channel/web/static/logos/linkai.svg create mode 100644 channel/web/static/logos/minimax.svg create mode 100644 channel/web/static/logos/moonshot.svg create mode 100644 channel/web/static/logos/openai.svg create mode 100644 channel/web/static/logos/qianfan.svg create mode 100644 channel/web/static/logos/zhipu.svg diff --git a/channel/web/chat.html b/channel/web/chat.html index 56ce808f..31705d66 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -137,6 +137,11 @@ 配置 + + + 模型 + @@ -850,6 +855,41 @@ + + + +
+ + +
+
+
+
+

模型管理

+

统一管理对话、视觉、语音、向量、图像、搜索能力

+
+ +
+
+ Loading... +
+ +
+
+
+ @@ -959,7 +999,7 @@ -
${formatTime(timestamp)} +
`; @@ -1856,11 +1873,12 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) { scrollChatToBottom(); } else if (item.type === 'done') { + // Don't close the stream yet: the backend keeps it open + // for a short tail to deliver async attachments such as + // TTS audio (`voice_attach`). It will close the stream on + // its own via onerror once the tail expires. done = true; - es.close(); - delete activeStreams[requestId]; - // item.content may be empty when "done" is only a stream-close signal after media. const finalText = item.content || accumulatedText; if (!botEl && finalText) { @@ -1874,6 +1892,7 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) { if (copyBtn && finalText) copyBtn.style.display = ''; applyHighlighting(botEl); } + renderBotSpeakerButton(botEl, finalText); scrollChatToBottom(); if (titleInfo) { @@ -1883,6 +1902,15 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) { loadSessionList(); } + } else if (item.type === 'voice_attach') { + // TTS finished — attach a playable audio element to the + // current bot bubble. The stream closes right after. + if (botEl && item.url) { + attachAudioToBotBubble(botEl, item.url, { autoplay: true }); + } + es.close(); + delete activeStreams[requestId]; + } else if (item.type === 'error') { done = true; es.close(); @@ -1896,7 +1924,10 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) { es.close(); delete activeStreams[requestId]; - if (done) return; + if (done) { + // Normal close after the post-done tail expired; nothing to do. + return; + } if (currentReasoningEl) { finalizeThinking(currentReasoningEl, reasoningStartTime, reasoningText); @@ -2187,21 +2218,174 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
${stepsHtml ? `
${stepsHtml}
` : ''}
${renderMarkdown(displayContent)}
+
${formatTime(timestamp)} +
`; el.querySelector('.answer-content').dataset.rawMd = displayContent; + // Existing TTS attachment (history replay): mount the player up-front. + const existingAudio = msg && msg.extras && msg.extras.audio && msg.extras.audio.url; + if (existingAudio) { + attachAudioToBotBubble(el, existingAudio, { autoplay: false }); + } + renderBotSpeakerButton(el, displayContent); applyHighlighting(el); bindChatKnowledgeLinks(el); return el; } +// Append (or replace) a small audio player inside a bot bubble's +// dedicated `.bot-audio-slot`. Used by both live TTS pushes and history +// replay. Silent failures: never throws. +function attachAudioToBotBubble(botEl, audioUrl, opts) { + try { + if (!botEl || !audioUrl) return; + const slot = botEl.querySelector('.bot-audio-slot'); + if (!slot) return; + slot.innerHTML = ''; + slot.style.marginTop = '6px'; + const pill = renderVoicePill(audioUrl, { autoplay: !!(opts && opts.autoplay) }); + slot.appendChild(pill); + const speakBtn = botEl.querySelector('.speak-msg-btn'); + if (speakBtn) speakBtn.style.display = 'none'; + } catch (_) { /* silent */ } +} + +// Build a compact play/pause + progress + duration pill that wraps a +// hidden