fix: 修复创建单个es索引报错

This commit is contained in:
wenjinbo 2025-09-22 15:34:40 +08:00
parent 919ab51570
commit e5f8cfb8f9
5 changed files with 344 additions and 52 deletions

View File

@ -64,6 +64,23 @@
</svg>
</el-button>
</div>
<!-- 权限引导提示 -->
<div v-if="showPermissionGuide" class="permission-guide">
<div class="permission-guide-content">
<div class="permission-icon">🎤</div>
<div class="permission-text">
<h4>需要麦克风权限</h4>
<p>请点击地址栏左侧的麦克风图标选择"允许"来启用语音功能</p>
<div class="permission-steps">
<div class="step">1. 点击地址栏的 🔒 🎤 图标</div>
<div class="step">2. 选择"允许"麦克风访问</div>
<div class="step">3. 刷新页面后重试</div>
</div>
</div>
<button class="permission-close" @click="showPermissionGuide = false">×</button>
</div>
</div>
<div class="input-footer">
<span class="footer-hint">{{ t('vabI18n.chat.sendHint') }}</span>
</div>
@ -100,8 +117,13 @@ const inputText = ref(props.modelValue)
//
const isRecording = ref(false)
const isProcessingVoice = ref(false)
const showPermissionGuide = ref(false)
let mediaRecorder: MediaRecorder | null = null
let audioChunks: Blob[] = []
let audioContext: AudioContext | null = null
let mediaStreamSource: MediaStreamAudioSourceNode | null = null
let processor: ScriptProcessorNode | null = null
let recordedBuffer: Float32Array[] = []
//
watch(inputText, (newValue) => {
@ -113,67 +135,219 @@ watch(() => props.modelValue, (newValue) => {
inputText.value = newValue
})
// WAV
const encodeWAV = (samples: Float32Array, sampleRate: number) => {
const buffer = new ArrayBuffer(44 + samples.length * 2)
const view = new DataView(buffer)
// WAV
const writeString = (offset: number, string: string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
const floatTo16BitPCM = (output: DataView, offset: number, input: Float32Array) => {
for (let i = 0; i < input.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, input[i]))
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
}
}
writeString(0, 'RIFF')
view.setUint32(4, 32 + samples.length * 2, true)
writeString(8, 'WAVE')
writeString(12, 'fmt ')
view.setUint32(16, 16, true)
view.setUint16(20, 1, true)
view.setUint16(22, 1, true)
view.setUint32(24, sampleRate, true)
view.setUint32(28, sampleRate * 2, true)
view.setUint16(32, 2, true)
view.setUint16(34, 16, true)
writeString(36, 'data')
view.setUint32(40, samples.length * 2, true)
floatTo16BitPCM(view, 44, samples)
return new Blob([view], { type: 'audio/wav' })
}
//
const mergeBuffers = (buffers: Float32Array[], length: number) => {
const result = new Float32Array(length)
let offset = 0
for (const buffer of buffers) {
result.set(buffer, offset)
offset += buffer.length
}
return result
}
//
const checkMicrophonePermission = async () => {
try {
// API
if (!navigator.permissions) {
console.warn('浏览器不支持权限API')
return 'unknown'
}
const permission = await navigator.permissions.query({ name: 'microphone' as PermissionName })
return permission.state
} catch (error) {
console.warn('无法查询麦克风权限状态:', error)
return 'unknown'
}
}
//
const checkBrowserSupport = () => {
// HTTPSlocalhost
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost'
// API
const hasMediaDevices = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
const hasMediaRecorder = !!window.MediaRecorder
return {
isSecureContext,
hasMediaDevices,
hasMediaRecorder
}
}
//
const startRecording = async () => {
if (isRecording.value || isProcessingVoice.value) return
//
const support = checkBrowserSupport()
if (!support.isSecureContext) {
ElMessage.error('语音功能需要在HTTPS环境下使用请使用HTTPS访问此页面')
return
}
if (!support.hasMediaDevices) {
ElMessage.error('您的浏览器不支持麦克风访问功能')
return
}
if (!support.hasMediaRecorder) {
ElMessage.error('您的浏览器不支持录音功能')
return
}
try {
//
const permissionState = await checkMicrophonePermission()
console.log('麦克风权限状态:', permissionState)
if (permissionState === 'denied') {
ElMessage.error('麦克风权限已被拒绝,请在浏览器设置中允许访问麦克风')
return
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
autoGainControl: true,
sampleRate: 44100
}
})
//
const mimeTypes = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/mp4',
'audio/wav'
]
console.log('麦克风权限获取成功')
let selectedMimeType = 'audio/webm'
for (const mimeType of mimeTypes) {
if (MediaRecorder.isTypeSupported(mimeType)) {
selectedMimeType = mimeType
break
}
// 使Web Audio APIWAV
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
mediaStreamSource = audioContext.createMediaStreamSource(stream)
//
processor = audioContext.createScriptProcessor(4096, 1, 1)
recordedBuffer = []
processor.onaudioprocess = (e) => {
const inputBuffer = e.inputBuffer.getChannelData(0)
const buffer = new Float32Array(inputBuffer.length)
buffer.set(inputBuffer)
recordedBuffer.push(buffer)
}
mediaRecorder = new MediaRecorder(stream, { mimeType: selectedMimeType })
audioChunks = []
//
mediaStreamSource.connect(processor)
processor.connect(audioContext.destination)
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data)
}
}
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: selectedMimeType })
await processVoiceToText(audioBlob)
//
stream.getTracks().forEach(track => track.stop())
}
mediaRecorder.start()
console.log('开始录制WAV格式音频采样率:', audioContext.sampleRate)
isRecording.value = true
// stream
;(processor as any).stream = stream
} catch (error) {
console.error('获取麦克风权限失败:', error)
ElMessage.error('无法访问麦克风,请检查权限设置')
//
if (error.name === 'NotAllowedError') {
showPermissionGuide.value = true
ElMessage.error('麦克风权限被拒绝,请查看权限引导')
} else if (error.name === 'NotFoundError') {
ElMessage.error('未找到麦克风设备,请检查设备连接')
} else if (error.name === 'NotReadableError') {
ElMessage.error('麦克风被其他应用占用,请关闭其他使用麦克风的程序')
} else if (error.name === 'OverconstrainedError') {
ElMessage.error('麦克风不支持当前配置,请尝试使用其他设备')
} else if (error.name === 'SecurityError') {
ElMessage.error('安全限制请确保在HTTPS环境下使用语音功能')
} else {
//
showPermissionGuide.value = true
ElMessage.error(`无法访问麦克风: ${error.message || '未知错误'}`)
}
}
}
//
const stopRecording = () => {
if (!isRecording.value || !mediaRecorder) return
const stopRecording = async () => {
if (!isRecording.value) return
isRecording.value = false
mediaRecorder.stop()
if (processor && audioContext && mediaStreamSource) {
//
mediaStreamSource.disconnect()
processor.disconnect()
//
const stream = (processor as any).stream
if (stream) {
stream.getTracks().forEach((track: MediaStreamTrack) => track.stop())
}
//
if (recordedBuffer.length > 0) {
const totalLength = recordedBuffer.reduce((acc, buffer) => acc + buffer.length, 0)
const mergedBuffer = mergeBuffers(recordedBuffer, totalLength)
// WAV
const wavBlob = encodeWAV(mergedBuffer, audioContext.sampleRate)
console.log('生成WAV音频文件大小:', wavBlob.size, 'bytes')
await processVoiceToText(wavBlob)
} else {
ElMessage.warning('录音时间过短,请重新录制')
}
//
if (audioContext.state !== 'closed') {
await audioContext.close()
}
audioContext = null
mediaStreamSource = null
processor = null
recordedBuffer = []
}
}
//
@ -188,11 +362,10 @@ const processVoiceToText = async (audioBlob: Blob) => {
try {
const formData = new FormData()
//
const fileExtension = audioBlob.type.includes('wav') ? 'wav' :
audioBlob.type.includes('mp4') ? 'mp3' : 'webm'
// WAV
formData.append('file', audioBlob, 'recording.wav')
formData.append('file', audioBlob, `recording.${fileExtension}`)
console.log('上传音频文件:', audioBlob.type, '大小:', audioBlob.size)
const response = await voiceToText(formData)
@ -220,10 +393,21 @@ const processVoiceToText = async (audioBlob: Blob) => {
}
//
onUnmounted(() => {
onUnmounted(async () => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop()
}
// Web Audio API
if (processor) {
processor.disconnect()
}
if (mediaStreamSource) {
mediaStreamSource.disconnect()
}
if (audioContext && audioContext.state !== 'closed') {
await audioContext.close()
}
})
</script>
@ -542,6 +726,118 @@ onUnmounted(() => {
animation: spin 0.8s linear infinite;
}
/* 权限引导提示样式 */
.permission-guide {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease-out;
}
.permission-guide-content {
background: #ffffff;
border-radius: 16px;
padding: 2rem;
max-width: 480px;
margin: 1rem;
position: relative;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: slideUp 0.3s ease-out;
}
.permission-icon {
font-size: 3rem;
text-align: center;
margin-bottom: 1rem;
}
.permission-text h4 {
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.5rem 0;
text-align: center;
}
.permission-text p {
color: #6b7280;
margin: 0 0 1.5rem 0;
text-align: center;
line-height: 1.5;
}
.permission-steps {
background: #f9fafb;
border-radius: 8px;
padding: 1rem;
border-left: 4px solid #4f46e5;
}
.step {
color: #374151;
margin: 0.5rem 0;
font-size: 0.9rem;
line-height: 1.4;
}
.step:first-child {
margin-top: 0;
}
.step:last-child {
margin-bottom: 0;
}
.permission-close {
position: absolute;
top: 1rem;
right: 1rem;
width: 2rem;
height: 2rem;
border: none;
background: #f3f4f6;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #6b7280;
transition: all 0.2s ease;
}
.permission-close:hover {
background: #e5e7eb;
color: #374151;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.input-footer {
display: none;
}

View File

@ -104,9 +104,9 @@
<el-form :model="uploadForm.docAnalysisStrategy" label-width="140px" class="config-form">
<div class="form-section">
<h4 class="section-title">基础设置</h4>
<!-- <el-form-item label="策略名称">
<el-input v-model="uploadForm.docAnalysisStrategy.name" placeholder="请输入策略名称" />
</el-form-item> -->
<el-form-item label="策略名称">
<el-input v-model="uploadForm.docAnalysisStrategy.name" placeholder="请输入策略名称(可选)" />
</el-form-item>
<el-form-item label="分段模式">
<el-select v-model="uploadForm.docAnalysisStrategy.segmentationMode" placeholder="选择分段模式">
<el-option label="普通分段" value="normal" />
@ -553,9 +553,8 @@ const canProceedToNext = () => {
}
const canUpload = () => {
if (uploadForm.analysisStrategyType === 'custom') {
return uploadForm.docAnalysisStrategy.name !== ''
}
// UI
//
return true
}
@ -641,7 +640,7 @@ const handleUpload = async () => {
//
if (uploadForm.analysisStrategyType === 'custom') {
requestData.docAnalysisStrategy = {
name: uploadForm.docAnalysisStrategy.name,
name: uploadForm.docAnalysisStrategy.name || '自定义策略',
segmentationMode: 'custom',
indexingTechnique: uploadForm.docAnalysisStrategy.indexingTechnique,
docForm: 'text_model',

View File

@ -153,7 +153,7 @@ public class MetadataServiceImpl implements MetadataService {
@Override
public List<DifyMetadata> handleYsylcMetadata(String datasetId, String documentId, MultipartFile file,String metadataId) {
// 异步执行
// 异步执行工作流
CompletableFuture.runAsync(() -> {
String apiKey = tApiKeyService.getApiKeyFromCache(DifyConstants.API_KEY_YSYLC_DATA_PROC);
workFlowService.runWorkflowByFile(documentId, apiKey, file);

View File

@ -35,7 +35,7 @@ public class EsTDatasetFilesImporter {
if (datasetFiles == null) {
throw new IllegalArgumentException("datasetFiles 不存在");
}
String filePath = datasetFiles.getSourceUrl();
String filePath = datasetFiles.getDifyStoragePath();
File file = new File(filePath);
if (!file.exists()) {
throw new IllegalArgumentException(filePath + " 不存在");

View File

@ -81,6 +81,3 @@ elasticsearch:
#是否删除索引,重新构建索引
deleteIndex: ${es-deleteIndex:false}
voice2text:
url: ${voice2text-url:http://192.168.8.253:11023/v1/audio/transcriptions}
model: ${voice2text-model:whisper-1}