fix: 修复文件上传,按 OpenAI 兼容接口正确处理不同类型附件
This commit is contained in:
119
public/app.js
119
public/app.js
@@ -452,31 +452,57 @@ function buildUserMessage(message, attachments) {
|
|||||||
return { role: 'user', content: message.content || '' };
|
return { role: 'user', content: message.content || '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentText = attachments
|
|
||||||
.map((file) => `- ${file.name} (${file.type || 'application/octet-stream'}, ${formatBytes(file.size)})`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const promptText = message.content || '请读取并处理这些附件。';
|
const promptText = message.content || '请读取并处理这些附件。';
|
||||||
const content = [
|
const content = [];
|
||||||
{
|
|
||||||
type: 'text',
|
// 先放主文本
|
||||||
text: `${promptText}\n\n附件列表:\n${attachmentText}`,
|
if (promptText) {
|
||||||
},
|
content.push({ type: 'text', text: promptText });
|
||||||
];
|
}
|
||||||
|
|
||||||
|
const nonInlineable = [];
|
||||||
|
|
||||||
attachments.forEach((file) => {
|
attachments.forEach((file) => {
|
||||||
|
if (file.kind === 'image') {
|
||||||
|
// 图片:用 image_url + base64 data URL(OpenAI 标准)
|
||||||
content.push({
|
content.push({
|
||||||
type: 'image_url',
|
type: 'image_url',
|
||||||
image_url: {
|
image_url: { url: file.dataUrl },
|
||||||
url: file.dataUrl,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
} else if (file.kind === 'text') {
|
||||||
|
// 文本类文件:解码 base64,内联为 text block
|
||||||
|
let decoded = '';
|
||||||
|
try {
|
||||||
|
decoded = atob(file.base64);
|
||||||
|
// 尝试 UTF-8 解码(处理多字节字符)
|
||||||
|
decoded = new TextDecoder('utf-8').decode(
|
||||||
|
Uint8Array.from(atob(file.base64), (c) => c.charCodeAt(0))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
decoded = file.base64;
|
||||||
|
}
|
||||||
|
content.push({
|
||||||
|
type: 'text',
|
||||||
|
text: `文件名:${file.name}\n内容:\n\`\`\`\n${decoded}\n\`\`\``,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// PDF / 其他二进制:记录下来,后面统一追加描述
|
||||||
|
nonInlineable.push(file);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
// 不可内联的文件:追加描述性文本
|
||||||
role: 'user',
|
if (nonInlineable.length) {
|
||||||
content,
|
const desc = nonInlineable
|
||||||
};
|
.map((f) => `- ${f.name} (${f.type || 'application/octet-stream'}, ${formatBytes(f.size)})`)
|
||||||
|
.join('\n');
|
||||||
|
content.push({
|
||||||
|
type: 'text',
|
||||||
|
text: `以下附件无法直接内联,仅供参考:\n${desc}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { role: 'user', content };
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractAssistantText(data) {
|
function extractAssistantText(data) {
|
||||||
@@ -732,39 +758,52 @@ function renderMessageContent(container, message) {
|
|||||||
container.appendChild(details);
|
container.appendChild(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主文本
|
// 主文本(marked.js 会自动处理图片)
|
||||||
const images = extractImageDataUrls(mainText);
|
if (mainText) {
|
||||||
const textOnly = removeImageDataUrls(mainText).trim();
|
|
||||||
if (textOnly) {
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerHTML = renderMarkdown(textOnly);
|
div.innerHTML = renderMarkdown(mainText);
|
||||||
container.appendChild(div);
|
// 给 marked 渲染出来的 img 加上样式类
|
||||||
}
|
div.querySelectorAll('img').forEach((img) => {
|
||||||
if (images.length) {
|
|
||||||
const gallery = document.createElement('div');
|
|
||||||
gallery.className = 'message-image-gallery';
|
|
||||||
images.forEach((src) => {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = src;
|
|
||||||
img.className = 'message-inline-image';
|
img.className = 'message-inline-image';
|
||||||
gallery.appendChild(img);
|
|
||||||
});
|
});
|
||||||
container.appendChild(gallery);
|
container.appendChild(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMarkdown(input) {
|
function renderMarkdown(input) {
|
||||||
return marked.parse(String(input || ''), {
|
const renderer = new marked.Renderer();
|
||||||
renderer: (() => {
|
|
||||||
const r = new marked.Renderer();
|
renderer.code = ({ text, lang }) => {
|
||||||
r.code = ({ text, lang }) => {
|
|
||||||
const escaped = text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
const escaped = text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
return `<pre class="md-pre"><code>${escaped}</code></pre>`;
|
return `<pre class="md-pre"><code>${escaped}</code></pre>`;
|
||||||
};
|
};
|
||||||
return r;
|
|
||||||
})(),
|
renderer.image = ({ href, title, text }) => {
|
||||||
breaks: true,
|
if (!href) return '';
|
||||||
});
|
const isVideo = /\.(mp4|webm|ogg|mov)([?#]|$)/i.test(href) || /video/i.test(href);
|
||||||
|
if (isVideo) {
|
||||||
|
return `<video class="message-inline-video" controls playsinline preload="metadata" style="max-width:100%;border-radius:12px;margin-top:8px">
|
||||||
|
<source src="${href}">
|
||||||
|
<a href="${href}" target="_blank">点击查看视频</a>
|
||||||
|
</video>`;
|
||||||
|
}
|
||||||
|
const alt = text || title || '';
|
||||||
|
return `<img class="message-inline-image" src="${href}" alt="${alt}">`;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.link = ({ href, title, text }) => {
|
||||||
|
if (!href) return text;
|
||||||
|
const isVideo = /\.(mp4|webm|ogg|mov)([?#]|$)/i.test(href) || /video/i.test(href);
|
||||||
|
if (isVideo) {
|
||||||
|
return `<video class="message-inline-video" controls playsinline preload="metadata" style="max-width:100%;border-radius:12px;margin-top:8px">
|
||||||
|
<source src="${href}">
|
||||||
|
<a href="${href}" target="_blank">点击查看视频</a>
|
||||||
|
</video>`;
|
||||||
|
}
|
||||||
|
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return marked.parse(String(input || ''), { renderer, breaks: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractImageDataUrls(text) {
|
function extractImageDataUrls(text) {
|
||||||
|
|||||||
Reference in New Issue
Block a user