Files
msh-system/msh_single_uniapp/api/models-api.js
panchengyong 3023115bb0 fix: 移除损坏的 Claude gitlink 并同步业务与文档更新
- 从索引移除误记录的 .claude/worktrees gitlink(旧绝对路径会导致 git 命令失败)
- 新增根目录 .gitignore 忽略 .claude/worktrees 与 .DS_Store
- 后端:Coze/知识库、ResultAdvice、应用配置
- 前端 uniapp:AI 营养、食物百科等页面与 API
- 更新 README、测试文档与 shop-msh.sql

Made-with: Cursor
2026-03-30 12:46:24 +08:00

590 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API服务工具文件
// 统一访问 crmeb-front 项目,基地址来自 config/app.js
import { domain } from '@/config/app.js'
const API_BASE_URL = domain
/**
* 通用请求方法
* @param {string} url 请求地址
* @param {object} options 请求配置
* @returns {Promise} 请求结果
*/
function request(url, options = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: `${API_BASE_URL}${url}`,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
...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}`)
}
// ==================== 扣子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} 对话响应
*/
/**
* KieAI Gemini 对话POST /api/front/kieai/gemini/chat
* 文本对话请求体: { messages: [{ role: 'user', content: 用户输入 }], stream: false }
* @param {object} data 请求体
* @param {Array} data.messages 消息列表 [{ role: 'user'|'assistant'|'system', content: string|Array }]
* @param {boolean} data.stream 是否流式,默认 false
* @returns {Promise} 响应 data 为 Gemini 格式 { choices: [{ message: { content } }] },回复取 data.choices[0].message.content
*/
function kieaiGeminiChat(data) {
return request('/api/front/kieai/gemini/chat', {
method: 'POST',
data: data
})
}
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) {
// skip malformed JSON fragments
}
}
}
}
const parseSseResponseBody = (body) => {
if (!body || typeof body !== 'string') return
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) {
// skip malformed JSON fragments
}
}
}
}
const token = uni.getStorageSync('LOGIN_STATUS_TOKEN') || ''
_task = uni.request({
url: `${API_BASE_URL}/api/front/coze/chat/stream`,
method: 'POST',
data: data,
header: {
'Content-Type': 'application/json',
...(token ? { 'Authori-zation': token } : {})
},
enableChunked: true,
responseType: 'text',
success: (res) => {
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) => {
_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))
parseSseLines(text)
} catch (e) {
// chunk decode error, skip
}
})
}
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)
}
})
})
}
export default {
request,
getArticleById,
getArticleList,
searchArticles,
createTextToVideoTask,
createImageToVideoTask,
createImageEditTask,
createImageEditTaskPro,
uploadFile,
createAsrTask,
queryAsrStatus,
kieaiGeminiChat,
// Coze API
cozeChat,
cozeChatStream,
cozeRetrieveChat,
cozeMessageList,
cozeWorkflowRun,
cozeWorkflowStream,
cozeWorkflowResume,
cozeUploadFile
}