From d19784a414b18ffa6938e76366071e965e79b4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sat, 8 Aug 2026 14:19:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E8=B7=A8=E7=AB=AF?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0(file.1216.top=20URL=20?= =?UTF-8?q?=E4=BC=98=E5=85=88,=E5=A4=B1=E8=B4=A5=E5=9B=9E=E9=80=80=20base6?= =?UTF-8?q?4,=E5=8D=8F=E8=AE=AE=E5=AF=B9=E9=BD=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/df-miniapp/src/composables/useAiChat.ts | 6 + apps/df-miniapp/src/config.ts | 6 + apps/df-miniapp/src/pages/chat/index.vue | 64 ++++++++--- apps/df-miniapp/src/types/events.ts | 2 + apps/df-miniapp/src/types/relay.ts | 18 +-- apps/df-miniapp/src/utils/fileUpload.ts | 34 ++++++ src/components/ai/ChatInput.vue | 109 ++++++++++++++++--- src/components/ai/ImageInput.vue | 12 +- src/composables/ai/fileUpload.ts | 38 +++++++ 9 files changed, 247 insertions(+), 42 deletions(-) create mode 100644 apps/df-miniapp/src/utils/fileUpload.ts create mode 100644 src/composables/ai/fileUpload.ts diff --git a/apps/df-miniapp/src/composables/useAiChat.ts b/apps/df-miniapp/src/composables/useAiChat.ts index f0b4797..1b2da92 100644 --- a/apps/df-miniapp/src/composables/useAiChat.ts +++ b/apps/df-miniapp/src/composables/useAiChat.ts @@ -988,10 +988,16 @@ function send(text: string, skill?: string | null, spans?: MentionSpan[] | null, maxRoundsActive.value = false // P1-E:新 generation 清 MaxRounds 挂起 // 本地先 push user 气泡(乐观渲染,对齐桌面 sendMessage) + // images:从 parts 提取图片展示(优先 url,回退 base64 dataURL),避免 content 空时空白气泡。 + const userImages = (parts || []) + .filter((p) => p.type === 'image') + .map((p) => (p.type === 'image' ? (p.url || (p.base64 ? `data:${p.media_type};base64,${p.base64}` : '')) : '')) + .filter((s) => !!s) messages.value.push({ id: genId('user'), role: 'user', content: trimmed, + ...(userImages.length ? { images: userImages } : {}), timestamp: Date.now(), }); triggerRef(messages) diff --git a/apps/df-miniapp/src/config.ts b/apps/df-miniapp/src/config.ts index fc3aa98..a1766fa 100644 --- a/apps/df-miniapp/src/config.ts +++ b/apps/df-miniapp/src/config.ts @@ -20,6 +20,10 @@ export interface MiniappConfig { reconnectBaseDelay: number /** 重连最大间隔(ms) */ reconnectMaxDelay: number + /** file.1216.top 文件服务上传地址(不带路径,上传时拼 /upload) */ + fileUploadBaseUrl: string + /** 上传 X-Source 头(项目隔离,文件存到 file.1216.top 对应目录) */ + fileUploadSource: string } /** @@ -40,6 +44,8 @@ export const defaultConfig: MiniappConfig = { heartbeatInterval: 30000, reconnectBaseDelay: 1000, reconnectMaxDelay: 30000, + fileUploadBaseUrl: 'https://file.1216.top', + fileUploadSource: 'devflow', } /** storage key(持久化完整 MiniappConfig JSON) */ diff --git a/apps/df-miniapp/src/pages/chat/index.vue b/apps/df-miniapp/src/pages/chat/index.vue index 3e78727..6189836 100644 --- a/apps/df-miniapp/src/pages/chat/index.vue +++ b/apps/df-miniapp/src/pages/chat/index.vue @@ -2,6 +2,7 @@ import { computed, ref, watch } from 'vue' import { marked } from 'marked' import { styleMarkdown } from '@/utils/mdRenderer' +import { uploadImage } from '@/utils/fileUpload' import { useAiChat } from '@/composables/useAiChat' import type { MentionSpan, MiniContentPart } from '@/types/relay' import type { ProjectRecord, TaskRecord, IdeaRecord, AiToolCallInfo, ChatMessage, TokenUsage } from '@/types/events' @@ -239,15 +240,27 @@ const pendingSkill = ref(null) // 用户可连续 @ 多个实体,发送时整体透传,后端 resolve 投影成 Augmentation 注入。 const pendingMentionSpans = ref([]) -// —— 图片输入(2026-08-05):选图转 base64,发送时透传 ai.send parts 参数 —— -// 存临时文件路径(缩略图预览) + ContentPart(发送),对齐后端 df-ai-core ContentPart serde externally tagged。 +// —— 图片输入(2026-08-05):选图后上传 file.1216.top 拿 URL / 失败回退 base64,发送时透传 ai.send parts 参数 —— +// 存临时文件路径(缩略图预览) + ContentPart(发送),对齐后端 df-ai-core ContentPart serde 内部标签 tag=type。 interface PendingImage { tempPath: string part: MiniContentPart } const pendingImages = ref([]) -/** 选图:uni.chooseMedia(compressed 压缩小图)→ base64 → 入 pendingImages。 */ +/** 临时文件扩展名 → MIME 映射(fileType 是类别 'image' 非 MIME,须按扩展名推断) */ +function toMime(file: { tempFilePath?: string; fileType?: string }): string { + const ft = file.fileType + if (ft && /^image\//i.test(ft)) return ft // 真 MIME 优先 + const p = file.tempFilePath || '' + if (/\.png$/i.test(p)) return 'image/png' + if (/\.(jpe?g)$/i.test(p)) return 'image/jpeg' + if (/\.webp$/i.test(p)) return 'image/webp' + if (/\.gif$/i.test(p)) return 'image/gif' + return 'image/jpeg' +} + +/** 选图:uni.chooseMedia(compressed)→ 上传 file.1216.top 拿 URL;失败回退 base64。 */ function onPickImage(): void { uni.chooseMedia({ count: 1, @@ -256,19 +269,26 @@ function onPickImage(): void { success: (res) => { const file = res.tempFiles && res.tempFiles[0] if (!file) return - uni.getFileSystemManager().readFile({ - filePath: file.tempFilePath, - encoding: 'base64', - success: (r) => { - const b64 = r.data as string - if (!b64) return - pendingImages.value.push({ - tempPath: file.tempFilePath, - part: { Image: { base64: b64, media_type: file.fileType || 'image/jpeg' } }, + const mt = toMime(file) + // 先尝试上传 file.1216.top → URL 引用(公开可达,provider 直传) + uploadImage(file.tempFilePath) + .then(({ url }) => { + pendingImages.value.push({ tempPath: file.tempFilePath, part: { type: 'image', url, media_type: mt } }) + }) + .catch(() => { + // 上传失败回退 base64(Phase 0 修复后 base64 可真正到达 provider,不阻断) + uni.getFileSystemManager().readFile({ + filePath: file.tempFilePath, + encoding: 'base64', + success: (r) => { + const b64 = r.data as string + if (!b64) return + pendingImages.value.push({ tempPath: file.tempFilePath, part: { type: 'image', base64: b64, media_type: mt } }) + }, + fail: () => uni.showToast({ title: '图片读取失败', icon: 'none' }), }) - }, - fail: () => uni.showToast({ title: '图片读取失败', icon: 'none' }), - }) + uni.showToast({ title: '上传失败,已改用本地直传', icon: 'none' }) + }) }, }) } @@ -748,7 +768,12 @@ function tokenInOf(t: TokenUsage): number {