新增: 跨端图片上传(file.1216.top URL 优先,失败回退 base64,协议对齐)

This commit is contained in:
lxy
2026-08-08 14:19:26 +08:00
parent 4ce6a859e7
commit d19784a414
9 changed files with 247 additions and 42 deletions
+48 -16
View File
@@ -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<SkillInfoLike | null>(null)
// 用户可连续 @ 多个实体,发送时整体透传,后端 resolve 投影成 Augmentation 注入。
const pendingMentionSpans = ref<MentionSpan[]>([])
// —— 图片输入(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<PendingImage[]>([])
/** 选图: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 {
</view>
<template v-for="m in visibleMessages" :key="m.id">
<view v-if="shouldRenderMsg(m)" class="msg" :class="[m.role, m.isError ? 'msg-error' : '']" @longpress="onCopyMessage(m)">
<text v-if="m.role === 'user'" user-select>{{ m.content }}</text>
<view v-if="m.role === 'user'">
<!-- 图片预览(消息带图时) -->
<image v-for="(img, i) in m.images || []" :key="i" :src="img" mode="widthFix" class="msg-user-img" />
<!-- 文本(有文本才显,图片+文本共存;纯图片则只有图,不空白) -->
<text v-if="m.content" user-select>{{ m.content }}</text>
</view>
<template v-else>
<!-- P0-3:错误气泡纯文本渲染(不走 markdown 二次解析,错误串含 `**`/`#` 时不会被解析成格式) -->
<text v-if="m.isError" user-select>{{ m.content }}</text>
@@ -1029,6 +1054,13 @@ function tokenInOf(t: TokenUsage): number {
.msg.user text {
color: #ffffff;
}
/* 用户消息图片预览(2026-08-06):120px 宽 + 圆角,与气泡风格一致;mode=widthFix 高自适应 */
.msg-user-img {
display: block;
width: 120px;
border-radius: 6px;
margin-bottom: 6px;
}
.msg.system text {
color: #999999;
font-size: 12px;