mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
perf(webui): /chat/history 分段懒加载 + lean 瘦身模式
问题:/api/v1/chat/history 无分页返回全量 1.26MB(200条),移动端/弱网首屏很慢。 实测体积构成:tool_calls args/result 占 71.8%,reasoning_content 11.6%,content 仅 3.4%。 后端 handler.go: - 新增查询参数 limit(1..maxChatHistory)/ before(游标)/ lean(瘦身) - 全部可选,省略时返回全量 → 向后兼容旧客户端 - 响应加 total/offset/has_more,供前端判断能否继续向上加载 - lean=1 裁剪 tool_calls 的 args/result 并置 truncated 标记、省略 reasoning_content - ChatToolCall 新增 Truncated 字段(前端可显示'详情需展开加载'而非误判执行失败) 前端 WebUI + GUI(两端同步): - 首屏只拉最新 40 条(CHAT_PAGE_SIZE),1.26MB → 48.5KB(lean 26KB) - 新增 loadOlderChat():滚动触顶(<60px)自动拉上一页,插入后按 scrollHeight 差值补偿 滚动位置避免视口跳动 - state 新增 chatOffset/chatTotal/chatHasMore - syncChatFromHistory 改用重叠区对齐(分段后不能再用长度比较判断新增), 找不到重叠点安全退化为全量刷新最新页 实测:无参数 1287.9KB / limit=40 48.5KB / limit=40&lean=1 26.0KB; before 游标翻页、limit 越界/非法值、before=0 等边界均正确
This commit is contained in:
@ -44,6 +44,9 @@ const state = {
|
|||||||
pendingTools: [],
|
pendingTools: [],
|
||||||
eventSource: null,
|
eventSource: null,
|
||||||
chatFinalIdx: -1,
|
chatFinalIdx: -1,
|
||||||
|
chatOffset: 0, // 分段历史:当前已加载消息在服务端全量中的起始下标
|
||||||
|
chatTotal: 0, // 服务端历史总条数
|
||||||
|
chatHasMore: false, // 是否还有更早历史可向上加载
|
||||||
sseLastEventID: "", // 最近一次 SSE 事件 id,断线重连时随 Last-Event-ID 头回传
|
sseLastEventID: "", // 最近一次 SSE 事件 id,断线重连时随 Last-Event-ID 头回传
|
||||||
lang: localStorage.getItem("ha-lang") || "zh",
|
lang: localStorage.getItem("ha-lang") || "zh",
|
||||||
connections: [],
|
connections: [],
|
||||||
@ -1340,6 +1343,10 @@ function renderChat() {
|
|||||||
() => {
|
() => {
|
||||||
state.chatStick =
|
state.chatStick =
|
||||||
msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight < 80;
|
msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight < 80;
|
||||||
|
// 触顶(近顶部 60px)且服务端还有更早历史 → 向上懒加载下一页
|
||||||
|
if (msgsEl.scrollTop < 60 && state.chatHasMore) {
|
||||||
|
loadOlderChat();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
@ -2351,13 +2358,26 @@ function switchChatPanel(tab, el) {
|
|||||||
if (tab === "knowledge") searchKnowledgeChat();
|
if (tab === "knowledge") searchKnowledgeChat();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首屏分段加载条数:只拉最新 N 条,向上滚动触顶时再拉更早的。
|
||||||
|
var CHAT_PAGE_SIZE = 40;
|
||||||
|
|
||||||
async function loadChatHistory() {
|
async function loadChatHistory() {
|
||||||
try {
|
try {
|
||||||
var data = await api("/chat/history");
|
var data = await api("/chat/history?limit=" + CHAT_PAGE_SIZE);
|
||||||
if (data && data.messages) {
|
if (data && data.messages) {
|
||||||
state.messages = data.messages;
|
state.messages = data.messages;
|
||||||
|
state.chatOffset = typeof data.offset === "number" ? data.offset : 0;
|
||||||
|
state.chatTotal =
|
||||||
|
typeof data.total === "number" ? data.total : data.messages.length;
|
||||||
|
state.chatHasMore = !!data.has_more;
|
||||||
if (window.homeagent && window.homeagent.log)
|
if (window.homeagent && window.homeagent.log)
|
||||||
window.homeagent.log("history: loaded " + data.messages.length);
|
window.homeagent.log(
|
||||||
|
"history: loaded " +
|
||||||
|
data.messages.length +
|
||||||
|
"/" +
|
||||||
|
state.chatTotal +
|
||||||
|
(state.chatHasMore ? " (has more)" : ""),
|
||||||
|
);
|
||||||
} else if (window.homeagent && window.homeagent.log) {
|
} else if (window.homeagent && window.homeagent.log) {
|
||||||
window.homeagent.log("history: no messages field");
|
window.homeagent.log("history: no messages field");
|
||||||
}
|
}
|
||||||
@ -2367,35 +2387,101 @@ async function loadChatHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadOlderChat 向上翻页:拉 offset 之前的一页,前置到 messages 头部。
|
||||||
|
// 保持滚动位置补偿,避免视口跳动。
|
||||||
|
var _loadingOlder = false;
|
||||||
|
async function loadOlderChat() {
|
||||||
|
if (_loadingOlder || !state.chatHasMore) return;
|
||||||
|
_loadingOlder = true;
|
||||||
|
var msgsEl = document.getElementById("chat-msgs");
|
||||||
|
var prevH = msgsEl ? msgsEl.scrollHeight : 0;
|
||||||
|
var prevTop = msgsEl ? msgsEl.scrollTop : 0;
|
||||||
|
try {
|
||||||
|
var before = state.chatOffset || 0;
|
||||||
|
if (before <= 0) {
|
||||||
|
state.chatHasMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = await api(
|
||||||
|
"/chat/history?limit=" + CHAT_PAGE_SIZE + "&before=" + before,
|
||||||
|
);
|
||||||
|
if (data && data.messages && data.messages.length) {
|
||||||
|
state.messages = data.messages.concat(state.messages);
|
||||||
|
state.chatOffset =
|
||||||
|
typeof data.offset === "number" ? data.offset : 0;
|
||||||
|
state.chatHasMore = !!data.has_more;
|
||||||
|
state.chatStick = false;
|
||||||
|
rerenderChatIfActive();
|
||||||
|
if (msgsEl) {
|
||||||
|
msgsEl.scrollTop = prevTop + (msgsEl.scrollHeight - prevH);
|
||||||
|
}
|
||||||
|
if (window.homeagent && window.homeagent.log)
|
||||||
|
window.homeagent.log(
|
||||||
|
"history: older " + data.messages.length + " (offset=" + state.chatOffset + ")",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
state.chatHasMore = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
_loadingOlder = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// syncChatFromHistory 增量同步:对比服务端历史,仅追加新消息 DOM 节点,
|
// syncChatFromHistory 增量同步:对比服务端历史,仅追加新消息 DOM 节点,
|
||||||
// 不重建已有消息 → 无闪烁。用于 SSE 断连恢复期间的轮询兜底(跨渠道消息补偿)。
|
// 不重建已有消息 → 无闪烁。用于 SSE 断连恢复期间的轮询兜底(跨渠道消息补偿)。
|
||||||
function syncChatFromHistory() {
|
function syncChatFromHistory() {
|
||||||
return api("/chat/history").then(function (data) {
|
return api("/chat/history?limit=" + CHAT_PAGE_SIZE)
|
||||||
if (!data || !data.messages || data.messages.length === 0) return;
|
.then(function (data) {
|
||||||
var serverMsgs = data.messages;
|
if (!data || !data.messages || data.messages.length === 0) return;
|
||||||
var localMsgs = state.messages;
|
var serverMsgs = data.messages;
|
||||||
if (localMsgs.length === 0) {
|
var localMsgs = state.messages;
|
||||||
state.messages = serverMsgs;
|
// 首次加载(空列表)→ 全量赋值
|
||||||
rerenderChatIfActive();
|
if (localMsgs.length === 0) {
|
||||||
return;
|
state.messages = serverMsgs;
|
||||||
}
|
state.chatOffset = typeof data.offset === "number" ? data.offset : 0;
|
||||||
if (serverMsgs.length <= localMsgs.length) {
|
state.chatHasMore = !!data.has_more;
|
||||||
|
rerenderChatIfActive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 分段拉取只回传最新页,本地可能已向上翻页加载更多,
|
||||||
|
// 因此不能用长度比较,改用末尾内容比对 + 重叠区对齐。
|
||||||
var lastLocal = localMsgs[localMsgs.length - 1];
|
var lastLocal = localMsgs[localMsgs.length - 1];
|
||||||
var lastServer = serverMsgs[serverMsgs.length - 1];
|
var lastServer = serverMsgs[serverMsgs.length - 1];
|
||||||
var localContent = lastLocal.content || lastLocal.Content || "";
|
var localContent = lastLocal.content || lastLocal.Content || "";
|
||||||
var serverContent = lastServer.content || lastServer.Content || "";
|
var serverContent = lastServer.content || lastServer.Content || "";
|
||||||
if (lastServer.role === "assistant" && localContent !== serverContent && serverContent) {
|
// 末尾一致 → 无新增
|
||||||
lastLocal.content = serverContent;
|
if (localContent === serverContent) return;
|
||||||
if (lastServer.ReasoningContent) lastLocal.reasoning_content = lastServer.ReasoningContent;
|
// 寻找重叠点(本地末尾 k 条在服务端页中的位置)
|
||||||
|
var overlap = -1;
|
||||||
|
var maxK = Math.min(3, serverMsgs.length - 1, localMsgs.length - 1);
|
||||||
|
for (var k = maxK; k >= 1; k--) {
|
||||||
|
var sMsg = serverMsgs[serverMsgs.length - 1 - k];
|
||||||
|
var lMsg = localMsgs[localMsgs.length - 1 - k];
|
||||||
|
if (
|
||||||
|
lMsg &&
|
||||||
|
sMsg &&
|
||||||
|
(lMsg.content || "") === (sMsg.content || "") &&
|
||||||
|
lMsg.role === sMsg.role
|
||||||
|
) {
|
||||||
|
overlap = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overlap >= 0) {
|
||||||
|
var newMsgs = serverMsgs.slice(serverMsgs.length - overlap);
|
||||||
|
if (newMsgs.length === 0) return;
|
||||||
|
Array.prototype.push.apply(state.messages, newMsgs);
|
||||||
|
rerenderChatIfActive();
|
||||||
|
} else {
|
||||||
|
// 无重叠点(本地领先过多/已失同步)→ 安全退化为全量刷新最新页
|
||||||
|
state.messages = serverMsgs;
|
||||||
|
state.chatOffset = typeof data.offset === "number" ? data.offset : 0;
|
||||||
|
state.chatHasMore = !!data.has_more;
|
||||||
rerenderChatIfActive();
|
rerenderChatIfActive();
|
||||||
}
|
}
|
||||||
return;
|
})
|
||||||
}
|
.catch(function () {});
|
||||||
// 服务端消息更多 → 仅追加新消息对象(不重建已有 DOM)
|
|
||||||
var newMsgs = serverMsgs.slice(localMsgs.length);
|
|
||||||
Array.prototype.push.apply(state.messages, newMsgs);
|
|
||||||
rerenderChatIfActive();
|
|
||||||
}).catch(function () {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTerminals() {
|
async function loadTerminals() {
|
||||||
|
|||||||
@ -2258,6 +2258,9 @@
|
|||||||
pendingTools: [],
|
pendingTools: [],
|
||||||
eventSource: null,
|
eventSource: null,
|
||||||
chatFinalIdx: -1,
|
chatFinalIdx: -1,
|
||||||
|
chatOffset: 0, // 分段历史:当前已加载消息在服务端全量中的起始下标
|
||||||
|
chatTotal: 0, // 服务端历史总条数
|
||||||
|
chatHasMore: false, // 是否还有更早历史可向上加载
|
||||||
lang: localStorage.getItem("ha-lang") || "zh",
|
lang: localStorage.getItem("ha-lang") || "zh",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2996,6 +2999,10 @@
|
|||||||
state.chatStick =
|
state.chatStick =
|
||||||
msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight <
|
msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight <
|
||||||
80;
|
80;
|
||||||
|
// 触顶(近顶部 60px)且服务端还有更早历史 → 向上懒加载下一页
|
||||||
|
if (msgsEl.scrollTop < 60 && state.chatHasMore) {
|
||||||
|
loadOlderChat();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
@ -4199,47 +4206,116 @@
|
|||||||
if (tab === "context") queryMemoryContext();
|
if (tab === "context") queryMemoryContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首屏分段加载条数:只拉最新 N 条,向上滚动触顶时再拉更早的。
|
||||||
|
var CHAT_PAGE_SIZE = 40;
|
||||||
async function loadChatHistory() {
|
async function loadChatHistory() {
|
||||||
try {
|
try {
|
||||||
var data = await api("/chat/history");
|
var data = await api("/chat/history?limit=" + CHAT_PAGE_SIZE);
|
||||||
if (data && data.messages) state.messages = data.messages;
|
if (data && data.messages) {
|
||||||
|
state.messages = data.messages;
|
||||||
|
state.chatOffset = typeof data.offset === "number" ? data.offset : 0;
|
||||||
|
state.chatTotal = typeof data.total === "number" ? data.total : data.messages.length;
|
||||||
|
state.chatHasMore = !!data.has_more;
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadOlderChat 向上翻页:拉 offset 之前的一页,前置到 messages 头部。
|
||||||
|
// 保持滚动位置(插入前后 scrollHeight 差值补偿),避免视口跳动。
|
||||||
|
var _loadingOlder = false;
|
||||||
|
async function loadOlderChat() {
|
||||||
|
if (_loadingOlder || !state.chatHasMore) return;
|
||||||
|
_loadingOlder = true;
|
||||||
|
var msgsEl = document.getElementById("chat-msgs");
|
||||||
|
var prevH = msgsEl ? msgsEl.scrollHeight : 0;
|
||||||
|
var prevTop = msgsEl ? msgsEl.scrollTop : 0;
|
||||||
|
try {
|
||||||
|
var before = state.chatOffset || 0;
|
||||||
|
if (before <= 0) {
|
||||||
|
state.chatHasMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = await api(
|
||||||
|
"/chat/history?limit=" + CHAT_PAGE_SIZE + "&before=" + before,
|
||||||
|
);
|
||||||
|
if (data && data.messages && data.messages.length) {
|
||||||
|
state.messages = data.messages.concat(state.messages);
|
||||||
|
state.chatOffset = typeof data.offset === "number" ? data.offset : 0;
|
||||||
|
state.chatHasMore = !!data.has_more;
|
||||||
|
state.chatStick = false; // 向上翻页时不自动粘底
|
||||||
|
renderChat();
|
||||||
|
if (msgsEl) {
|
||||||
|
// 补偿新增内容高度,保持用户视觉位置不变
|
||||||
|
msgsEl.scrollTop = prevTop + (msgsEl.scrollHeight - prevH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.chatHasMore = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
_loadingOlder = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// syncChatFromHistory 增量同步:对比服务端历史,仅追加新消息 DOM 节点,
|
// syncChatFromHistory 增量同步:对比服务端历史,仅追加新消息 DOM 节点,
|
||||||
// 不重建已有消息 → 无闪烁。用于 SSE 断连恢复期间的轮询兜底。
|
// 不重建已有消息 → 无闪烁。用于 SSE 断连恢复期间的轮询兜底。
|
||||||
function syncChatFromHistory() {
|
function syncChatFromHistory() {
|
||||||
return api("/chat/history").then(function (data) {
|
return api("/chat/history?limit=" + CHAT_PAGE_SIZE).then(function (data) {
|
||||||
if (!data || !data.messages || data.messages.length === 0) return;
|
if (!data || !data.messages || data.messages.length === 0) return;
|
||||||
var serverMsgs = data.messages;
|
var serverMsgs = data.messages;
|
||||||
var localMsgs = state.messages;
|
var localMsgs = state.messages;
|
||||||
// 空历史 → 全量加载(首次同步)
|
// 首次加载(空列表)→ 全量赋值
|
||||||
if (localMsgs.length === 0) {
|
if (localMsgs.length === 0) {
|
||||||
state.messages = serverMsgs;
|
state.messages = serverMsgs;
|
||||||
|
state.chatOffset = typeof data.offset === "number" ? data.offset : 0;
|
||||||
|
state.chatHasMore = !!data.has_more;
|
||||||
rerenderChat(true);
|
rerenderChat(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 无新增消息 → 检查最后一条是否被改写
|
// 以「本地末尾 vs 服务端末尾」做依据。分段拉取只会回传最新页,
|
||||||
if (serverMsgs.length <= localMsgs.length) {
|
// 本地已向上翻页加载了更早内容,长度上本地可能 ≥ 服务端。
|
||||||
var lastLocal = localMsgs[localMsgs.length - 1];
|
var lastLocal = localMsgs[localMsgs.length - 1];
|
||||||
var lastServer = serverMsgs[serverMsgs.length - 1];
|
var lastServer = serverMsgs[serverMsgs.length - 1];
|
||||||
var localContent = lastLocal.content || lastLocal.Content || "";
|
var localContent = lastLocal.content || lastLocal.Content || "";
|
||||||
var serverContent = lastServer.content || lastServer.Content || "";
|
var serverContent = lastServer.content || lastServer.Content || "";
|
||||||
|
// 服务端末尾和本地末尾内容相同 → 无新增(可能裁剪过的轻量项因不含 tool_calls args/result,
|
||||||
|
// 依赖 content+role+time 足以识别)
|
||||||
|
if (localContent === serverContent) {
|
||||||
|
// 检查是否本地末尾被服务端改写(如 stage 插件改写最终文本)
|
||||||
if (lastServer.role === "assistant" && localContent !== serverContent && serverContent) {
|
if (lastServer.role === "assistant" && localContent !== serverContent && serverContent) {
|
||||||
lastLocal.content = serverContent;
|
lastLocal.content = serverContent;
|
||||||
if (lastServer.ReasoningContent) lastLocal.reasoning_content = lastServer.ReasoningContent;
|
if (lastServer.ReasoningContent) lastLocal.reasoning_content = lastServer.ReasoningContent;
|
||||||
// 仅更新最后一条消息 DOM,不全量重建
|
var msgsEl0 = document.getElementById("chat-msgs");
|
||||||
var msgsEl = document.getElementById("chat-msgs");
|
if (msgsEl0 && msgsEl0.lastElementChild) {
|
||||||
if (msgsEl && msgsEl.lastElementChild) {
|
var textEl0 = msgsEl0.lastElementChild.querySelector(".msg-bubble .text");
|
||||||
var el = msgsEl.lastElementChild;
|
if (textEl0) textEl0.innerHTML = renderMd(serverContent);
|
||||||
var textEl = el.querySelector(".msg-bubble .text");
|
|
||||||
if (textEl) textEl.innerHTML = renderMd(serverContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 有新增消息:追加到 state.messages + DOM(仅追加节点,不触碰已有)
|
// 有新增消息:服务端末尾与本地末尾不同 → 找出本地新增的差异,追加到 DOM。
|
||||||
var newMsgs = serverMsgs.slice(localMsgs.length);
|
// 服务端只回传最新页,我们需在 localMsgs 末尾取交集起点(本地末尾在服务端页的位置)
|
||||||
|
// 简化:取服务端末尾3条与本地末尾3条比对,找到重含点的下标;若找不到全量替换最新页。
|
||||||
|
var overlap = -1;
|
||||||
|
for (var k = Math.min(3, serverMsgs.length - 1, localMsgs.length - 1); k >= 1; k--) {
|
||||||
|
var sMsg = serverMsgs[serverMsgs.length - 1 - k];
|
||||||
|
var lMsg = localMsgs[localMsgs.length - 1 - k];
|
||||||
|
if (lMsg && (lMsg.content || "") === (sMsg.content || "") && lMsg.role === sMsg.role) {
|
||||||
|
overlap = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var newMsgs;
|
||||||
|
if (overlap >= 0) {
|
||||||
|
// 本地末尾 overlap 条在服务端页里,之后的部分是新增
|
||||||
|
newMsgs = serverMsgs.slice(serverMsgs.length - overlap);
|
||||||
|
if (newMsgs.length === 0) return; // 无新增(内容改写走上面的分支)
|
||||||
|
} else {
|
||||||
|
// 找不到重含点(本地领先太多,超出服务端页)→ 无法精确差异,安全退化为全量刷新
|
||||||
|
state.messages = serverMsgs;
|
||||||
|
rerenderChat(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var msgsEl = document.getElementById("chat-msgs");
|
var msgsEl = document.getElementById("chat-msgs");
|
||||||
if (msgsEl) {
|
if (msgsEl) {
|
||||||
var aiAvatar = '<img src="/mascot.webp" alt="小宅">';
|
var aiAvatar = '<img src="/mascot.webp" alt="小宅">';
|
||||||
|
|||||||
@ -174,6 +174,9 @@ type ChatToolCall struct {
|
|||||||
Result interface{} `json:"result,omitempty"`
|
Result interface{} `json:"result,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
Plugin string `json:"plugin,omitempty"`
|
Plugin string `json:"plugin,omitempty"`
|
||||||
|
// Truncated 标记本条工具调用的 args/result 已被服务端裁剪(lean 模式),
|
||||||
|
// 前端可据此显示「详情需展开加载」而不是把空结果当成执行失败。
|
||||||
|
Truncated bool `json:"truncated,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CmdExec struct {
|
type CmdExec struct {
|
||||||
@ -1382,12 +1385,87 @@ func (h *Handler) addChatMsg(msg ChatMsg) {
|
|||||||
h.chatMu.Unlock()
|
h.chatMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleChatHistory 返回对话历史,支持分段懒加载。
|
||||||
|
//
|
||||||
|
// 查询参数(全部可选,省略时保持旧行为=返回全量,向后兼容旧客户端):
|
||||||
|
// - limit: 返回条数上限(1..maxChatHistory)。带 limit 时默认取「最新的 limit 条」。
|
||||||
|
// - before: 游标,只返回下标 < before 的消息(配合 limit 向上翻页取更早历史)。
|
||||||
|
// - lean: "1"/"true" 时裁剪重负载字段(tool_calls 的 args/result 置空、reasoning_content 省略),
|
||||||
|
// 体积可降约 80%(实测 tool_calls 占 ~72%、reasoning ~12%)。用于移动端/弱网首屏。
|
||||||
|
//
|
||||||
|
// 响应额外返回 total / offset / has_more,供前端判断是否继续向上加载。
|
||||||
func (h *Handler) handleChatHistory(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleChatHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
limit := parseIntDefault(q.Get("limit"), 0)
|
||||||
|
if limit < 0 {
|
||||||
|
limit = 0
|
||||||
|
}
|
||||||
|
if limit > maxChatHistory {
|
||||||
|
limit = maxChatHistory
|
||||||
|
}
|
||||||
|
lean := q.Get("lean") == "1" || strings.EqualFold(q.Get("lean"), "true")
|
||||||
|
|
||||||
h.chatMu.Lock()
|
h.chatMu.Lock()
|
||||||
result := make([]ChatMsg, len(h.chatHistory))
|
total := len(h.chatHistory)
|
||||||
copy(result, h.chatHistory)
|
// before 游标:默认取到末尾(最新)
|
||||||
|
end := parseIntDefault(q.Get("before"), total)
|
||||||
|
if end < 0 || end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
start := 0
|
||||||
|
if limit > 0 && end-limit > 0 {
|
||||||
|
start = end - limit
|
||||||
|
}
|
||||||
|
result := make([]ChatMsg, end-start)
|
||||||
|
copy(result, h.chatHistory[start:end])
|
||||||
h.chatMu.Unlock()
|
h.chatMu.Unlock()
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"messages": result})
|
|
||||||
|
if lean {
|
||||||
|
result = leanChatMsgs(result)
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"messages": result,
|
||||||
|
"total": total,
|
||||||
|
"offset": start,
|
||||||
|
"has_more": start > 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIntDefault 解析十进制整数,失败/空串返回 def。
|
||||||
|
func parseIntDefault(s string, def int) int {
|
||||||
|
if s == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// leanChatMsgs 裁剪重负载字段用于移动端/弱网首屏:
|
||||||
|
// tool_calls 只保留 tool/name/status/plugin(args/result 置 nil 并标记 truncated),
|
||||||
|
// reasoning_content 整体省略。前端需要完整内容时按 before/limit 拉非 lean 分段。
|
||||||
|
func leanChatMsgs(in []ChatMsg) []ChatMsg {
|
||||||
|
out := make([]ChatMsg, len(in))
|
||||||
|
for i, m := range in {
|
||||||
|
m.ReasoningContent = ""
|
||||||
|
if len(m.ToolCalls) > 0 {
|
||||||
|
tcs := make([]ChatToolCall, len(m.ToolCalls))
|
||||||
|
for j, tc := range m.ToolCalls {
|
||||||
|
tcs[j] = ChatToolCall{
|
||||||
|
Tool: tc.Tool,
|
||||||
|
Name: tc.Name,
|
||||||
|
Status: tc.Status,
|
||||||
|
Plugin: tc.Plugin,
|
||||||
|
Truncated: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.ToolCalls = tcs
|
||||||
|
}
|
||||||
|
out[i] = m
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleChatFile 处理用户经 webui 上传文件并附带消息注入 agent。
|
// handleChatFile 处理用户经 webui 上传文件并附带消息注入 agent。
|
||||||
|
|||||||
Reference in New Issue
Block a user