feat: 新增本月经营态势弹窗+千问TTS语音播报
- 新增 BusinessReportDialog 组件,展示整体经营/成本费用/风险态势/利润机会四部分文字报告 - BossPage 标题栏新增「本月经营态势」按钮 - 后端新增 /api/tts 路由,调用千问 CosyVoice TTS API 合成语音 - 前端按句分段合成、逐句顺序播放,边播边合成 - 报告含待确认门店数量
This commit is contained in:
@@ -13,6 +13,7 @@ import smartSchedulingRoutes from './routes/smart-scheduling.js'
|
||||
import situationalAwarenessRoutes from './routes/situational-awareness.js'
|
||||
import analyticsEnhancedRoutes from './routes/analytics-enhanced.js'
|
||||
import adminRoutes from './routes/admin.js'
|
||||
import ttsRoutes from './routes/tts.js'
|
||||
|
||||
const app = express()
|
||||
const PORT = parseInt(process.env.PORT || '3333')
|
||||
@@ -59,6 +60,7 @@ app.use('/api/store-expense', storeExpenseRoutes)
|
||||
app.use('/api/smart-scheduling', smartSchedulingRoutes)
|
||||
app.use('/api/situational-awareness', situationalAwarenessRoutes)
|
||||
app.use('/api/analytics-enhanced', analyticsEnhancedRoutes)
|
||||
app.use('/api/tts', ttsRoutes)
|
||||
|
||||
app.use(notFoundHandler)
|
||||
app.use(errorHandler)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Router } from 'express'
|
||||
import { sendSuccess, sendError } from '../middleware/error.js'
|
||||
import type { AuthRequest } from '../middleware/auth.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const TTS_API_KEY = process.env.TTS_API_KEY || 'sk-ws-H.EHYXPRE.Gv4a.MEQCIDOofoXP-VBQILl0c_kspJZhSUj8fyYIZupigveHnpRrAiAsjWXuJ3dpyTBDCUF1ZLdcvLz8GJoan0ObIkCwfE06kQ'
|
||||
const TTS_API_HOST = process.env.TTS_API_HOST || 'llm-znfsxsdp6uwik7ad.cn-beijing.maas.aliyuncs.com'
|
||||
const TTS_URL = `https://${TTS_API_HOST}/api/v1/services/audio/tts/SpeechSynthesizer`
|
||||
|
||||
router.post('/', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const { text } = req.body
|
||||
if (!text || typeof text !== 'string') {
|
||||
return sendError(res, 'text is required')
|
||||
}
|
||||
|
||||
if (text.length > 3000) {
|
||||
return sendError(res, 'text too long (max 3000 characters)')
|
||||
}
|
||||
|
||||
const response = await fetch(TTS_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${TTS_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'cosyvoice-v3-flash',
|
||||
input: {
|
||||
text,
|
||||
voice: 'longanyang',
|
||||
format: 'mp3',
|
||||
sample_rate: 22050,
|
||||
volume: 50,
|
||||
rate: 1.0,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text()
|
||||
console.error('TTS API error:', response.status, errText)
|
||||
return sendError(res, `TTS API error: ${response.status}`, 502)
|
||||
}
|
||||
|
||||
const result: any = await response.json()
|
||||
|
||||
if (result.output?.audio?.url) {
|
||||
sendSuccess(res, { url: result.output.audio.url })
|
||||
} else {
|
||||
console.error('TTS API unexpected response:', JSON.stringify(result))
|
||||
sendError(res, 'TTS API returned no audio URL', 502)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('TTS route error:', err.message)
|
||||
sendError(res, err.message, 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user