feat: 新增语音转文字(ASR)功能
- 后端:新增 /api/audio/transcribe 接口,调用 Ollama whisper 进行语音识别 - 前端:新增 AudioTranscribePage.vue 页面,支持 MP3/WAV/M4A/OGG/FLAC 等格式 - 注册路由、工具卡片、智能助手欢迎语更新 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import http from './http'
|
||||
|
||||
export function quickActions() {
|
||||
return http.get('/ai-chat/quick-actions')
|
||||
}
|
||||
|
||||
export function quickAction(actionId, params) {
|
||||
return http.post('/ai-chat/quick-action', { action_id: actionId, params })
|
||||
}
|
||||
|
||||
// 本人 AI 点数 + 用量(PathCoach 面板数据源)
|
||||
export function getAiMe() {
|
||||
return http.get('/ai/me')
|
||||
}
|
||||
|
||||
// 用量聚合(管理员 = 全量;员工 = 本人,后端按角色过滤)
|
||||
export function getAiUsage(params) {
|
||||
return http.get('/ai/usage', { params })
|
||||
}
|
||||
|
||||
// 管理员:按用户聚合用量 + 剩余点数
|
||||
export function getAiUsageUsers() {
|
||||
return http.get('/ai/usage/users')
|
||||
}
|
||||
|
||||
// 管理员:AI 配置(ai_config.json)读 / 写 / 热重载 / 密钥状态
|
||||
export function getAiConfig() {
|
||||
return http.get('/ai/config')
|
||||
}
|
||||
export function putAiConfig(config) {
|
||||
return http.put('/ai/config', config)
|
||||
}
|
||||
export function reloadAiConfig() {
|
||||
return http.post('/ai/reload')
|
||||
}
|
||||
export function getSecretsStatus() {
|
||||
return http.get('/ai/secrets-status')
|
||||
}
|
||||
|
||||
// SSE 流式对话:后端 text/event-stream,每行 "data: {json}\n\n"
|
||||
export async function streamChat(message, context, history, handlers) {
|
||||
const token = localStorage.getItem('token')
|
||||
const onChunk = typeof handlers === 'function' ? handlers : handlers?.onChunk
|
||||
const onMeta = typeof handlers === 'function' ? null : handlers?.onMeta
|
||||
const onDone = typeof handlers === 'function' ? null : handlers?.onDone
|
||||
const traceId = `chat-${Date.now()}`
|
||||
// #region debug-point A:frontend-request
|
||||
fetch('http://127.0.0.1:7777/event', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: 'knowledge-chat-401',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'A',
|
||||
location: 'frontend/src/api/ai.js:streamChat:request',
|
||||
traceId,
|
||||
msg: '[DEBUG] streamChat request',
|
||||
data: {
|
||||
hasToken: Boolean(token),
|
||||
url: '/api/ai-chat/message',
|
||||
messagePreview: String(message || '').slice(0, 80),
|
||||
spaceKey: context?.knowledge_space_key || '',
|
||||
},
|
||||
ts: Date.now(),
|
||||
}),
|
||||
}).catch(() => {})
|
||||
// #endregion
|
||||
const resp = await fetch('/api/ai-chat/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ message, context, history }),
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
let msg = 'LLM 请求失败'
|
||||
let raw = ''
|
||||
try {
|
||||
raw = await resp.text()
|
||||
const j = JSON.parse(raw)
|
||||
msg = j.message || j.error || msg
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
// #region debug-point A:frontend-non200
|
||||
fetch('http://127.0.0.1:7777/event', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
sessionId: 'knowledge-chat-401',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'A',
|
||||
location: 'frontend/src/api/ai.js:streamChat:non200',
|
||||
traceId,
|
||||
msg: '[DEBUG] streamChat non-200 response',
|
||||
data: {
|
||||
status: resp.status,
|
||||
statusText: resp.statusText,
|
||||
bodyPreview: String(raw || '').slice(0, 240),
|
||||
},
|
||||
ts: Date.now(),
|
||||
}),
|
||||
}).catch(() => {})
|
||||
// #endregion
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
let idx
|
||||
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
||||
const raw = buffer.slice(0, idx).trim()
|
||||
buffer = buffer.slice(idx + 2)
|
||||
if (!raw.startsWith('data:')) continue
|
||||
const payload = raw.slice(5).trim()
|
||||
if (!payload) continue
|
||||
let obj
|
||||
try {
|
||||
obj = JSON.parse(payload)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (obj.type === 'text') {
|
||||
onChunk?.(obj.content)
|
||||
} else if (obj.type === 'meta') {
|
||||
onMeta?.(obj)
|
||||
} else if (obj.type === 'error') {
|
||||
throw new Error(obj.message || 'LLM 请求失败')
|
||||
} else if (obj.type === 'done') {
|
||||
onDone?.()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user