代码拉取完成,页面将自动刷新
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reactor流式问答-背压会话记忆案例</title>
<!-- 引入marked.js库 -->
<script src="js/marked.min.js"></script>
<!-- 引入MathJax库用于公式渲染 -->
<script src="https://polyfill-fastly.io/v3/polyfill.js"></script>
<script id="MathJax-script" async
src="js/tex-mml-chtml.js"></script>
<link rel="stylesheet" href="css/chat.css">
</head>
<body>
<div id="chat-container"></div>
<!-- 新增图片上传区域 -->
<!-- 优化后的图片上传区域 -->
<!-- 修改图片上传区域 -->
<div id="upload-container" style="display: none;">
<div class="upload-controls">
<label for="file-upload" class="upload-label">选择图片</label>
<input id="file-upload" type="file" accept="image/*" multiple /> <!-- 添加multiple属性 -->
<button id="clear-images-btn" class="clear-btn">清除所有图片</button>
</div>
<div class="image-preview-container" id="image-preview-container">
<!-- 多图片预览将在这里动态添加 -->
</div>
<div id="image-input-container" class="image-url-container">
<input type="text" id="image-url-input" placeholder="请输入图片地址..." />
</div>
</div>
<!-- 在 #input-container 中添加音频输入容器 -->
<div id="audio-input-container" style="display: none; flex: 1; align-items: center; gap: 10px;">
<button id="record-btn">开始录音</button>
<span id="recording-status" style="display: none; color: red;">录音中...</span>
<input type="file" id="audio-file-input" accept="audio/*" style="display: none;" />
<label for="audio-file-input" class="upload-audio-label">选择音频文件</label>
<audio id="audio-preview" controls style="display: none; max-width: 200px;"></audio>
<button id="clear-audio-btn" style="display: none;">清除音频</button>
</div>
<div id="input-container">
<div id="model-selection">
<label for="model-select">选择模型:</label>
<select id="model-select">
<option type="LLM" value="deepseek" selected>DeepSeek-LLM</option>
<option type="LLM" value="qwenvlplus">通义千问-LLM</option>
<option type="LLM" value="jiutian">九天-LLM</option>
<option type="LLM" value="kimi">kimi-LLM</option>
<option type="LLM" value="zhipu">智谱-LLM</option>
<option type="LLM" value="minimax">minimax-LLM(MiniMax-M2.7)</option>
<option type="LLM" value="hunyuan">混元-LLM(hunyuan-2.0-thinking-20251109)</option>
<option type="VL" value="jiutian">LLMImage2Text(九天图片识别)</option>
<option type="VL" value="qwenvlplus">QwenVL Plus(阿里百炼图片识别)</option>
<option type="VL" value="volcengine">doubao-seed-1.8(豆包图片识别)</option>
<option type="VL" value="guiji">QwenVL32B(硅基流动图片识别)</option>
<option type="VL" value="kimi">kimi(Kimi图片识别)</option>
<option type="VL" value="zhipu">智谱(智谱图片识别)</option>
<option type="VL" value="hunyuan">hunyuan-vision(腾讯混元图片识别)</option>
<option type="IMAGE" value="jiutian">cntxt2image(九天图片生成)</option>
<option type="IMAGE" value="qwenvlplus">qwen-image-plus(百炼图片生成)</option>
<option type="IMAGE" value="volcengine">doubao-seedream-4.5(豆包火山引擎图片生成)</option>
<option type="IMAGE" value="zhipu">智谱(智谱图片生成)</option>
<option type="IMAGE2IMG" value="jiutian">cntxt2image(九天文图生图)</option>
<option type="IMAGE2IMG" value="qwenvlplus">qwen-image-plus(百炼文图生图)</option>
<option type="IMAGE2IMG" value="volcengine">doubao-seedream-4.5(豆包火山引擎文图生图)</option>
<option type="TTS" value="qwenvlplus">qwen3-tts-flash(百炼语音生成)</option>
<option type="TTS" stream="true" value="qwenvlplus">qwen3-tts-flash(百炼语音生成-流式模式)</option>
<option type="TTS" value="zhipu">智谱(智谱语音生成)</option>
<option type="TTS" stream="true" value="zhipu">智谱(智谱语音生成-流式模式)</option>
<option type="AUDIO" value="qwenvlplus">qwen3-asr-flash(百炼语音识别)</option>
<option type="AUDIO" value="zhipu">glm-asr(智谱语音识别)</option>
<option type="VIDEO" value="qwenvlplus">wan2.5-t2v-preview(百炼视频生成)</option>
<option type="VIDEO" value="volcengine">doubao-seedance-1-5-pro-251215(豆包视频生成)</option>
<option type="VIDEO" value="zhipu">cogvideox-3(智谱视频生成)</option>
</select>
</div>
<!-- 替换原有的<input type="text" id="question-input" ...> -->
<div id="question-input-container">
<input type="text" id="question-input" placeholder="请输入你的问题..." />
</div>
<div id="options-container">
<input type="checkbox" id="reset-checkbox">
<label for="reset-checkbox">重置会话</label>
<input type="checkbox" id="deep-think-checkbox">
<label for="deep-think-checkbox">深度思考</label>
<!-- 新增stream控制参数 -->
<input type="checkbox" id="stream-checkbox" checked>
<label for="stream-checkbox">启用流式输出</label>
<!-- 新增多图生成控制参数 -->
<input type="checkbox" id="multi-image-checkbox">
<label for="multi-image-checkbox">生成多图</label>
</div>
<button id="send-btn">发送</button>
<button id="cancel-btn">取消</button>
</div>
<!-- 视频任务查询区域 -->
<div id="video-task-container" style="display: none; margin-top: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 5px;">
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<input type="text" id="task-id-input" placeholder="请输入视频生成任务ID..." style="flex: 1; padding: 5px;" />
<button id="query-task-btn">查询任务状态</button>
</div>
<div id="task-result-container" style="margin-top: 10px;"></div>
</div>
<script>
class ReactiveChatClient {
constructor() {
this.eventSource = null;
this.abortController = null;
this.isStreaming = false;
this.audioQueue = [];
this.isPlaying = false;
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.chatContainer = document.getElementById('chat-container');
this.questionInput = document.getElementById('question-input');
this.resetCheckbox = document.getElementById('reset-checkbox');
this.deepThinkCheckbox = document.getElementById('deep-think-checkbox');
this.streamCheckbox = document.getElementById('stream-checkbox');
this.sendBtn = document.getElementById('send-btn');
this.cancelBtn = document.getElementById('cancel-btn');
this.modelSelect = document.getElementById('model-select');
let selectedOption = this.modelSelect.options[this.modelSelect.selectedIndex];
this.modelType = selectedOption.getAttribute('type');
this.modelTypeStream = selectedOption.getAttribute('stream');
this.uploadContainer = document.getElementById('upload-container');
// this.imageInputContainer = document.getElementById('image-input-container');
this.imageUrlInput = document.getElementById('image-url-input');
// 新增图片相关属性
// 修改图片相关属性
this.fileUpload = document.getElementById('file-upload');
this.imagePreviewContainer = document.getElementById('image-preview-container'); // 修改为容器
this.clearImagesBtn = document.getElementById('clear-images-btn'); // 修改为清除所有图片
// 添加用于存储多张图片Base64数据的属性
this.imagesBase64Data = []; // 修改为数组
// 在 constructor 中添加新属性
this.audioInputContainer = document.getElementById('audio-input-container');
this.recordBtn = document.getElementById('record-btn');
this.recordingStatus = document.getElementById('recording-status');
this.audioFileInput = document.getElementById('audio-file-input');
this.audioPreview = document.getElementById('audio-preview');
this.clearAudioBtn = document.getElementById('clear-audio-btn');
// 添加音频相关变量
this.mediaRecorder = null;
this.audioChunks = [];
this.audioBlob = null;
// 在 constructor 中添加新属性
this.videoTaskContainer = document.getElementById('video-task-container');
this.taskIdInput = document.getElementById('task-id-input');
this.queryTaskBtn = document.getElementById('query-task-btn');
this.taskResultContainer = document.getElementById('task-result-container');
// 在 constructor 中添加多图控制参数属性
this.multiImageCheckbox = document.getElementById('multi-image-checkbox');
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
// 在 CONTENT 和 REASONING_CONTENT 后面添加
this.AUDIO_CONTENT = 2;
this.CONTENT = 0;
this.REASONING_CONTENT = 1;
// 配置marked选项
marked.setOptions({
breaks: true,
gfm: true
});
this.initEventListeners();
}
initEventListeners() {
this.sendBtn.addEventListener('click', () => this.sendMessage());
this.cancelBtn.addEventListener('click', () => this.cancelStream());
this.questionInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// 在initEventListeners方法中添加
this.modelSelect.addEventListener('change', () => this.toggleImageInput());
// 新增图片上传相关事件监听
// 修改图片上传相关事件监听
this.fileUpload.addEventListener('change', (e) => this.handleImagesUpload(e));
this.clearImagesBtn.addEventListener('click', () => this.clearAllImages());
// 添加音频相关事件监听
this.modelSelect.addEventListener('change', () => this.toggleAudioInput());
this.recordBtn.addEventListener('click', () => this.toggleRecording());
this.audioFileInput.addEventListener('change', (e) => this.handleAudioFileUpload(e));
this.clearAudioBtn.addEventListener('click', () => this.clearAudio());
// 在 initEventListeners 方法中添加事件监听
this.modelSelect.addEventListener('change', () => this.toggleVideoInput());
this.queryTaskBtn.addEventListener('click', () => this.queryVideoTask());
}
toggleAudioInput() {
this.modelType = this.modelSelect.options[this.modelSelect.selectedIndex].getAttribute('type');
this.modelTypeStream = this.modelSelect.options[this.modelSelect.selectedIndex].getAttribute('stream');
if (this.modelType === 'VL' || this.modelType === 'IMAGE2IMG' || this.modelType === 'IMAGE') {
this.uploadContainer.style.display = 'block';
this.audioInputContainer.style.display = 'none';
// 控制多图生成参数的显示
if (this.modelType === 'IMAGE2IMG' || this.modelType === 'IMAGE') {
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'inline';
this.multiImageCheckbox.style.display = 'inline';
} else {
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
}
// 设置问题输入框固定宽度
this.questionInput.style.flex = 'unset';
this.questionInput.style.width = '200px';
}
else if (this.modelType === 'AUDIO') {
this.uploadContainer.style.display = 'none';
this.audioInputContainer.style.display = 'flex';
// 隐藏多图生成控制参数
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
// 设置问题输入框固定宽度
this.questionInput.style.flex = 'unset';
this.questionInput.style.width = '200px';
} else {
this.uploadContainer.style.display = 'none';
this.audioInputContainer.style.display = 'none';
// 隐藏多图生成控制参数
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
// 恢复问题输入框弹性宽度
this.questionInput.style.flex = '1';
this.questionInput.style.width = '';
// 清空图片地址
this.imageUrlInput.value = '';
}
}
// 在 toggleAudioInput 方法后添加
toggleVideoInput() {
this.modelType = this.modelSelect.options[this.modelSelect.selectedIndex].getAttribute('type');
this.modelTypeStream = this.modelSelect.options[this.modelSelect.selectedIndex].getAttribute('stream');
// 隐藏所有特殊输入区域
this.uploadContainer.style.display = 'none';
this.audioInputContainer.style.display = 'none';
this.videoTaskContainer.style.display = 'none';
// 根据模型类型显示对应区域
if (this.modelType === 'VL'|| this.modelType === 'IMAGE2IMG' || this.modelType === 'IMAGE') {
this.uploadContainer.style.display = 'block';
this.questionInput.style.flex = 'unset';
this.questionInput.style.width = '200px';
// 控制多图生成参数的显示
if (this.modelType === 'IMAGE2IMG' || this.modelType === 'IMAGE') {
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'inline';
this.multiImageCheckbox.style.display = 'inline';
} else {
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
}
} else if (this.modelType === 'AUDIO') {
this.audioInputContainer.style.display = 'flex';
this.questionInput.style.flex = 'unset';
this.questionInput.style.width = '200px';
// 隐藏多图生成控制参数
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
} else if (this.modelType === 'VIDEO') {
this.uploadContainer.style.display = 'block';
this.videoTaskContainer.style.display = 'block';
this.questionInput.style.flex = 'unset';
this.questionInput.style.width = '200px';
// 隐藏多图生成控制参数
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
} else {
this.questionInput.style.flex = '1';
this.questionInput.style.width = '';
this.imageUrlInput.value = '';
// 隐藏多图生成控制参数
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
}
}
// 添加视频任务查询方法
queryVideoTask() {
const taskId = this.taskIdInput.value.trim();
if (!taskId) {
alert('请输入任务ID');
return;
}
const url = 'reactor/getVideoTaskResult.api';
const requestBody = { taskId: taskId ,selectedModel: this.modelSelect.value};
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
this.displayTaskResult(data);
})
.catch(error => {
console.error('查询任务失败:', error);
this.displayErrorMessage('查询任务失败: ' + error.message);
});
}
// 显示任务查询结果
displayTaskResult(data) {
this.taskResultContainer.innerHTML = '';
if (data.error) {
this.taskResultContainer.innerHTML = `<div style="color: red;">错误: ${data.error}</div>`;
return;
}
const status = data.taskStatus ? data.taskStatus : '未知';
const resultDiv = document.createElement('div');
resultDiv.className = 'task-result';
let content = `<div><strong>任务ID:</strong> ${data.taskId || 'N/A'}</div>`;
content += `<div><strong>任务状态:</strong> ${status}</div>`;
content += `<div><strong>任务提交时间:</strong> ${data.submitTime}</div>`;
content += `<div><strong>任务执行时间:</strong> ${data.scheduledTime}</div>`;
content += `<div><strong>任务结束时间:</strong> ${data.endTime}</div>`;
content += `<div><strong>origPrompt:</strong> ${data.origPrompt}</div>`;
if (data.videoUrl ) {
content += `<div><strong>视频地址:</strong> ${data.videoUrl}</div>`;
content += ` <div style="margin-top: 10px;">
<strong>生成的视频:</strong><br/>
<video controls style="max-width: 100%; height: auto; margin-top: 5px;">
<source src="${data.videoUrl}" type="video/mp4">
您的浏览器不支持视频播放。
</video>
<div style="margin-top: 5px;">
<a href="${data.videoUrl}" target="_blank" download>下载视频</a>
</div>
</div>
`;
}
resultDiv.innerHTML = content;
this.taskResultContainer.appendChild(resultDiv);
}
toggleRecording() {
if (!this.mediaRecorder) {
// 开始录音
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
this.mediaRecorder = new MediaRecorder(stream);
this.audioChunks = [];
this.mediaRecorder.ondataavailable = event => {
this.audioChunks.push(event.data);
};
this.mediaRecorder.onstop = () => {
this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(this.audioBlob);
this.audioPreview.src = audioUrl;
this.audioPreview.style.display = 'block';
this.clearAudioBtn.style.display = 'inline-block';
// 停止所有音轨
stream.getTracks().forEach(track => track.stop());
};
this.mediaRecorder.start();
this.recordBtn.textContent = '停止录音';
this.recordBtn.classList.add('recording');
this.recordingStatus.style.display = 'inline';
})
.catch(err => {
console.error('录音功能不可用:', err);
alert('无法访问麦克风,请检查权限设置');
});
} else {
// 停止录音
this.mediaRecorder.stop();
this.mediaRecorder = null;
this.recordBtn.textContent = '开始录音';
this.recordBtn.classList.remove('recording');
this.recordingStatus.style.display = 'none';
}
}
handleAudioFileUpload(event) {
const file = event.target.files[0];
if (file && file.type.startsWith('audio/')) {
this.audioBlob = file;
const audioUrl = URL.createObjectURL(file);
this.audioPreview.src = audioUrl;
this.audioPreview.style.display = 'block';
this.clearAudioBtn.style.display = 'inline-block';
} else {
this.clearAudio();
}
}
clearAudio() {
this.audioFileInput.value = '';
this.audioPreview.style.display = 'none';
this.audioPreview.src = '';
this.clearAudioBtn.style.display = 'none';
this.audioBlob = null;
this.audioChunks = [];
if (this.mediaRecorder) {
this.mediaRecorder.stop();
this.mediaRecorder = null;
}
this.recordBtn.textContent = '开始录音';
this.recordBtn.classList.remove('recording');
this.recordingStatus.style.display = 'none';
}
handleImagesUpload(event) {
const files = Array.from(event.target.files);
this.imagesBase64Data = []; // 清空之前的图片数据
// 清空预览容器
this.imagePreviewContainer.innerHTML = '';
files.forEach((file, index) => {
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
// 保存图片的Base64数据
this.imagesBase64Data.push(e.target.result);
// 创建预览元素
const previewWrapper = document.createElement('div');
previewWrapper.className = 'image-preview-wrapper';
const img = document.createElement('img');
img.src = e.target.result;
img.alt = `预览图片 ${index + 1}`;
img.className = 'image-preview-item';
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.className = 'remove-image-btn';
removeBtn.onclick = () => this.removeImage(index, previewWrapper);
previewWrapper.appendChild(img);
previewWrapper.appendChild(removeBtn);
this.imagePreviewContainer.appendChild(previewWrapper);
};
reader.readAsDataURL(file);
}
});
}
removeImage(index, element) {
// 从数组中移除对应图片数据
this.imagesBase64Data.splice(index, 1);
// 从DOM中移除预览元素
element.remove();
}
clearAllImages() {
this.fileUpload.value = '';
this.imagePreviewContainer.innerHTML = '';
this.imagesBase64Data = [];
}
toggleImageInput() {
this.modelType = this.modelSelect.options[this.modelSelect.selectedIndex].getAttribute('type');
this.modelTypeStream = this.modelSelect.options[this.modelSelect.selectedIndex].getAttribute('stream');
if (this.modelType === 'VL' || this.modelType === 'IMAGE2IMG' || this.modelType === 'IMAGE') {
this.uploadContainer.style.display = 'block';
// 控制多图生成参数的显示
if (this.modelType === 'IMAGE2IMG' || this.modelType === 'IMAGE') {
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'inline';
this.multiImageCheckbox.style.display = 'inline';
} else {
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
}
// 设置问题输入框固定宽度
this.questionInput.style.flex = 'unset';
this.questionInput.style.width = '200px';
} else {
this.uploadContainer.style.display = 'none';
// 恢复问题输入框弹性宽度
this.questionInput.style.flex = '1';
this.questionInput.style.width = '';
// 清空图片地址
this.imageUrlInput.value = '';
// 隐藏多图生成控制参数
document.querySelector('label[for="multi-image-checkbox"]').style.display = 'none';
this.multiImageCheckbox.style.display = 'none';
}
}
sendMessage() {
const message = this.questionInput.value.trim();
// if (!message || this.isStreaming) return;
// if ((!message && this.modelType !== 'AUDIO') || this.isStreaming) return;
// 添加LLM模型的特殊验证
if (this.modelType === 'LLM' && !message) {
this.displayErrorMessage('请输入问题内容');
return;
}
// 添加音频模型的特殊验证
else if (this.modelType === 'AUDIO' && !this.audioBlob) {
this.displayErrorMessage('请选择音频文件或录制音频');
return;
}
// 添加VL模型的特殊验证
else if (this.modelType === 'VL') {
const hasImageUrl = this.imageUrlInput.value.trim();
const hasImageUpload = this.imagesBase64Data.length > 0;
// if (!hasImageUrl && !hasImageUpload) {
// this.displayErrorMessage('请选择图片或输入图片链接');
// return;
// }
}
// 在 sendMessage 方法的验证部分更新
// 添加IMAGE2IMG模型的特殊验证
else if (this.modelType === 'IMAGE2IMG') {
const hasImageUrl = this.imageUrlInput.value.trim();
const hasImageUpload = this.imagesBase64Data.length > 0;
const hasMessage = message.trim(); // 文图生图需要输入描述
if (!hasMessage) {
this.displayErrorMessage('请输入图片修改描述');
return;
}
}
const reset = this.resetCheckbox.checked;
const deepThink = this.deepThinkCheckbox.checked;
// 获取stream参数
const enableStream = this.streamCheckbox.checked;
// 显示用户消息
// this.displayMessage(message, 'user');
// this.displayUserMessage(message);
// 显示用户消息
if (this.modelType === 'AUDIO') {
this.displayUserAudioMessage(message);
} else {
this.displayUserMessage(message);
}
this.questionInput.value = '';
// 显示机器人正在输入指示器
const indicatorId = this.showTypingIndicator();
// 修改为POST请求URL
//Deepseek服务地址
const deepseekurl = `reactor/deepseekBackuppressSession.api`;
//千问视觉模型服务地址
const vlurl = `reactor/qwenvl.api`;
//千问图片生成服务地址
const imageurl = `reactor/genImage.api`;
// 在 sendMessage 方法中的 URL 定义部分添加
const image2imageUrl = `reactor/genImageFromImage.api`;
// 音频识别服务地址
const audiourl = `reactor/audioFileRecognition.api`;
// 千问语音生成服务地址
const ttsurl = `reactor/genAudioByqwentts.api`;
// 千问实时播放语音生成服务地址
const streamttsurl = `reactor/streamGenAudioByqwentts.api`;
let url;
switch(this.modelType) {
case 'LLM':
url = deepseekurl;
break;
case 'VL':
url = vlurl;
break;
case 'IMAGE':
url = imageurl;
break;
case 'IMAGE2IMG':
url = image2imageUrl;
break;
case 'AUDIO':
url = audiourl;
break;
case 'TTS':
if(this.modelTypeStream === 'true'){
url = streamttsurl;
}
else{
url = ttsurl;
}
break;
case 'VIDEO':
url = `reactor/submitVideoByqwenwan.api`;
break;
default:
url = deepseekurl;
}
const selectedModel = this.modelSelect.value;
// 创建AbortController用于取消请求
this.abortController = new AbortController();
this.isStreaming = true;
// 构建请求体
const requestBody = {
message: message,
reset: reset,
selectedModel: selectedModel,
deepThink: deepThink,
// 添加stream参数
enableStream: enableStream
};
// 仅对IMAGE2IMG模型添加多图参数
if (this.modelType === 'IMAGE' || this.modelType === 'IMAGE2IMG') {
requestBody.generateMultipleImages = this.multiImageCheckbox.checked;
}
// 根据模型类型添加相应的图片参数
if (this.modelType === 'VL' || this.modelType === 'IMAGE2IMG' || this.modelType === 'VIDEO') {
// 如果是URL模式且有输入URL,则使用URL
if (this.imageUrlInput.value.trim()) {
requestBody.imageUrl = this.imageUrlInput.value.trim();
}
// 如果是上传图片模式且有上传图片,则使用base64数据
else if (this.imagesBase64Data.length > 0) {
requestBody.imagesBase64 = this.imagesBase64Data; // 发送所有图片
}
}
this.clearAllImages();
let fetchParams = {};
if (this.modelType === 'AUDIO' && this.audioBlob) {
const formData = new FormData();
formData.append('audio', this.audioBlob);
formData.append('reset', reset);
formData.append('message', message);
formData.append('selectedModel', selectedModel);
formData.append('enableStream', enableStream);
fetchParams = {
method: 'POST',
body: formData,
signal: this.abortController.signal
}
}
else{
fetchParams = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// 在sendMessage方法中,修改fetch请求的body部分
body: JSON.stringify(requestBody),
signal: this.abortController.signal
};
}
// 使用fetch API进行POST流式请求,包含reset参数
fetch(url, fetchParams)
.then(response => {
// 移除正在输入指示器
this.removeTypingIndicator(indicatorId);
if(this.modelType === 'AUDIO'){
this.clearAudio();
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 特殊处理图像生成模型的响应
if (this.modelType === 'IMAGE') {
return response.json().then(data => {
this.isStreaming = false;
if(data && data.code != "200"){
// 处理错误
const errorMessage = data.message;
this.displayErrorMessage(errorMessage);
return;
}
let botMessageElement = this.createBotMessageElement();
// 显示生成的图片
if (data ) {
if( data.imageUrl) {
const img = document.createElement('img');
img.src = data.imageUrl;
img.className = 'generated-image';
img.alt = '生成的图片';
botMessageElement.appendChild(img);
}
else if(data.imageUrls){
for (let i = 0; i < data.imageUrls.length; i++) {
const img = document.createElement('img');
img.src = data.imageUrls[i];
img.className = 'generated-image';
img.alt = '生成的图片';
botMessageElement.appendChild(img);
}
}
} else {
botMessageElement.textContent = '图片生成失败';
}
this.scrollToBottom();
});
}
// 在响应处理部分添加 IMAGE2IMG 处理
else if (this.modelType === 'IMAGE2IMG') {
return response.json().then(data => {
this.isStreaming = false;
if(data && data.code != "200"){
// 处理错误
const errorMessage = data.message;
this.displayErrorMessage(errorMessage);
return;
}
let botMessageElement = this.createBotMessageElement();
// 显示生成的图片
if (data ) {
if( data.imageUrl) {
const img = document.createElement('img');
img.src = data.imageUrl;
img.className = 'generated-image';
img.alt = '根据描述修改后的图片';
botMessageElement.appendChild(img);
}
else if(data.imageUrls){
for (let i = 0; i < data.imageUrls.length; i++) {
const img = document.createElement('img');
img.src = data.imageUrls[i];
img.className = 'generated-image';
img.alt = '生成的图片';
botMessageElement.appendChild(img);
}
}
} else {
botMessageElement.textContent = '图片修改失败:'+data.response;
}
this.scrollToBottom();
});
}
// 特殊处理TTS模型的响应
else if (this.modelType === 'TTS') {
if(!this.modelTypeStream) {
return response.json().then(data => {
if(data && data.code != "200"){
// 处理错误
const errorMessage = data.message;
this.displayErrorMessage(errorMessage);
return;
}
this.isStreaming = false;
let botMessageElement = this.createBotMessageElement();
// 显示生成的音频
if (data && data.audioUrl) {
const audioContainer = document.createElement('div');
const audio = document.createElement('audio');
audio.src = data.audioUrl;
audio.controls = true;
audioContainer.appendChild(audio);
const downloadLink = document.createElement('a');
downloadLink.href = data.audioUrl;
downloadLink.download = 'tts-audio.wav';
downloadLink.textContent = '下载音频';
downloadLink.target = '_blank';
audioContainer.appendChild(downloadLink);
botMessageElement.appendChild(audioContainer);
} else {
botMessageElement.textContent = '语音生成失败';
}
this.scrollToBottom();
});
}
// 在 TTS 模型的响应处理部分,替换 else 分支中的 base64 编码语音流处理
else {
// 获取响应流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let botMessageElement = this.createBotMessageElement();
let accumulatedContent = '';
let lineBuffer = '';
let finishReason = null;
// 处理base64编码语音流片段播放:实时语音播放
// 处理流式返回的base64音频数据
// 替换原来的 processAudioChunk 函数
const processAudioChunk = async (base64Data) => {
try {
// 1. 移除可能的数据前缀
const cleanBase64 = base64Data.replace(/^data:audio\/\w+;base64,/, '');
// 2. 解码 base64
const binaryString = atob(cleanBase64);
const pcmBytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
pcmBytes[i] = binaryString.charCodeAt(i);
}
// 3. 将 PCM 转换为 WAV
const wavBytes = pcmToWav(pcmBytes, {
sampleRate: 16000,
numChannels: 1,
bitDepth: 16, // 16-bit, 8-bit, 24-bit
signed: true,
byteOrder: 'little', // 'little' 或 'big'
encoding: 'PCM_SIGNED'
});
// 4. 创建 WAV Blob
const audioBlob = new Blob([wavBytes], { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
// 将音频添加到播放队列
this.audioQueue.push({
url: audioUrl,
blob: audioBlob
});
// 如果当前没有正在播放的音频,则开始播放队列
if (!this.isPlaying) {
processNextAudio();
}
} catch (error) {
console.error('处理音频数据异常', error);
}
};
// 添加新的音频播放处理函数
const processNextAudio = async () => {
if (this.audioQueue.length === 0) {
this.isPlaying = false;
return;
}
this.isPlaying = true;
const audioItem = this.audioQueue.shift(); // 取出队列中的第一个音频
try {
// 创建音频元素并播放
this.audio = new Audio();
this.audio.src = audioItem.url;
this.audio.oncanplay = () => {
console.log('音频已准备好播放');
};
await this.audio.play();
// 等待当前音频播放完成后再处理下一个
await new Promise((resolve) => {
this.audio.onended = () => {
// 播放完成后释放资源
URL.revokeObjectURL(audioItem.url);
console.log('音频播放完成');
resolve();
};
// 如果播放出错也继续下一个
this.audio.onerror = () => {
URL.revokeObjectURL(audioItem.url);
console.log('音频播放出错,跳转到下一个');
resolve();
};
});
} catch (error) {
console.error('播放音频异常', error);
}
// 处理下一个音频
processNextAudio();
};
// PCM 转 WAV 的核心函数
const pcmToWav = (pcmData, options = {}) => {
/**
* sampleRate: 16000,
* numChannels: 1,
* bitDepth: 16, // 16-bit, 8-bit, 24-bit
* signed: true,
* byteOrder: 'little', // 'little' 或 'big'
* encoding: 'PCM_SIGNED'
* @type {number}
*/
const {
sampleRate = 16000,
numChannels = 1,
bytesPerSample = 2,
byteOrder = 'little' // 'little' 或 'big'
} = options;
const dataSize = pcmData.length;
const headerSize = 44;
const wavData = new DataView(new ArrayBuffer(headerSize + dataSize));
// 设置 WAV 文件头
// RIFF 头
writeString(wavData, 0, 'RIFF');
wavData.setUint32(4, 36 + dataSize, true); // 文件大小
writeString(wavData, 8, 'WAVE');
// fmt 子块
writeString(wavData, 12, 'fmt ');
wavData.setUint32(16, 16, true); // fmt 块大小
wavData.setUint16(20, 1, true); // 音频格式: 1 = PCM
wavData.setUint16(22, numChannels, true);
wavData.setUint32(24, sampleRate, true);
wavData.setUint32(28, sampleRate * numChannels * bytesPerSample, true); // 字节率
wavData.setUint16(32, numChannels * bytesPerSample, true); // 块对齐
wavData.setUint16(34, bytesPerSample * 8, true); // 位深度
// data 子块
writeString(wavData, 36, 'data');
wavData.setUint32(40, dataSize, true);
// 复制 PCM 数据
for (let i = 0; i < dataSize; i++) {
wavData.setUint8(44 + i, pcmData[i]);
}
return wavData;
};
// 辅助函数:写入字符串到 DataView
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
// 修改readStream函数,处理TTS流式数据
// const originalReadStream = readStream;
const newReadStream = () => {
reader.read().then(({ done, value }) => {
if (done) {
// 处理剩余数据
if (lineBuffer.trim()) {
try {
const jsonData = JSON.parse(lineBuffer.trim());
for (let i = 0; i < jsonData.length; i++) {
const line = jsonData[i];
if(line.first && line.extendDatas){
let title = line.extendDatas["title"];
let url = line.extendDatas["url"];
accumulatedContent += `<a href="${url}" target="_blank">${title}</a><br><br>`;
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent);
}
if (line.data) {
// 假设line.data是base64编码的音频片段
processAudioChunk(line.data);
}
if(line.done && line.extendDatas){
let title = line.extendDatas["title"];
let url = line.extendDatas["url"];
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent +`<br><br><a href="${url}" target="_blank">${title}</a>`);
}
if(line.done){
botMessageElement.appendChild(audioContainer);
}
}
} catch (e) {
console.error('解析最终音频数据时出错:', e);
}
}
this.isStreaming = false;
if (reset) {
this.resetCheckbox.checked = false;
}
return;
}
// 解码当前chunk
const chunk = decoder.decode(value, { stream: true });
// 将新数据添加到行缓冲区
if (lineBuffer) {
lineBuffer += chunk;
} else {
lineBuffer = chunk;
}
// 按行分割数据
const lines = lineBuffer.split('\n');
lineBuffer = lines.pop() || '';
// 处理完整的行
lines.forEach(line => {
if (line.trim()) {
try {
const jsonData = JSON.parse(line.trim());
for (let i = 0; i < jsonData.length; i++) {
const lineData = jsonData[i];
if(lineData.first && lineData.extendDatas){
let title = lineData.extendDatas["title"];
let url = lineData.extendDatas["url"];
accumulatedContent += `<a href="${url}" target="_blank">${title}</a><br><br>`;
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent);
}
if (lineData.data) {
// 假设lineData.data是base64编码的音频片段
processAudioChunk(lineData.data);
}
if (!finishReason && lineData.finishReason) {
finishReason = lineData.finishReason;
}
if (this.terminalIntercept(finishReason)) {
accumulatedContent = accumulatedContent + `....对话异常终止,终止原因:` + finishReason;
this.updateBotMessage(botMessageElement, accumulatedContent);
finishReason = null;
}
if(lineData.done && lineData.extendDatas){
let title = lineData.extendDatas["title"];
let url = lineData.extendDatas["url"];
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent +`<br><br><a href="${url}" target="_blank">${title}</a>`);
}
if(lineData.done){
botMessageElement.appendChild(audioContainer);
}
}
} catch (e) {
console.error('解析音频数据时出错:', e);
}
}
});
// 继续读取
newReadStream();
}).catch(error => {
if (error.name !== 'AbortError') {
console.error('Stream reading error:', error);
this.displayErrorMessage('接收音频数据时出错');
}
this.isStreaming = false;
});
};
newReadStream();
return ;
}
}
else if (this.modelType === 'VIDEO') {
return response.json().then(data => {
this.isStreaming = false;
let botMessageElement = this.createBotMessageElement();
if(data.message != null){
this.displayErrorMessage("code:"+data.code +",errormessage:"+ data.message);
return
}
// 显示任务信息
if (data && data.taskId) {
const taskInfo = document.createElement('div');
taskInfo.innerHTML = ` <div><strong>视频生成任务已提交</strong></div>
<div><strong>任务ID:</strong> ${data.taskId}</div>
<div><strong>状态:</strong> ${data.taskStatus ? data.taskStatus: '提交中'}</div>
${data && data.message ? `<div><strong>信息:</strong> ${data.message}</div>` : ''}
`;
botMessageElement.appendChild(taskInfo);
this.taskIdInput.value = data.taskId;
} else {
botMessageElement.textContent = '视频生成任务提交失败';
}
this.scrollToBottom();
});
}
// 获取响应流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let botMessageElement = this.createBotMessageElement();
let accumulatedContent = '';
let lineBuffer = '';
let isDeepseekBegin = false;
let finishReason = null;
// 修改readStream函数实现逐行处理
const readStream = () => {
reader.read().then(({ done, value }) => {
if (done) {
// 处理缓冲区中剩余的数据
if (lineBuffer.trim()) {
try {
const jsonData = JSON.parse(lineBuffer.trim());
for (let i = 0; i < jsonData.length; i++) {
const line = jsonData[i];
if(line.first && line.extendDatas){
let title = line.extendDatas["title"];
let url = line.extendDatas["url"];
accumulatedContent += `<a href="${url}" target="_blank">${title}</a><br><br>`;
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent);
}
//处理思维链输出样式
if(line.contentType == this.REASONING_CONTENT && !isDeepseekBegin) {
accumulatedContent += `<div class="reasoning-container">`;
isDeepseekBegin = true;
}
if(line.contentType == this.CONTENT) {
if(isDeepseekBegin) {
accumulatedContent += `</div>`;
isDeepseekBegin = false;
}
}
if (line.data) {
accumulatedContent += line.data;
this.updateBotMessage(botMessageElement, accumulatedContent);
}
if(!finishReason && line.finishReason){
finishReason = line.finishReason;
}
if(this.terminalIntercept(finishReason)){
//对话异常终止
accumulatedContent = accumulatedContent +`....对话异常终止,终止原因:`+finishReason
this.updateBotMessage(botMessageElement, accumulatedContent );
finishReason = null;
}
if(line.done && line.extendDatas){
let title = line.extendDatas["title"];
let url = line.extendDatas["url"];
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent +`<br><br><a href="${url}" target="_blank">${title}</a>`);
}
}
} catch (e) {
// 如果不是JSON格式,作为普通文本处理
accumulatedContent += lineBuffer;
this.updateBotMessage(botMessageElement, accumulatedContent);
}
}
this.isStreaming = false;
// 发送完成后,如果reset被选中,则取消选中
if (reset) {
this.resetCheckbox.checked = false;
}
return;
}
// 解码当前chunk
const chunk = decoder.decode(value, { stream: true });
// 将新数据添加到行缓冲区
if(lineBuffer) {
lineBuffer += chunk;
}
else{
lineBuffer = chunk;
}
// 按行分割数据
const lines = lineBuffer.split('\n');
// 保留最后一行(可能是不完整的)在缓冲区中
lineBuffer = lines.pop() || '';
// 处理完整的行
lines.forEach(line => {
if (line.trim()) {
try {
const jsonData = JSON.parse(line.trim());
for (let i = 0; i < jsonData.length; i++) {
const lineData = jsonData[i];
if(lineData.first && lineData.extendDatas){
let title = lineData.extendDatas["title"]+"(验证在问题答案前嵌入附加材料信息)";
let url = lineData.extendDatas["url"];
accumulatedContent += `<a href="${url}" target="_blank">${title}</a><br><br>`;
//将链接作为一个超链接追加到答案的开头
this.updateBotMessage(botMessageElement, accumulatedContent);
}
//处理思维链输出样式
if(lineData.contentType == this.REASONING_CONTENT && !isDeepseekBegin) {
accumulatedContent += `<div class="reasoning-container">`;
isDeepseekBegin = true;
}
if(lineData.contentType == this.CONTENT) {
if(isDeepseekBegin) {
accumulatedContent += `</div>`;
isDeepseekBegin = false;
}
}
if (lineData.data) {
accumulatedContent += lineData.data;
this.updateBotMessage(botMessageElement, accumulatedContent);
}
if(!finishReason && lineData.finishReason){
finishReason = lineData.finishReason;
}
if(this.terminalIntercept(finishReason)){
//对话异常终止
accumulatedContent = accumulatedContent +`....对话异常终止,终止原因:`+finishReason
this.updateBotMessage(botMessageElement, accumulatedContent);
finishReason = null;
}
if(lineData.done && lineData.extendDatas){
let title = lineData.extendDatas["title"]+"(验证在问题答案后嵌入附加材料信息)";
let url = lineData.extendDatas["url"];
//将链接作为一个超链接追加到答案的末尾
this.updateBotMessage(botMessageElement, accumulatedContent +`<br><br><a href="${url}" target="_blank">${title}</a>`);
}
}
} catch (e) {
// 如果不是JSON格式,作为普通文本处理
accumulatedContent += line;
this.updateBotMessage(botMessageElement, accumulatedContent);
}
}
});
// 继续读取
readStream();
}).catch(error => {
if (error.name !== 'AbortError') {
console.error('Stream reading error:', error);
this.displayErrorMessage('接收数据时出错');
}
this.isStreaming = false;
});
};
readStream();
})
.catch(error => {
this.removeTypingIndicator(indicatorId);
this.isStreaming = false;
if (error.name === 'AbortError') {
this.displayErrorMessage('请求已取消');
} else {
console.error('Fetch error:', error);
this.displayErrorMessage('发送请求时出错: ' + error.message);
}
});
}
cancelStream() {
if (this.isStreaming && this.abortController) {
this.abortController.abort();
this.isStreaming = false;
this.displayErrorMessage('请求已被用户取消');
}
}
displayUserAudioMessage(message) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message user-message';
const textDiv = document.createElement('div');
textDiv.textContent = message;
messageDiv.appendChild(textDiv);
// 添加音频预览
if (this.audioPreview.style.display !== 'none') {
const audioClone = this.audioPreview.cloneNode(true);
audioClone.controls = true;
audioClone.style.display = 'block';
messageDiv.appendChild(audioClone);
}
this.chatContainer.appendChild(messageDiv);
this.scrollToBottom();
}
// 修改 displayUserMessage 方法以显示多张图片
displayUserMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message user-message';
// 添加文字内容
if (content) {
const textDiv = document.createElement('div');
textDiv.textContent = content;
messageDiv.appendChild(textDiv);
}
// 如果有预览图片,则添加所有图片
if (this.imagesBase64Data.length > 0) {
this.imagesBase64Data.forEach((imgSrc, index) => {
const img = document.createElement('img');
img.src = imgSrc;
img.alt = `用户上传的图片 ${index + 1}`;
messageDiv.appendChild(img);
});
}
this.chatContainer.appendChild(messageDiv);
this.scrollToBottom();
}
createBotMessageElement() {
const messageDiv = document.createElement('div');
messageDiv.className = 'message bot-message';
this.chatContainer.appendChild(messageDiv);
this.scrollToBottom();
return messageDiv;
}
terminalIntercept(finishReason){
if(finishReason && finishReason.toLowerCase() != 'stop' && finishReason != 'null'){
//对话异常终止
return true;
}
return false;
}
updateBotMessage(element, content) {
// 实时更新Markdown内容
element.innerHTML = marked.parse(content);
// 渲染数学公式
this.renderMath(element);
this.scrollToBottom();
}
// renderMath(element) {
// // 更健壮的MathJax渲染实现
// if (window.MathJax && typeof MathJax.typesetPromise !== 'undefined') {
// MathJax.typesetPromise([element]).catch(function (err) {
// console.warn('MathJax渲染警告:', err);
// });
// } else if (window.MathJax) {
// // 兼容旧版本
// MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]);
// }
// }
//
renderMath(element) {
// 使用MathJax渲染数学公式
if (typeof MathJax !== 'undefined') {
MathJax.typesetPromise([element]).catch(function (err) {
console.error('MathJax渲染错误:', err);
});
}
}
showTypingIndicator() {
const indicator = document.createElement('div');
indicator.className = 'message bot-message typing-indicator';
indicator.id = 'typing-' + Date.now();
indicator.textContent = '正在思考中...';
this.chatContainer.appendChild(indicator);
this.scrollToBottom();
return indicator.id;
}
removeTypingIndicator(id) {
const indicator = document.getElementById(id);
if (indicator) {
indicator.remove();
}
}
displayErrorMessage(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'message bot-message';
errorDiv.style.color = 'red';
errorDiv.textContent = message;
this.chatContainer.appendChild(errorDiv);
this.scrollToBottom();
}
scrollToBottom() {
this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
}
}
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
new ReactiveChatClient();
});
</script>
</body>
</html>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。