From 87efcdd2962d432d79504f0e59d60e9d850be4de Mon Sep 17 00:00:00 2001
From: MT-Fire <798521692@qq.com>
Date: Thu, 5 Mar 2026 10:51:18 +0800
Subject: [PATCH] =?UTF-8?q?feat(oss):=20=E5=AE=8C=E6=88=90=E9=99=84?=
=?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=B5=8B?=
=?UTF-8?q?=E8=AF=95=20doxc=20=E6=96=87=E4=BB=B6=EF=BC=8CGLM=20=E6=88=90?=
=?UTF-8?q?=E5=8A=9F=E8=AF=86=E5=88=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
server/utils/glm_adapter.py | 58 ++++--
server/utils/test_oss_doc_glm.py | 171 ++++++++++++++++++
src/components/chat/ChatHeader.vue | 23 +--
src/components/input/ChatInput.vue | 23 ++-
.../modals/ConversationSettingsModal.vue | 23 ++-
src/services/api.ts | 23 +--
src/stores/settings.ts | 60 ++++--
7 files changed, 312 insertions(+), 69 deletions(-)
create mode 100644 server/utils/test_oss_doc_glm.py
diff --git a/server/utils/glm_adapter.py b/server/utils/glm_adapter.py
index 157c8a9..a4270b7 100644
--- a/server/utils/glm_adapter.py
+++ b/server/utils/glm_adapter.py
@@ -46,27 +46,18 @@ def get_client():
# ── 模型映射 ──────────────────────────────────────────────────────────
-DEFAULT_TEXT_MODEL = "glm-4.5-Air" # glm-4.6 文本统一模型
-DEFAULT_VISION_MODEL = "glm-4.6v"
-
-MODEL_MAP = {
- "qwen-max": "glm-4.5-Air",
- "qwen-plus": "glm-4.5-Air",
- "qwen-turbo": "glm-4.5-Air",
- "qwen-vl-max": "glm-4.5-Air",
- "qwen-vl-plus": "glm-4.5-Air",
-}
+DEFAULT_TEXT_MODEL = "glm-4-flash" # 默认文本模型
+DEFAULT_VISION_MODEL = "glm-4.6v" # 图片/附件识别用 glm-4.6v
def resolve_model(model: str, has_vision: bool = False) -> str:
- if model.startswith("glm"):
- return model
- mapped = MODEL_MAP.get(model, DEFAULT_TEXT_MODEL)
- # 当消息包含图片时,强制使用视觉模型
- if has_vision and mapped != DEFAULT_VISION_MODEL:
- print(f"[GLM] 检测到图片,模型从 {mapped} 切换为 {DEFAULT_VISION_MODEL}")
+ # 当消息包含图片或附件时,使用视觉模型
+ if has_vision:
+ print(f"[GLM] 检测到图片/附件,使用视觉模型:{model} → {DEFAULT_VISION_MODEL}")
return DEFAULT_VISION_MODEL
- return mapped
+ # 普通文本对话,保持原模型不变
+ print(f"[GLM] 使用模型:{model}")
+ return model
# ── 文件上传(含 file_id 缓存)───────────────────────────────────────
@@ -156,6 +147,10 @@ def build_glm_messages(messages: list, files: list | None = None) -> tuple[list,
img_val.get("url", "") if isinstance(img_val, dict) else img_val
)
new_content.append(encode_image(img_src))
+ elif t == "file_url":
+ # file_url 类型(PDF/DOCX/TXT 等文档链接)原样透传
+ has_vision = True
+ new_content.append(item)
else:
new_content.append({"type": "text", "text": str(item)})
glm_messages.append({"role": role, "content": new_content})
@@ -164,7 +159,20 @@ def build_glm_messages(messages: list, files: list | None = None) -> tuple[list,
# 处理独立附件列表
if files:
- doc_exts = {".pdf", ".doc", ".docx", ".xlsx", ".xls", ".pptx", ".ppt"}
+ doc_exts = {
+ ".pdf",
+ ".doc",
+ ".docx",
+ ".xlsx",
+ ".xls",
+ ".pptx",
+ ".ppt",
+ ".txt",
+ ".md",
+ ".csv",
+ ".json",
+ ".log",
+ }
img_exts = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
inserts = []
@@ -172,6 +180,20 @@ def build_glm_messages(messages: list, files: list | None = None) -> tuple[list,
parsed = urlparse(file_url)
filename = parsed.path.split("/")[-1]
suffix = Path(filename).suffix.lower()
+
+ # ── 远程 URL(OSS 等)→ 直接透传 ─────────────────
+ if file_url.startswith(("http://", "https://")):
+ has_vision = True
+ if suffix in img_exts:
+ inserts.append(
+ {"type": "image_url", "image_url": {"url": file_url}}
+ )
+ else:
+ # 文档/文本类统一走 file_url
+ inserts.append({"type": "file_url", "file_url": {"url": file_url}})
+ continue
+
+ # ── 本地文件回退逻辑 ──────────────────────────────
rel = parsed.path.lstrip("/")
local = Path(rel)
diff --git a/server/utils/test_oss_doc_glm.py b/server/utils/test_oss_doc_glm.py
new file mode 100644
index 0000000..6a1d35a
--- /dev/null
+++ b/server/utils/test_oss_doc_glm.py
@@ -0,0 +1,171 @@
+"""
+测试脚本:上传 PDF / DOCX / TXT 文件到阿里云 OSS → 获取 URL → 发送给 GLM-4.6V 识别
+
+支持的文件类型:
+ - .pdf → 上传 OSS 后以 file_url 类型发送 URL 给 GLM
+ - .docx → 上传 OSS 后以 file_url 类型发送 URL 给 GLM
+ - .txt → 上传 OSS 后以 file_url 类型发送 URL 给 GLM
+
+用法:
+ cd server
+ source ~/.bashrc && source .venv/bin/activate
+ python -m utils.test_oss_doc_glm --file <本地文件路径> [--prompt "请总结这份文件"]
+"""
+
+import argparse
+import sys
+from pathlib import Path
+
+# 确保 server 目录在 sys.path
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+from utils.oss_uploader import upload_file
+from utils.glm_adapter import glm_chat_sync
+
+
+# 文件类型分组
+DOC_EXTS = {".pdf", ".doc", ".docx", ".xlsx", ".xls", ".pptx", ".ppt"}
+TXT_EXTS = {".txt", ".md", ".csv", ".json", ".log"}
+IMG_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
+# 所有可通过 file_url 发送的类型
+FILE_URL_EXTS = DOC_EXTS | TXT_EXTS
+
+
+def detect_file_type(suffix: str) -> str:
+ """根据后缀判断文件类别: 'file_url' / 'image' / 'unknown'"""
+ suffix = suffix.lower()
+ if suffix in FILE_URL_EXTS:
+ return "file_url"
+ elif suffix in IMG_EXTS:
+ return "image"
+ return "unknown"
+
+
+def build_messages_for_file_url(file_url: str, prompt: str) -> list:
+ """
+ 为文档/文本文件构建消息。
+ 使用 file_url 类型,直接传递 OSS 的 URL 给 GLM。
+ """
+ return [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "file_url",
+ "file_url": {"url": file_url},
+ },
+ {
+ "type": "text",
+ "text": prompt,
+ },
+ ],
+ }
+ ]
+
+
+def build_messages_for_image(file_url: str, prompt: str) -> list:
+ """为图片文件构建消息,使用 image_url 类型。"""
+ return [
+ {
+ "role": "user",
+ "content": [
+ {"type": "image_url", "image_url": {"url": file_url}},
+ {"type": "text", "text": prompt},
+ ],
+ }
+ ]
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="上传 PDF/DOCX/TXT 文件到 OSS 并让 GLM-4.6V 识别"
+ )
+ parser.add_argument("--file", required=True, help="要上传的本地文件路径")
+ parser.add_argument(
+ "--prompt", default="请总结这份文件的主要内容", help="发给 GLM 的提示词"
+ )
+ parser.add_argument(
+ "--model", default="glm-4.6v", help="GLM 模型名称(默认: glm-4.6v)"
+ )
+ args = parser.parse_args()
+
+ file_path = Path(args.file).resolve()
+ if not file_path.exists():
+ print(f"❌ 文件不存在: {file_path}")
+ sys.exit(1)
+
+ suffix = file_path.suffix.lower()
+ file_type = detect_file_type(suffix)
+ print(f"📂 文件信息:")
+ print(f" 路径: {file_path}")
+ print(f" 后缀: {suffix}")
+ print(f" 类型: {file_type}")
+ print(f" 大小: {file_path.stat().st_size / 1024:.1f} KB")
+ print()
+
+ # ── 第一步:上传文件到 OSS ────────────────────────────────
+ print(f"📤 正在上传文件到阿里云 OSS...")
+ oss_result = upload_file(str(file_path))
+ file_url = oss_result["url"]
+ print(f"✅ OSS 上传成功!")
+ print(f" URL: {file_url}")
+ print(f" ETag: {oss_result['etag']}")
+ print()
+
+ # ── 第二步:根据文件类型构建消息 ──────────────────────────
+ print(f"🔧 正在构建 GLM 消息...")
+
+ if file_type == "file_url":
+ # PDF / DOCX / TXT 等:使用 file_url 类型发送 OSS URL
+ print(f" 策略: 使用 file_url 发送 OSS 链接")
+ messages = build_messages_for_file_url(file_url, args.prompt)
+ elif file_type == "image":
+ # 图片:使用 image_url
+ print(f" 策略: 使用 image_url 发送 OSS 链接")
+ messages = build_messages_for_image(file_url, args.prompt)
+ else:
+ print(f"❌ 不支持的文件类型: {suffix}")
+ print(f" 支持: {', '.join(sorted(FILE_URL_EXTS | IMG_EXTS))}")
+ sys.exit(1)
+
+ print()
+
+ # ── 第三步:发送给 GLM 识别 ──────────────────────────────
+ print(f"🤖 正在请求 GLM ({args.model}) 识别文件...")
+ print(f" 提示词: {args.prompt}")
+ print()
+
+ try:
+ result = glm_chat_sync(
+ messages=messages,
+ model=args.model,
+ temperature=0.7,
+ max_tokens=4096,
+ )
+
+ print("─" * 60)
+ print("📝 GLM 回复:")
+ print("─" * 60)
+ print(result["content"])
+ print("─" * 60)
+
+ if result.get("usage"):
+ usage = result["usage"]
+ print(
+ f"\n📊 Token 用量: 输入 {usage['promptTokens']} | "
+ f"输出 {usage['completionTokens']} | "
+ f"总计 {usage['totalTokens']}"
+ )
+
+ print(f"\n✅ 测试完成! 使用模型: {result.get('model', args.model)}")
+
+ except Exception as e:
+ print(f"\n❌ GLM 请求失败: {e}")
+ import traceback
+
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/components/chat/ChatHeader.vue b/src/components/chat/ChatHeader.vue
index e3afcf6..105001b 100644
--- a/src/components/chat/ChatHeader.vue
+++ b/src/components/chat/ChatHeader.vue
@@ -143,7 +143,6 @@ const props = withDefaults(
showSidebarToggle?: boolean;
isWideMode?: boolean;
isPinned?: boolean;
- currentModelId?: string;
}>(),
{
title: "新对话",
@@ -151,7 +150,6 @@ const props = withDefaults(
showSidebarToggle: true,
isWideMode: true,
isPinned: false,
- currentModelId: "gpt-4",
},
);
@@ -164,7 +162,6 @@ const emit = defineEmits<{
pin: [];
archive: [];
settings: [];
- "select-model": [modelId: string];
"conversation-settings": [];
}>();
@@ -173,25 +170,29 @@ const showMoreMenu = ref(false);
const settingsStore = useSettingsStore();
const currentModel = ref(localStorage.getItem("modelSelect") || "");
+const currentModelId = ref(settingsStore.getSelectedModelId());
const models: any = ref([]);
onMounted(() => {
chatApi.getModels().then((res: any) => {
models.value = res;
- if (!localStorage.getItem("modelSelect")) {
- currentModel.value = models.value[0]["name"] || "";
- localStorage.setItem("modelSelect", currentModel.value);
+ // 初始化模型显示名称
+ const model = models.value?.find((m: any) => m.id === currentModelId.value);
+ if (model) {
+ currentModel.value = model.name;
+ } else if (models.value.length > 0) {
+ currentModel.value = models.value[0].name;
+ currentModelId.value = models.value[0].id;
}
+ localStorage.setItem("modelSelect", currentModel.value);
});
});
function selectModel(modelId: string, modelName: string) {
+ currentModel.value = modelName;
+ currentModelId.value = modelId;
localStorage.setItem("modelSelect", modelName);
- const model = models.value?.find((m: any) => m.id === modelId);
- if (model) {
- currentModel.value = model.name;
- emit("select-model", modelId);
- }
+ settingsStore.setSelectedModelId(modelId); // 更新选中的模型 ID
showModelMenu.value = false;
}
diff --git a/src/components/input/ChatInput.vue b/src/components/input/ChatInput.vue
index 255d3d6..bfbb964 100644
--- a/src/components/input/ChatInput.vue
+++ b/src/components/input/ChatInput.vue
@@ -71,12 +71,13 @@
@@ -151,6 +152,7 @@ import {
Maximize2,
Minimize2,
Brain,
+ Loader2,
} from "@/components/icons";
import AttachmentPreview from "./AttachmentPreview.vue";
import { generateId } from "@/utils/helpers";
@@ -209,11 +211,15 @@ const imageInputRef = ref(null);
// 计算属性
const charCount = computed(() => inputText.value.length);
+const isUploading = computed(() =>
+ attachments.value.some((a) => a.uploading),
+);
const canSend = computed(() => {
return (
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
!props.disabled &&
- charCount.value <= props.maxChars
+ charCount.value <= props.maxChars &&
+ !isUploading.value
);
});
@@ -355,6 +361,9 @@ async function uploadFileToServer(id: string, file: File) {
attachment.uploading = false;
attachment.progress = 100;
}
+
+ // 显示上传成功提示
+ window.$toast && window.$toast('文件上传成功', 'success');
} catch (error) {
console.error('文件上传失败:', error);
@@ -521,6 +530,12 @@ onMounted(() => {
}
}
+ &.loading {
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
+ color: white;
+ cursor: wait;
+ }
+
&:disabled {
cursor: not-allowed;
opacity: 0.6;
diff --git a/src/components/modals/ConversationSettingsModal.vue b/src/components/modals/ConversationSettingsModal.vue
index e53b24f..3658c47 100644
--- a/src/components/modals/ConversationSettingsModal.vue
+++ b/src/components/modals/ConversationSettingsModal.vue
@@ -212,14 +212,20 @@ const { showConversationSettingsModal: visible, settings } =
storeToRefs(settingsStore);
const availableModels: any = ref([]);
const modelSelect = ref(localStorage.getItem("modelSelect") || "");
+const currentModelId = ref(settingsStore.getSelectedModelId());
onMounted(() => {
chatApi.getModels().then((res: any) => {
availableModels.value = res;
- if (!localStorage.getItem("modelSelect")) {
- modelSelect.value = availableModels.value[0]["name"] || "";
- localStorage.setItem("modelSelect", modelSelect.value);
+ // 初始化模型显示名称
+ const model = availableModels.value?.find((m: any) => m.id === currentModelId.value);
+ if (model) {
+ modelSelect.value = model.name;
+ } else if (availableModels.value.length > 0) {
+ modelSelect.value = availableModels.value[0].name;
+ currentModelId.value = availableModels.value[0].id;
}
+ localStorage.setItem("modelSelect", modelSelect.value);
});
});
@@ -273,8 +279,15 @@ const presetPrompts = [
},
];
-function updateSelect(data: any) {
- localStorage.setItem("modelSelect", data);
+function updateSelect(modelName: any) {
+ modelSelect.value = modelName;
+ localStorage.setItem("modelSelect", modelName);
+ // 根据模型名称找到对应的 ID 并更新
+ const model = availableModels.value?.find((m: any) => m.name === modelName);
+ if (model) {
+ currentModelId.value = model.id;
+ settingsStore.setSelectedModelId(model.id);
+ }
}
// 格式化日期
diff --git a/src/services/api.ts b/src/services/api.ts
index 4f7998a..e15aa29 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -108,7 +108,7 @@ class ChatApi {
// 将前端简化的请求翻译为 OpenAI 兼容的规范请求体
const openAiRequest = {
- model: request.model || "qwen-plus", // 可能需要指定支持视觉的模型
+ model: request.model || "glm-4-flash", // 可能需要指定支持视觉的模型
messages: [
{
role: "system",
@@ -248,19 +248,20 @@ class ChatApi {
async getModels(): Promise {
return [
{
- id: "qwen-max",
- name: "通义千问 Max",
+ id: "glm-4.6",
+ name: "智普 GLM-4.6",
description: "最强大的模型",
maxTokens: 8192,
- provider: "Aliyun",
- },
- {
- id: "qwen-plus",
- name: "通义千问 Plus",
- description: "能力均衡",
- maxTokens: 8192,
- provider: "Aliyun",
+ provider: "Zhipu",
},
+ // GLM-4.5,联网搜索功能有问题
+ // {
+ // id: "glm-4.5",
+ // name: "智普 GLM-4.5",
+ // description: "能力均衡",
+ // maxTokens: 8192,
+ // provider: "Zhipu",
+ // },
];
}
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index 512f604..be9b3f5 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -16,7 +16,7 @@ export const useSettingsStore = defineStore('settings', () => {
compactMode: false,
// AI 默认设置
- defaultModel: 'qwen-plus',
+ defaultModel: 'glm-4.6',
defaultTemperature: 0.7,
defaultMaxTokens: 4096,
defaultSystemPrompt: '你是一个有帮助的 AI 助手。',
@@ -34,32 +34,32 @@ export const useSettingsStore = defineStore('settings', () => {
// 可用的 AI 模型
const availableModels: AIModel[] = [
{
- id: 'qwen-max',
- name: '通义千问 Max',
- description: '最强大的模型,适合复杂任务',
- maxTokens: 8192,
- provider: 'Aliyun',
- },
+ id: "glm-4.6",
+ name: "智普 GLM-4.6",
+ description: "最强大的模型",
+ maxTokens: 8192,
+ provider: "Zhipu",
+ },
+ {
+ id: "glm-4.5",
+ name: "智普 GLM-4.5",
+ description: "能力均衡",
+ maxTokens: 8192,
+ provider: "Zhipu",
+ },
{
- id: 'qwen-plus',
- name: '通义千问 Plus',
- description: '能力均衡,更快的响应速度',
- maxTokens: 8192,
- provider: 'Aliyun',
- },
- {
- id: 'qwen-turbo',
- name: '通义千问 Turbo',
+ id: 'glm-4-flash',
+ name: '智普 GLM-4-Flash',
description: '快速高效,适合日常对话',
maxTokens: 8192,
- provider: 'Aliyun',
+ provider: 'Zhipu',
},
{
- id: 'qwen-vl-max',
- name: '通义千问 VL-Max',
+ id: 'glm-4v-plus',
+ name: '智普 GLM-4V-Plus',
description: '强大的视觉理解模型',
maxTokens: 8192,
- provider: 'Aliyun',
+ provider: 'Zhipu',
},
]
@@ -212,6 +212,24 @@ export const useSettingsStore = defineStore('settings', () => {
}
}
+
+ // 存储选中模型 ID 的 localStorage key
+ const MODEL_ID_KEY = 'modelSelectId'
+
+ // 获取当前选择的模型 ID
+ function getSelectedModelId(): string {
+
+ return defaultSettings.defaultModel
+ }
+
+ // 设置当前选择的模型 ID
+ function setSelectedModelId(modelId: string) {
+ localStorage.setItem(MODEL_ID_KEY, modelId)
+ // 同时更新 settings 中的 defaultModel
+ settings.value.defaultModel = modelId
+ saveToStorage()
+ }
+
function loadFromStorage() {
try {
const stored = localStorage.getItem('chat-settings')
@@ -280,5 +298,7 @@ export const useSettingsStore = defineStore('settings', () => {
exportSettings,
importSettings,
loadFromStorage,
+ getSelectedModelId,
+ setSelectedModelId,
}
})
\ No newline at end of file