Merge branch 'feat/kexue-ui'
This commit is contained in:
commit
d81fb4d0a0
|
|
@ -306,4 +306,26 @@ async def stop_generation_handler(message_id: str = None):
|
||||||
message = (
|
message = (
|
||||||
f"已发出停止指令,消息ID: {message_id}" if message_id else "已发出停止指令"
|
f"已发出停止指令,消息ID: {message_id}" if message_id else "已发出停止指令"
|
||||||
)
|
)
|
||||||
return {"success": True, "message": message}
|
return {"success": True, "message": message}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_attachment_handler(url: str):
|
||||||
|
"""删除附件处理器 - 从 OSS 删除文件"""
|
||||||
|
try:
|
||||||
|
from utils.oss_uploader import delete_file, extract_object_key_from_url
|
||||||
|
|
||||||
|
object_key = extract_object_key_from_url(url)
|
||||||
|
if not object_key:
|
||||||
|
raise HTTPException(status_code=400, detail="无效的文件 URL")
|
||||||
|
|
||||||
|
success = delete_file(object_key)
|
||||||
|
if success:
|
||||||
|
return {"success": True, "message": "文件删除成功"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=500, detail="文件删除失败")
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"删除附件失败: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}")
|
||||||
|
|
@ -55,6 +55,7 @@ load_dotenv()
|
||||||
|
|
||||||
# ── 会话管理路由处理器 ────────────────────────────────────────────────
|
# ── 会话管理路由处理器 ────────────────────────────────────────────────
|
||||||
from api.conversation_routes import (add_message_handler,
|
from api.conversation_routes import (add_message_handler,
|
||||||
|
delete_attachment_handler,
|
||||||
delete_conversation_handler,
|
delete_conversation_handler,
|
||||||
get_conversation_handler,
|
get_conversation_handler,
|
||||||
get_conversations_handler,
|
get_conversations_handler,
|
||||||
|
|
@ -227,6 +228,11 @@ async def stop_generation_by_id(message_id: str):
|
||||||
return await stop_generation_handler(message_id)
|
return await stop_generation_handler(message_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/chat-ui/attachment")
|
||||||
|
async def delete_attachment(url: str):
|
||||||
|
return await delete_attachment_handler(url)
|
||||||
|
|
||||||
|
|
||||||
# ── 程序入口 ──────────────────────────────────────────────────────────
|
# ── 程序入口 ──────────────────────────────────────────────────────────
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,13 @@
|
||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
|
<!-- 附件预览区 -->
|
||||||
|
<div v-if="hasAttachments" class="attachments-preview-container">
|
||||||
|
<AttachmentPreview
|
||||||
|
:attachments="currentAttachments"
|
||||||
|
@remove="handleRemoveAttachment"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="input-container" :class="{ wide: isWideMode }">
|
<div class="input-container" :class="{ wide: isWideMode }">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
ref="chatInputRef"
|
ref="chatInputRef"
|
||||||
|
|
@ -56,6 +63,7 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import ChatHeader from "./ChatHeader.vue";
|
import ChatHeader from "./ChatHeader.vue";
|
||||||
import MessageList from "./MessageList.vue";
|
import MessageList from "./MessageList.vue";
|
||||||
import ChatInput from "@/components/input/ChatInput.vue";
|
import ChatInput from "@/components/input/ChatInput.vue";
|
||||||
|
import AttachmentPreview from "@/components/input/AttachmentPreview.vue";
|
||||||
import { MessageType, MessageRole } from "@/types/chat";
|
import { MessageType, MessageRole } from "@/types/chat";
|
||||||
import type { Attachment, Suggestion } from "@/types/chat";
|
import type { Attachment, Suggestion } from "@/types/chat";
|
||||||
import { chatApi, type ModelInfo } from "@/services/api";
|
import { chatApi, type ModelInfo } from "@/services/api";
|
||||||
|
|
@ -119,6 +127,14 @@ const inputPlaceholder = computed(() => {
|
||||||
return "输入你的问题,按 Ctrl+Enter 发送";
|
return "输入你的问题,按 Ctrl+Enter 发送";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 附件相关
|
||||||
|
const currentAttachments = computed(() => chatInputRef.value?.attachments || []);
|
||||||
|
const hasAttachments = computed(() => currentAttachments.value.length > 0);
|
||||||
|
|
||||||
|
function handleRemoveAttachment(id: string) {
|
||||||
|
chatInputRef.value?.removeAttachment(id);
|
||||||
|
}
|
||||||
|
|
||||||
function toggleWideMode() {
|
function toggleWideMode() {
|
||||||
isWideMode.value = !isWideMode.value;
|
isWideMode.value = !isWideMode.value;
|
||||||
}
|
}
|
||||||
|
|
@ -513,6 +529,17 @@ watch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachments-preview-container {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #f3f4f5;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background: #1e1e2e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// min-width: 1000px;
|
// min-width: 1000px;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,6 @@
|
||||||
class="chat-input-container"
|
class="chat-input-container"
|
||||||
:class="{ 'is-focused': isFocused, 'is-expanded': isExpanded }"
|
:class="{ 'is-focused': isFocused, 'is-expanded': isExpanded }"
|
||||||
>
|
>
|
||||||
<!-- 附件预览区 -->
|
|
||||||
<AttachmentPreview
|
|
||||||
v-if="attachments.length > 0"
|
|
||||||
:attachments="attachments"
|
|
||||||
@remove="removeAttachment"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<!-- 左侧功能按钮 -->
|
<!-- 左侧功能按钮 -->
|
||||||
|
|
@ -414,13 +407,28 @@ async function uploadFileToServer(id: string, file: File) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除附件
|
// 移除附件
|
||||||
function removeAttachment(id: string) {
|
async function removeAttachment(id: string) {
|
||||||
const index = attachments.value.findIndex((a) => a.id === id);
|
const index = attachments.value.findIndex((a) => a.id === id);
|
||||||
if (index !== -1) {
|
if (index === -1) return;
|
||||||
// 释放 blob URL
|
|
||||||
URL.revokeObjectURL(attachments.value[index].url);
|
const attachment = attachments.value[index];
|
||||||
attachments.value.splice(index, 1);
|
|
||||||
|
// 如果已上传到 OSS(不是本地 blob URL),则从 OSS 删除
|
||||||
|
if (attachment.url && !attachment.url.startsWith('blob:')) {
|
||||||
|
try {
|
||||||
|
await chatApi.deleteAttachment(attachment.url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除 OSS 文件失败:', error);
|
||||||
|
// 即使删除失败也继续移除本地引用
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 释放 blob URL(如果是本地的)
|
||||||
|
if (attachment.url.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(attachment.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换功能(深度搜索与联网搜索互斥)
|
// 切换功能(深度搜索与联网搜索互斥)
|
||||||
|
|
@ -470,6 +478,8 @@ function clear() {
|
||||||
defineExpose({
|
defineExpose({
|
||||||
focus,
|
focus,
|
||||||
clear,
|
clear,
|
||||||
|
attachments,
|
||||||
|
removeAttachment,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听文本变化,自动调整高度
|
// 监听文本变化,自动调整高度
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,23 @@ class ChatApi {
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除附件(从 OSS 删除)
|
||||||
|
*/
|
||||||
|
async deleteAttachment(url: string): Promise<void> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/api/chat-ui/attachment?url=${encodeURIComponent(url)}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`删除附件失败: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出单例
|
// 导出单例
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue