- Add Coze TTS endpoint and service; expose binary MP3 from controller. - Bypass ResponseFilter for /audio/speech so MP3 bodies are not UTF-8 wrapped. - UniApp: cozeTextToSpeech, TTS UI and play flow; SSE HTTP errors and diagnostics. - Document TTS in docs/features.md; extend test-0325-1 with curl verification. Made-with: Cursor
672 lines
21 KiB
JavaScript
672 lines
21 KiB
JavaScript
// API服务工具文件
|
||
// 统一访问 crmeb-front 项目,基地址来自 config/app.js
|
||
import { domain, TOKENNAME } from '@/config/app.js'
|
||
import store from '@/store'
|
||
|
||
const API_BASE_URL = domain
|
||
|
||
/**
|
||
* 通用请求方法
|
||
* @param {string} url 请求地址
|
||
* @param {object} options 请求配置
|
||
* @returns {Promise} 请求结果
|
||
*/
|
||
function request(url, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const token = store.state && store.state.app && store.state.app.token
|
||
uni.request({
|
||
url: `${API_BASE_URL}${url}`,
|
||
method: options.method || 'GET',
|
||
data: options.data || {},
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { [TOKENNAME]: token } : {}),
|
||
...options.header
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
resolve(res.data)
|
||
} else {
|
||
reject(new Error(`请求失败: ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
reject(error)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 根据ID获取文章详情
|
||
* @param {string|number} id 文章ID
|
||
* @returns {Promise} 文章详情数据
|
||
*/
|
||
function getArticleById(id) {
|
||
return request(`/api/front/article-models/${id}`)
|
||
}
|
||
|
||
/**
|
||
* 获取文章列表
|
||
* @param {object} params 查询参数
|
||
* @param {number} params.page 页码,默认1
|
||
* @param {number} params.size 每页数量,默认10
|
||
* @returns {Promise} 文章列表数据
|
||
*/
|
||
function getArticleList(params = { page: 1, size: 10 }) {
|
||
// 构建查询字符串(兼容小程序环境)
|
||
const queryString = Object.keys(params)
|
||
.filter(key => params[key] !== undefined && params[key] !== null)
|
||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||
.join('&')
|
||
|
||
const url = queryString ? `/api/front/article-models?${queryString}` : '/api/front/article-models'
|
||
return request(url, {
|
||
method: 'GET'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 根据条件查询文章列表
|
||
* @param {object} params 查询参数
|
||
* @param {number} params.statusTask 任务状态(可选)
|
||
* @param {string} params.uid 用户ID(可选)
|
||
* @param {string} params.tags 标签(可选)
|
||
* @param {string} params.type 文章类型(可选)
|
||
* @param {number} params.page 页码,默认1
|
||
* @param {number} params.size 每页数量,默认10
|
||
* @returns {Promise} 符合条件的文章列表数据
|
||
*/
|
||
function searchArticles(params = { page: 1, size: 10 }) {
|
||
// 构建查询字符串(兼容小程序环境)
|
||
const queryString = Object.keys(params)
|
||
.filter(key => params[key] !== undefined && params[key] !== null)
|
||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||
.join('&')
|
||
|
||
const url = queryString ? `/api/front/article-models/search?${queryString}` : '/api/front/article-models/search'
|
||
return request(url, {
|
||
method: 'GET'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 创建文生视频任务
|
||
* @param {object} params 创建参数
|
||
* @param {string} params.prompt 文本描述
|
||
* @param {Array} params.image_urls 图片URL数组(可选)
|
||
* @param {string} params.aspect_ratio 视频比例,如 'portrait' 或 '16:9'
|
||
* @param {number} params.n_frames 帧数,默认5
|
||
* @param {string} params.size 视频尺寸,默认 'standard'
|
||
* @param {boolean} params.remove_watermark 是否移除水印,默认true
|
||
* @returns {Promise} 任务创建结果
|
||
*/
|
||
function createTextToVideoTask(params) {
|
||
const requestBody = {
|
||
model: "sora-2-text-to-video",
|
||
title: params.prompt,
|
||
task_id: "",
|
||
nickname: params.nickname || "",
|
||
uid: params.uid || "",
|
||
input: {
|
||
prompt: params.prompt,
|
||
image_urls: params.image_urls || [],
|
||
aspect_ratio: params.aspect_ratio || "portrait",
|
||
n_frames: params.n_frames || 5,
|
||
size: params.size || "standard",
|
||
remove_watermark: params.remove_watermark !== false
|
||
}
|
||
}
|
||
|
||
return request('/api/front/kieai/text-to-video', {
|
||
method: 'POST',
|
||
data: requestBody
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 创建图生视频任务
|
||
* @param {object} params 创建参数
|
||
* @param {string} params.imageUrl 图片URL
|
||
* @param {string} params.prompt 文本描述(可选)
|
||
* @param {string} params.aspect_ratio 视频比例,如 'portrait' 或 '16:9'
|
||
* @param {number} params.n_frames 帧数,默认5
|
||
* @param {string} params.size 视频尺寸,默认 'standard'
|
||
* @param {boolean} params.remove_watermark 是否移除水印,默认true
|
||
* @returns {Promise} 任务创建结果
|
||
*/
|
||
function createImageToVideoTask(params) {
|
||
const requestBody = {
|
||
model: "sora-2-image-to-video",
|
||
title: params.prompt || "根据图片生成视频",
|
||
task_id: "",
|
||
nickname: params.nickname || "",
|
||
uid: params.uid || "",
|
||
input: {
|
||
prompt: params.prompt || "根据图片生成视频",
|
||
image_urls: [params.imageUrl],
|
||
aspect_ratio: params.aspect_ratio || "portrait",
|
||
n_frames: params.n_frames || 5,
|
||
size: params.size || "standard",
|
||
remove_watermark: params.remove_watermark !== false
|
||
}
|
||
}
|
||
|
||
return request('/api/front/kieai/image-to-video', {
|
||
method: 'POST',
|
||
data: requestBody
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 创建图片编辑任务
|
||
* @param {object} params 创建参数
|
||
* @param {string} params.prompt 编辑描述,如"改一下"、"去掉背景"
|
||
* @param {Array} params.image_urls 图片URL数组
|
||
* @param {string} params.output_format 输出格式,默认'png'
|
||
* @param {string} params.image_size 图片尺寸,默认'2:3'
|
||
* @param {string} params.title 任务标题,默认使用prompt
|
||
* @param {string} params.task_id 任务ID(可选)
|
||
* @returns {Promise} 任务创建结果
|
||
*/
|
||
function createImageEditTask(params) {
|
||
const requestBody = {
|
||
model: "google/nano-banana-edit",
|
||
title: params.title || params.prompt || "图片编辑",
|
||
task_id: params.task_id || "",
|
||
nickname: params.nickname || "",
|
||
uid: params.uid || "",
|
||
input: {
|
||
prompt: params.prompt,
|
||
image_urls: params.image_urls || [],
|
||
output_format: params.output_format || "png",
|
||
image_size: params.image_size || "2:3",
|
||
aspect_ratio: params.aspect_ratio || "9:16",
|
||
resolution: params.resolution || "2K",
|
||
}
|
||
}
|
||
|
||
return request('/api/front/kieai/image-edit', {
|
||
method: 'POST',
|
||
data: requestBody
|
||
})
|
||
}
|
||
|
||
|
||
/**
|
||
* 创建图片编辑任务
|
||
* @param {object} params 创建参数
|
||
* @param {string} params.prompt 编辑描述,如"改一下"、"去掉背景"
|
||
* @param {Array} params.image_urls 图片URL数组
|
||
* @param {string} params.output_format 输出格式,默认'png'
|
||
* @param {string} params.aspect_ratio 图片尺寸,默认'2:3'
|
||
* @param {string} params.title 任务标题,默认使用prompt
|
||
* @param {string} params.task_id 任务ID(可选)
|
||
* @returns {Promise} 任务创建结果
|
||
*/
|
||
function createImageEditTaskPro(params) {
|
||
const requestBody = {
|
||
model: "nano-banana-pro",
|
||
title: params.title || params.prompt || "图片编辑",
|
||
task_id: params.task_id || "",
|
||
nickname: params.nickname || "",
|
||
uid: params.uid || "",
|
||
input: {
|
||
prompt: params.prompt,
|
||
image_urls: params.image_urls || [],
|
||
output_format: params.output_format || "png",
|
||
aspect_ratio: params.aspect_ratio || "2:3",
|
||
resolution: params.resolution || "2K"
|
||
}
|
||
}
|
||
|
||
return request('/api/front/kieai/image-edit', {
|
||
method: 'POST',
|
||
data: requestBody
|
||
})
|
||
}
|
||
|
||
|
||
/**
|
||
* 用户上传文件
|
||
* @param {string} filePath 文件路径
|
||
* @param {object} options 上传配置
|
||
* @param {string} options.model 模块类型,默认'user'
|
||
* @param {string} options.pid 分类ID,默认'0'
|
||
* @returns {Promise} 上传结果
|
||
*/
|
||
function uploadFile(filePath, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url: `${API_BASE_URL}/api/front/upload/imageOuter`,
|
||
filePath: filePath,
|
||
name: 'multipart',
|
||
formData: {
|
||
model: options.model || 'user',
|
||
pid: options.pid || '0'
|
||
},
|
||
success: (res) => {
|
||
try {
|
||
let data = res.data
|
||
if (typeof data === 'string') {
|
||
data = JSON.parse(data)
|
||
}
|
||
if (!data || typeof data !== 'object') {
|
||
throw new Error('响应格式异常')
|
||
}
|
||
if (data.code === 200) {
|
||
// 拼接完整的图片URL
|
||
const fullUrl = data.data.url.startsWith('http')
|
||
? data.data.url
|
||
: `https://uthink2025.oss-cn-shanghai.aliyuncs.com/${data.data.url}`
|
||
resolve({
|
||
...data,
|
||
data: {
|
||
...data.data,
|
||
fullUrl: fullUrl
|
||
}
|
||
})
|
||
} else {
|
||
reject(new Error(data.message || '上传失败'))
|
||
}
|
||
} catch (error) {
|
||
const raw = typeof res.data === 'string' ? res.data.slice(0, 300) : String(res.data || '')
|
||
const msg = error.message || '响应数据解析失败'
|
||
reject(new Error(msg + (raw ? ' body: ' + raw : '')))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
reject(new Error('上传请求失败'))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 创建语音识别任务
|
||
* @param {object} params 请求参数
|
||
* @param {string} params.url 音频文件URL
|
||
* @param {string} params.engineModelType 引擎模型类型,默认16k_zh
|
||
* @param {number} params.channelNum 声道数,默认1
|
||
* @param {number} params.resTextFormat 结果文本格式,默认0
|
||
* @param {number} params.sourceType 源类型,默认0
|
||
* @returns {Promise} 识别任务信息
|
||
*/
|
||
function createAsrTask(params) {
|
||
const defaultParams = {
|
||
engineModelType: '16k_zh',
|
||
channelNum: 1,
|
||
resTextFormat: 0,
|
||
sourceType: 0,
|
||
filterDirty: false,
|
||
filterModal: false,
|
||
convertNumMode: false,
|
||
wordInfo: false
|
||
}
|
||
|
||
return request('/api/front/tencent/asr/create-task', {
|
||
method: 'POST',
|
||
data: {
|
||
...defaultParams,
|
||
...params
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 查询语音识别任务状态
|
||
* @param {string|number} taskId 任务ID
|
||
* @returns {Promise} 任务状态和识别结果
|
||
*/
|
||
function queryAsrStatus(taskId) {
|
||
return request(`/api/front/tencent/asr/query-status/${taskId}`)
|
||
}
|
||
|
||
// ==================== KieAI Gemini Chat(BUG-005:AI 营养师文本/多模态对话) ====================
|
||
|
||
/**
|
||
* KieAI Gemini 对话
|
||
* POST /api/front/kieai/gemini/chat
|
||
* 文本对话请求体: { messages: [{ role: 'user', content: 用户输入 }], stream: false }
|
||
* 多模态时 content 可为 OpenAI 风格 parts 数组。
|
||
* 成功时 HTTP 体为 CommonResult:模型结果为 data 字段(OpenAI 形态:data.choices[0].message.content)。
|
||
* 页面展示必须从该 content 读取,禁止用语义无关的本地固定话术冒充模型输出。
|
||
* @param {object} data 请求体
|
||
* @param {Array<{role: string, content: string|Array}>} data.messages 消息列表
|
||
* @param {boolean} [data.stream=false] 是否流式
|
||
* @returns {Promise<{code: number, data: {choices?: Array<{message?: {content?: unknown}}>}}>}
|
||
*/
|
||
function kieaiGeminiChat(data) {
|
||
const messages = data && data.messages
|
||
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||
return Promise.reject(new Error('messages 不能为空'))
|
||
}
|
||
// BUG-005:仅传 messages + stream;文本对话为 { messages: [{ role: 'user', content: 用户输入 }], stream: false }
|
||
const stream = data && typeof data.stream === 'boolean' ? data.stream : false
|
||
return request('/api/front/kieai/gemini/chat', {
|
||
method: 'POST',
|
||
data: { messages, stream }
|
||
})
|
||
}
|
||
|
||
// ==================== 扣子Coze API ====================
|
||
|
||
/**
|
||
* Coze - 发起对话 (Chat)
|
||
* @param {object} data 请求参数
|
||
* @param {string} data.bot_id 机器人ID
|
||
* @param {string} data.user_id 用户ID
|
||
* @param {Array} data.additional_messages 附加消息列表
|
||
* @param {boolean} data.stream 是否流式返回
|
||
* @param {boolean} data.auto_save_history 是否自动保存历史
|
||
* @param {object} data.meta_data 元数据
|
||
* @returns {Promise} 对话响应
|
||
*/
|
||
function cozeChat(data) {
|
||
return request('/api/front/coze/chat', {
|
||
method: 'POST',
|
||
data: data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 流式对话 (Chat Stream via SSE + enableChunked)
|
||
* 使用微信小程序 enableChunked 能力消费 SSE 事件流
|
||
* @param {object} data 请求参数(与 cozeChat 一致)
|
||
* @returns {object} 控制器 { onMessage, onError, onComplete, abort, getTask }
|
||
*/
|
||
function cozeChatStream(data) {
|
||
let _onMessage = () => {}
|
||
let _onError = () => {}
|
||
let _onComplete = () => {}
|
||
let _buffer = ''
|
||
let _task = null
|
||
let _gotChunks = false
|
||
|
||
const controller = {
|
||
onMessage(fn) { _onMessage = fn; return controller },
|
||
onError(fn) { _onError = fn; return controller },
|
||
onComplete(fn) { _onComplete = fn; return controller },
|
||
abort() { if (_task) _task.abort() },
|
||
getTask() { return _task }
|
||
}
|
||
|
||
const parseSseLines = (text) => {
|
||
_buffer += text
|
||
const lines = _buffer.split('\n')
|
||
_buffer = lines.pop() || ''
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim()
|
||
if (!trimmed || trimmed.startsWith(':')) continue
|
||
if (trimmed.startsWith('data:')) {
|
||
const jsonStr = trimmed.slice(5).trim()
|
||
if (!jsonStr) continue
|
||
try {
|
||
const evt = JSON.parse(jsonStr)
|
||
_onMessage(evt)
|
||
} catch (e) {
|
||
console.warn('[cozeChatStream] JSON parse failed in chunk, raw:', jsonStr.slice(0, 200))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const parseSseResponseBody = (body) => {
|
||
if (!body || typeof body !== 'string') {
|
||
console.warn('[cozeChatStream] parseSseResponseBody: body is not a string, type:', typeof body)
|
||
return
|
||
}
|
||
console.log('[cozeChatStream] parseSseResponseBody: body length', body.length, 'preview:', body.slice(0, 200))
|
||
const lines = body.split('\n')
|
||
for (const line of lines) {
|
||
const trimmed = line.trim()
|
||
if (!trimmed || trimmed.startsWith(':')) continue
|
||
if (trimmed.startsWith('data:')) {
|
||
const jsonStr = trimmed.slice(5).trim()
|
||
if (!jsonStr) continue
|
||
try {
|
||
const evt = JSON.parse(jsonStr)
|
||
_onMessage(evt)
|
||
} catch (e) {
|
||
console.warn('[cozeChatStream] JSON parse failed in body fallback, raw:', jsonStr.slice(0, 200))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const token = store.state && store.state.app && store.state.app.token
|
||
_task = uni.request({
|
||
url: `${API_BASE_URL}/api/front/coze/chat/stream`,
|
||
method: 'POST',
|
||
data: data,
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { [TOKENNAME]: token } : {})
|
||
},
|
||
enableChunked: true,
|
||
responseType: 'text',
|
||
success: (res) => {
|
||
console.log('[cozeChatStream] success: statusCode=', res.statusCode, '_gotChunks=', _gotChunks)
|
||
if (res.statusCode !== 200) {
|
||
let errMsg = '请求失败: ' + res.statusCode
|
||
try {
|
||
const body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
||
if (body && body.message) errMsg = body.message
|
||
else if (body && body.msg) errMsg = body.msg
|
||
} catch (e) { /* keep default message */ }
|
||
console.error('[cozeChatStream] HTTP error', res.statusCode, errMsg)
|
||
_onError(new Error(errMsg))
|
||
return
|
||
}
|
||
if (_buffer.trim()) {
|
||
parseSseLines('\n')
|
||
}
|
||
if (!_gotChunks && res && res.data) {
|
||
const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
|
||
parseSseResponseBody(body)
|
||
}
|
||
_onComplete()
|
||
},
|
||
fail: (err) => {
|
||
console.error('[cozeChatStream] request fail:', err)
|
||
_onError(err)
|
||
}
|
||
})
|
||
|
||
if (_task && _task.onChunkReceived) {
|
||
_task.onChunkReceived((res) => {
|
||
_gotChunks = true
|
||
try {
|
||
const bytes = new Uint8Array(res.data)
|
||
let text = ''
|
||
for (let i = 0; i < bytes.length; i++) {
|
||
text += String.fromCharCode(bytes[i])
|
||
}
|
||
text = decodeURIComponent(escape(text))
|
||
console.log('[cozeChatStream] chunk received, decoded length:', text.length)
|
||
parseSseLines(text)
|
||
} catch (e) {
|
||
console.warn('[cozeChatStream] chunk decode error:', e)
|
||
}
|
||
})
|
||
}
|
||
|
||
return controller
|
||
}
|
||
|
||
/**
|
||
* Coze - 检索对话详情 (Retrieve Chat)
|
||
* @param {object} params 请求参数
|
||
* @param {string} params.conversationId 会话ID
|
||
* @param {string} params.chatId 对话ID
|
||
* @returns {Promise} 对话详情
|
||
*/
|
||
function cozeRetrieveChat(params) {
|
||
return request('/api/front/coze/chat/retrieve', {
|
||
method: 'POST',
|
||
data: params
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 查看对话消息详情 (List Messages)
|
||
* @param {object} params 请求参数
|
||
* @param {string} params.conversationId 会话ID
|
||
* @param {string} params.chatId 对话ID
|
||
* @returns {Promise} 消息列表
|
||
*/
|
||
function cozeMessageList(params) {
|
||
return request('/api/front/coze/chat/messages/list', {
|
||
method: 'POST',
|
||
data: params
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 执行工作流 (Run Workflow)
|
||
* @param {object} data 请求参数
|
||
* @param {string} data.workflowId 工作流ID
|
||
* @param {object} data.parameters 工作流参数
|
||
* @param {boolean} data.isAsync 是否异步
|
||
* @returns {Promise} 执行结果
|
||
*/
|
||
function cozeWorkflowRun(data) {
|
||
return request('/api/front/coze/workflow/run', {
|
||
method: 'POST',
|
||
data: data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 执行工作流 (Run Workflow Stream)
|
||
* @param {object} data 请求参数
|
||
* @param {string} data.workflowId 工作流ID
|
||
* @param {object} data.parameters 工作流参数
|
||
* @returns {Promise} 执行结果
|
||
*/
|
||
function cozeWorkflowStream(data) {
|
||
return request('/api/front/coze/workflow/stream', {
|
||
method: 'POST',
|
||
data: data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 恢复工作流 (Resume Workflow)
|
||
* @param {object} data 请求参数
|
||
* @param {string} data.workflow_id 工作流ID
|
||
* @param {string} data.event_id 事件ID
|
||
* @param {string} data.resume_data 恢复数据
|
||
* @param {number} data.resume_type 恢复类型
|
||
* @returns {Promise} 执行结果
|
||
*/
|
||
function cozeWorkflowResume(data) {
|
||
return request('/api/front/coze/workflow/resume', {
|
||
method: 'POST',
|
||
data: data
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 上传文件 (Upload File)
|
||
* @param {string} filePath 文件路径
|
||
* @returns {Promise} 上传结果
|
||
*/
|
||
function cozeUploadFile(filePath) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url: `${API_BASE_URL}/api/front/coze/file/upload`,
|
||
filePath: filePath,
|
||
name: 'file',
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
try {
|
||
const data = JSON.parse(res.data)
|
||
resolve(data)
|
||
} catch (e) {
|
||
reject(new Error('响应解析失败'))
|
||
}
|
||
} else {
|
||
reject(new Error(`上传失败: ${res.statusCode}`))
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Coze - 文本转语音 (TTS)
|
||
* POST /api/front/coze/audio/speech
|
||
* 返回 Promise<string> 临时文件路径,可直接赋给 innerAudioContext.src 播放
|
||
* @param {object} data 请求参数
|
||
* @param {string} data.input 要合成的文本
|
||
* @param {string} [data.voiceId] 音色ID,不传时后端使用默认中文音色
|
||
* @param {string} [data.format] 音频格式,默认 "mp3"
|
||
* @param {number} [data.speed] 语速,默认 1.0
|
||
* @returns {Promise<string>} 临时音频文件路径
|
||
*/
|
||
function cozeTextToSpeech(data) {
|
||
return new Promise((resolve, reject) => {
|
||
const token = store.state && store.state.app && store.state.app.token
|
||
uni.request({
|
||
url: `${API_BASE_URL}/api/front/coze/audio/speech`,
|
||
method: 'POST',
|
||
data: data,
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { [TOKENNAME]: token } : {})
|
||
},
|
||
responseType: 'arraybuffer',
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
const fs = uni.getFileSystemManager()
|
||
const tempPath = `${uni.env.USER_DATA_PATH}/tts_${Date.now()}.mp3`
|
||
fs.writeFile({
|
||
filePath: tempPath,
|
||
data: res.data, // ArrayBuffer — 不传 encoding,否则数据会损坏
|
||
success: () => resolve(tempPath),
|
||
fail: (err) => reject(new Error('TTS 音频写入失败: ' + JSON.stringify(err)))
|
||
})
|
||
} else {
|
||
let errMsg = 'TTS 请求失败: ' + res.statusCode
|
||
try {
|
||
const body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
||
if (body && body.message) errMsg = body.message
|
||
} catch (e) { /* keep default */ }
|
||
reject(new Error(errMsg))
|
||
}
|
||
},
|
||
fail: (err) => reject(new Error('TTS 网络请求失败: ' + JSON.stringify(err)))
|
||
})
|
||
})
|
||
}
|
||
|
||
export default {
|
||
request,
|
||
getArticleById,
|
||
getArticleList,
|
||
searchArticles,
|
||
createTextToVideoTask,
|
||
createImageToVideoTask,
|
||
createImageEditTask,
|
||
createImageEditTaskPro,
|
||
uploadFile,
|
||
createAsrTask,
|
||
queryAsrStatus,
|
||
kieaiGeminiChat,
|
||
// Coze API
|
||
cozeChat,
|
||
cozeChatStream,
|
||
cozeRetrieveChat,
|
||
cozeMessageList,
|
||
cozeWorkflowRun,
|
||
cozeWorkflowStream,
|
||
cozeWorkflowResume,
|
||
cozeUploadFile,
|
||
cozeTextToSpeech
|
||
}
|