mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 10:28:06 +00:00
fix(webui): 附件消息历史持久化 + 用户文件上传(对齐 qq 插件收文件设计)
1. 附件展示链路补全
- ChatMsg 新增 Attachment 字段(type/url/size/name),channel_output
订阅提取 output_type/url/size 存档——修复刷新后附件变纯文本路径
(如 '/tmp/homeagent.png')的问题
- EventRawInput 订阅支持 upload_* 字段:用户上传的消息也带附件卡片
2. 用户→agent 文件上传(POST /api/v1/chat/file)
设计对齐 qq 插件收文件模式:
- multipart(file + message) 落盘 <data>/uploads/<原文件名>(重名加
毫秒后缀,路径穿越消毒),单文件上限 64MB
- 注入文本「[用户通过 webui 发送了图片: 名字 (大小)] + 保存路径」,
agent 用 files_read 等工具按路径消费
- 回复走与普通消息相同的 SSE 流式管道
- GET /uploads/<name> 下载,与 /files/ 同一鉴权与安全模型
3. 前端
- 输入区 📎 按钮;拖拽到聊天区直接发送;粘贴截图即发送
- 本地预览用 URL.createObjectURL 即时显示
This commit is contained in:
@ -2459,6 +2459,10 @@ background:
|
||||
headers: { "Content-Type": "application/json", ...o?.headers },
|
||||
...o,
|
||||
};
|
||||
// FormData 时不能手动设 Content-Type(浏览器需自动生成 multipart boundary)
|
||||
if (o?.body instanceof FormData) {
|
||||
delete opts.headers["Content-Type"];
|
||||
}
|
||||
var r = await fetch("/api/v1" + p, opts);
|
||||
if (r.status === 401) {
|
||||
location.href = "/login";
|
||||
@ -2500,6 +2504,7 @@ background:
|
||||
if (msgsEl) {
|
||||
msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||
}
|
||||
initChatDragDrop();
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
@ -2780,6 +2785,10 @@ background:
|
||||
html +=
|
||||
"</div>" +
|
||||
'<div class="chat-input-row">' +
|
||||
'<input type="file" id="chat-file" style="display:none" onchange="sendChatFile(this.files[0])">' +
|
||||
'<button class="btn" onclick=\'document.getElementById("chat-file").click()\' id="chat-file-btn" title="' +
|
||||
__("发送文件", "Send file") +
|
||||
'" style="padding:0 12px">📎</button>' +
|
||||
'<input id="chat-input" placeholder="' +
|
||||
__("输入消息...", "Type a message...") +
|
||||
'" onkeydown="if(event.key==\'Enter\')sendChat()">' +
|
||||
@ -3707,6 +3716,73 @@ background:
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
// 上传文件并注入 agent:multipart POST /api/v1/chat/file。
|
||||
// 服务端落盘 uploads/ 后注入「[用户发送了文件: 名字 (大小)] 已保存到 <路径>」;
|
||||
// 回复与普通消息一样走 SSE 流式渲染。可选附言从输入框读取。
|
||||
// 拖拽文件到聊天区即发送(可选:先在输入框写附言)
|
||||
function initChatDragDrop() {
|
||||
var panel = document.getElementById("chat-panel-chat");
|
||||
if (!panel || panel.__dnd) return;
|
||||
panel.__dnd = true;
|
||||
panel.addEventListener("dragover", function (e) {
|
||||
e.preventDefault();
|
||||
panel.style.outline = "2px dashed var(--accent, #4a90d9)";
|
||||
});
|
||||
panel.addEventListener("dragleave", function () {
|
||||
panel.style.outline = "";
|
||||
});
|
||||
panel.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
panel.style.outline = "";
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length) {
|
||||
sendChatFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
// 粘贴截图/复制的文件直接发送
|
||||
document.addEventListener("paste", function (e) {
|
||||
var chatVisible =
|
||||
document.getElementById("chat-panel-chat") &&
|
||||
document.getElementById("chat-input");
|
||||
if (!chatVisible) return;
|
||||
var items = e.clipboardData && e.clipboardData.files;
|
||||
if (items && items.length && document.activeElement !== null) {
|
||||
sendChatFile(items[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function sendChatFile(fileObj, extraText) {
|
||||
if (!fileObj || state.chatLoading) return;
|
||||
var inp = document.getElementById("chat-input");
|
||||
var message = (extraText || inp.value || "").trim();
|
||||
if (inp) inp.value = "";
|
||||
var fd = new FormData();
|
||||
fd.append("file", fileObj);
|
||||
if (message) fd.append("message", message);
|
||||
state.messages.push({
|
||||
role: "user",
|
||||
content: message,
|
||||
attachment: {
|
||||
type: /^image\//.test(fileObj.type) ? "image" : "file",
|
||||
url: URL.createObjectURL(fileObj),
|
||||
name: fileObj.name,
|
||||
size: fileObj.size,
|
||||
},
|
||||
});
|
||||
rerenderChat(true);
|
||||
state.chatLoading = true;
|
||||
state.chatStage = __("等待AI回复...", "Waiting for AI...");
|
||||
rerenderChat(true);
|
||||
try {
|
||||
await api("/chat/file", { method: "POST", body: fd, rawBody: true });
|
||||
// 回复经 SSE 流式到达,这里无需处理响应体
|
||||
} catch (e) {
|
||||
toast(__("文件发送失败:" + e.message, "File send failed: " + e.message), true);
|
||||
state.chatLoading = false;
|
||||
endChatTurn();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendChat() {
|
||||
var inp = document.getElementById("chat-input");
|
||||
var btn = document.getElementById("chat-send-btn");
|
||||
|
||||
Reference in New Issue
Block a user