fix: 移除损坏的 Claude gitlink 并同步业务与文档更新

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

Made-with: Cursor
This commit is contained in:
panchengyong
2026-03-30 12:46:24 +08:00
parent 3329a2b296
commit 3023115bb0
19 changed files with 671 additions and 166 deletions

View File

@@ -63,13 +63,13 @@
<!-- 消息气泡 -->
<view :class="['message-bubble', msg.role === 'user' ? 'user-bubble' : 'ai-bubble']">
<!-- AI 消息 loading 占位等待回复时显示打字动画-->
<view v-if="msg.role === 'ai' && msg.loading" class="typing-indicator">
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
<text v-else-if="msg.type !== 'image'" class="message-text">{{ msg.content }}</text>
<!-- AI 消息 loading 占位等待回复时显示打字动画-->
<view v-if="msg.role === 'ai' && msg.loading" class="typing-indicator">
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
<text v-else-if="msg.type !== 'image'" class="message-text">{{ msg.content }}<text v-if="msg.streaming" class="streaming-cursor">|</text></text>
<image
v-else
:src="msg.imageUrl"
@@ -81,8 +81,8 @@
</view>
</view>
<!-- 加载中提示 -->
<view v-if="isLoading" class="message-item ai-message">
<!-- 加载中提示仅在没有流式占位消息时显示 -->
<view v-if="isLoading && !messageList.some(m => m.loading || m.streaming)" class="message-item ai-message">
<view class="message-avatar">
<text class="avatar-icon">🤖</text>
</view>
@@ -236,6 +236,10 @@ export default {
if (this.isRecording && this.recorderManager) {
this.recorderManager.stop();
}
if (this._streamCtrl) {
this._streamCtrl.abort();
this._streamCtrl = null;
}
},
methods: {
// 初始化录音管理器
@@ -541,8 +545,13 @@ export default {
content: '确定要清空对话吗?',
success: (res) => {
if (res.confirm) {
this.messageList = []
this.conversationId = '' // 清空会话ID开始新的对话
if (this._streamCtrl) {
this._streamCtrl.abort();
this._streamCtrl = null;
}
this.isLoading = false;
this.messageList = [];
this.conversationId = '';
}
}
})
@@ -650,79 +659,164 @@ export default {
return data;
},
buildCozeMessages(content, type) {
const messages = [];
if (type === 'text') {
messages.push({
role: 'user',
content: typeof content === 'string' ? content : JSON.stringify(content),
content_type: 'text'
});
} else if (type === 'multimodal') {
const parts = Array.isArray(content) ? content : [{ type: 'text', text: String(content) }];
const textPart = parts.find(p => p && p.type === 'text');
const imgPart = parts.find(p => p && (p.type === 'image_url' || p.type === 'image'));
if (imgPart) {
const fileId = imgPart.file_id || (imgPart.image_url && imgPart.image_url.url) || '';
messages.push({
role: 'user',
content: JSON.stringify([
...(textPart ? [{ type: 'text', text: textPart.text }] : []),
{ type: 'image', file_id: fileId }
]),
content_type: 'object_string'
});
} else {
messages.push({
role: 'user',
content: textPart ? textPart.text : JSON.stringify(content),
content_type: 'text'
});
}
} else {
let fileInfo = content;
if (typeof fileInfo === 'string') {
try { fileInfo = JSON.parse(fileInfo); } catch (e) { /* ignore */ }
}
const fileId = (fileInfo && fileInfo.id) || (fileInfo && fileInfo.file_id) || '';
if (fileId) {
messages.push({
role: 'user',
content: JSON.stringify([{ type: 'image', file_id: fileId }]),
content_type: 'object_string'
});
} else {
messages.push({
role: 'user',
content: '我发送了一张图片,请帮我分析',
content_type: 'text'
});
}
}
return messages;
},
supportsChunked() {
try {
const sysInfo = uni.getSystemInfoSync();
if (sysInfo.SDKVersion) {
const parts = sysInfo.SDKVersion.split('.').map(Number);
return (parts[0] > 2) || (parts[0] === 2 && parts[1] > 20) ||
(parts[0] === 2 && parts[1] === 20 && (parts[2] || 0) >= 1);
}
} catch (e) { /* fallback */ }
return false;
},
async sendToAI(content, type) {
this.isLoading = true;
// 添加 AI 占位消息loading 状态,等待 Coze 返回后填充内容)
const aiMsg = { role: 'ai', content: '', loading: true };
const aiMsg = { role: 'ai', content: '', loading: true, streaming: false };
this.messageList.push(aiMsg);
this.scrollToBottom();
// 统一走 Coze API文本、多模态、图片均使用 Coze Bot
const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user';
const messages = this.buildCozeMessages(content, type);
const requestData = {
botId: this.botId,
userId: userId,
additionalMessages: messages,
stream: true,
autoSaveHistory: true
};
if (this.conversationId) requestData.conversationId = this.conversationId;
if (this.supportsChunked()) {
this.sendToAIStream(requestData, aiMsg);
} else {
this.sendToAIPoll(requestData, aiMsg);
}
},
sendToAIStream(requestData, aiMsg) {
const ctrl = api.cozeChatStream(requestData)
.onMessage((evt) => {
const eventType = evt.event || '';
if (eventType === 'conversation.chat.created') {
if (evt.conversation_id) {
this.conversationId = evt.conversation_id;
}
} else if (eventType === 'conversation.message.delta') {
const role = evt.role || 'assistant';
const type = evt.type || 'answer';
if (role === 'assistant' && type === 'answer') {
if (aiMsg.loading) {
aiMsg.loading = false;
aiMsg.streaming = true;
}
aiMsg.content += (evt.content || '');
this.messageList = [...this.messageList];
this.scrollToBottom();
}
} else if (eventType === 'conversation.chat.completed') {
aiMsg.streaming = false;
this.isLoading = false;
this.messageList = [...this.messageList];
this.scrollToBottom();
} else if (eventType === 'conversation.chat.failed') {
aiMsg.loading = false;
aiMsg.streaming = false;
if (!aiMsg.content) {
aiMsg.content = '抱歉AI 对话失败,请稍后再试。';
}
this.isLoading = false;
this.messageList = [...this.messageList];
this.scrollToBottom();
}
})
.onError((err) => {
console.error('SSE 流式对话失败:', err);
aiMsg.loading = false;
aiMsg.streaming = false;
if (!aiMsg.content) {
aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。';
}
this.isLoading = false;
this.messageList = [...this.messageList];
this.scrollToBottom();
})
.onComplete(() => {
aiMsg.loading = false;
aiMsg.streaming = false;
if (!aiMsg.content) {
aiMsg.content = '未能获取到有效回复。';
}
this.isLoading = false;
this.messageList = [...this.messageList];
this.scrollToBottom();
});
this._streamCtrl = ctrl;
},
async sendToAIPoll(requestData, aiMsg) {
requestData.stream = false;
try {
const messages = [];
if (type === 'text') {
// 纯文字消息
messages.push({
role: 'user',
content: typeof content === 'string' ? content : JSON.stringify(content),
content_type: 'text'
});
} else if (type === 'multimodal') {
// 图文混合content 为 parts 数组 [{ type: 'text', text }, { type: 'image_url', ... }]
const parts = Array.isArray(content) ? content : [{ type: 'text', text: String(content) }];
const textPart = parts.find(p => p && p.type === 'text');
const imgPart = parts.find(p => p && (p.type === 'image_url' || p.type === 'image'));
if (imgPart) {
const fileId = imgPart.file_id || (imgPart.image_url && imgPart.image_url.url) || '';
messages.push({
role: 'user',
content: JSON.stringify([
...(textPart ? [{ type: 'text', text: textPart.text }] : []),
{ type: 'image', file_id: fileId }
]),
content_type: 'object_string'
});
} else {
messages.push({
role: 'user',
content: textPart ? textPart.text : JSON.stringify(content),
content_type: 'text'
});
}
} else {
// 图片路径旧路径content 为 fileInfo 对象)
let fileInfo = content;
if (typeof fileInfo === 'string') {
try { fileInfo = JSON.parse(fileInfo); } catch (e) { /* 非JSON */ }
}
const fileId = (fileInfo && fileInfo.id) || (fileInfo && fileInfo.file_id) || '';
if (fileId) {
messages.push({
role: 'user',
content: JSON.stringify([{ type: 'image', file_id: fileId }]),
content_type: 'object_string'
});
} else {
messages.push({
role: 'user',
content: '我发送了一张图片,请帮我分析',
content_type: 'text'
});
}
}
const requestData = {
botId: this.botId,
userId: userId,
additionalMessages: messages,
stream: false,
autoSaveHistory: true
};
if (this.conversationId) requestData.conversationId = this.conversationId;
const response = await api.cozeChat(requestData);
const cozeData = this.unwrapCozeResponse(response);
if (cozeData) {
const response = await api.cozeChat(requestData);
const cozeData = this.unwrapCozeResponse(response);
if (cozeData) {
const chat = cozeData.chat || cozeData;
const conversationId = chat.conversation_id || chat.conversationID || chat.conversationId;
const chatId = chat.id;
@@ -741,13 +835,19 @@ export default {
this.isLoading = false;
aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。';
aiMsg.loading = false;
this.messageList = [...this.messageList]; // 触发响应式更新
this.messageList = [...this.messageList];
this.scrollToBottom();
}
},
getPollInterval(attempt) {
if (attempt <= 10) return 500;
if (attempt <= 30) return 1000;
return 1500;
},
async pollChatStatus(conversationId, chatId, aiMsg) {
const maxAttempts = 60; // 最多轮询60次每次1.5秒即90秒
const maxAttempts = 80;
let attempts = 0;
const checkStatus = async () => {
@@ -761,37 +861,33 @@ export default {
}
try {
const res = await api.cozeRetrieveChat({
conversationId,
chatId
});
const retrieveData = this.unwrapCozeResponse(res);
if (retrieveData) {
console.log("====api.cozeRetrieveChat response====", retrieveData);
const chatObj = retrieveData.chat || retrieveData;
const status = chatObj && chatObj.status;
if (status === 'completed') {
// 对话完成,获取消息详情
await this.getChatMessages(conversationId, chatId, aiMsg);
} else if (status === 'failed' || status === 'canceled') {
this.isLoading = false;
const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}`;
if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; }
else { this.messageList.push({ role: 'ai', content: failMsg }); }
this.scrollToBottom();
const res = await api.cozeRetrieveChat({
conversationId,
chatId
});
const retrieveData = this.unwrapCozeResponse(res);
if (retrieveData) {
const chatObj = retrieveData.chat || retrieveData;
const status = chatObj && chatObj.status;
if (status === 'completed') {
await this.getChatMessages(conversationId, chatId, aiMsg);
} else if (status === 'failed' || status === 'canceled') {
this.isLoading = false;
const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}`;
if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; }
else { this.messageList.push({ role: 'ai', content: failMsg }); }
this.scrollToBottom();
} else {
setTimeout(checkStatus, this.getPollInterval(attempts));
}
} else {
// 继续轮询 (created, in_progress) 每 1.5 秒
setTimeout(checkStatus, 1500);
}
} else {
// 查询失败,重试
setTimeout(checkStatus, 1000);
setTimeout(checkStatus, this.getPollInterval(attempts));
}
} catch (e) {
console.error('查询对话状态失败:', e);
setTimeout(checkStatus, 1000);
setTimeout(checkStatus, this.getPollInterval(attempts));
}
};
@@ -1088,6 +1184,18 @@ export default {
}
}
/* 流式输出闪烁光标 */
.streaming-cursor {
animation: blink 0.8s step-end infinite;
color: #4facfe;
font-weight: 600;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* 打字指示器 */
.typing-indicator {
display: flex;