auto-save 2026-05-09 18:47 (~5)

This commit is contained in:
2026-05-09 18:47:47 +08:00
parent 55806ef27f
commit ecff573dcf
5 changed files with 347 additions and 18 deletions

View File

@@ -10,6 +10,7 @@ const LS_AGENTS = "hermes-ui-agents-v1";
const LS_CUSTOM_SKILLS = "hermes-ui-custom-skills-v1";
const LS_FLOWS = "hermes-ui-flows-v1";
const LS_TAB = "hermes-ui-active-tab-v1";
const LS_WEEKLY_REPORTS = "hermes-ui-weekly-reports-v1";
const state = {
apiBase: "/api/v1",
@@ -37,6 +38,9 @@ const state = {
editingFlowId: null,
flowEditSelected: [], // 编辑 flow 时的临时 skill id 数组
// 周报记录
weeklyReports: [], // [{id,title,task,report,messages,createdAt,...}]
// 本次一次性使用的智能体 ID (不绑定会话)
pendingAgent: null,
@@ -55,6 +59,7 @@ document.addEventListener("DOMContentLoaded", () => {
loadFlows();
loadAgents();
loadConversations();
loadWeeklyReports();
bindTabs();
bindChat();
bindSearch();
@@ -104,7 +109,7 @@ function loadSettings() {
}
} catch (e) {}
}
function saveSettings() {
function saveSettings(options = {}) {
state.apiBase = document.getElementById("apiBase").value.trim();
state.apiKey = document.getElementById("apiKey").value.trim();
state.stream = document.getElementById("streamMode").checked;
@@ -113,7 +118,7 @@ function saveSettings() {
apiKey: state.apiKey,
stream: state.stream,
}));
toast("设置已保存");
if (!options.silent) toast("设置已保存");
pingBackend();
}
@@ -353,7 +358,10 @@ function switchTab(name, options = {}) {
if (name === "cron") refreshCron();
if (name === "memory") refreshMemory();
if (name === "tools") refreshTools();
if (name === "settings") refreshFeishuApps();
if (name === "settings") {
renderWeeklyReports();
refreshFeishuApps();
}
if (name === "runs") setTimeout(() => document.getElementById("runPrompt")?.focus(), 50);
if (name === "dashboard" && _dashboardDirty) {
// 推迟到下一帧,避免阻塞切换动画
@@ -521,6 +529,27 @@ async function pingBackend() {
}
}
async function testApiConnection() {
saveSettings({ silent: true });
try {
const res = await apiFetch(state.apiBase + "/models", {
headers: { "Authorization": "Bearer " + state.apiKey },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) {
toast("API 检测失败: HTTP " + res.status);
return;
}
const data = await res.json().catch(() => ({}));
const list = data?.data || data?.models || [];
toast("API 连接正常" + (list.length ? " · " + list.length + " 个模型" : ""));
} catch (e) {
toast("API 检测失败: " + (e.message || e));
} finally {
pingBackend();
}
}
async function fetchIP() {
const el = document.getElementById("statIP");
if (el) el.textContent = location.hostname;
@@ -891,6 +920,15 @@ function renderChat(streaming = false) {
actions.appendChild(pullBtn);
}
if (m.role === "assistant") {
const weeklyBtn = document.createElement("button");
weeklyBtn.className = "msg-action-btn";
weeklyBtn.title = "保存这条回答为周报记录";
weeklyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5z"/><path d="M8 7h8"/><path d="M8 11h8"/></svg><span>存周报</span>';
weeklyBtn.onclick = () => saveWeeklyReportFromMessage(capturedIdx);
actions.appendChild(weeklyBtn);
}
const copyBtn = document.createElement("button");
copyBtn.className = "msg-action-btn";
copyBtn.title = "复制";
@@ -963,6 +1001,183 @@ async function refreshDashboard() {
} catch (e) {}
}
// ---------- 周报记录 ----------
function loadWeeklyReports() {
try {
const raw = localStorage.getItem(LS_WEEKLY_REPORTS);
state.weeklyReports = raw ? (JSON.parse(raw) || []) : [];
if (!Array.isArray(state.weeklyReports)) state.weeklyReports = [];
} catch (e) {
state.weeklyReports = [];
}
}
function saveWeeklyReports() {
const sorted = (state.weeklyReports || [])
.filter(r => r && r.id && r.report)
.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
.slice(0, 80);
state.weeklyReports = sorted;
localStorage.setItem(LS_WEEKLY_REPORTS, JSON.stringify(sorted));
refreshDashboardLocal();
}
function findWeeklyAssistantIndex(messages, idx) {
if (!messages?.length) return -1;
if (Number.isInteger(idx) && messages[idx]?.role === "assistant" && String(messages[idx].content || "").trim()) {
return idx;
}
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "assistant" && String(messages[i].content || "").trim()) return i;
}
return -1;
}
function findTaskBefore(messages, idx) {
for (let i = idx - 1; i >= 0; i--) {
if (messages[i].role === "user" && String(messages[i].content || "").trim()) return messages[i];
}
return null;
}
function compactText(text, max = 180) {
const s = String(text || "").replace(/\s+/g, " ").trim();
return s.length > max ? s.slice(0, max - 1) + "…" : s;
}
function formatRecordTime(ts) {
const d = new Date(ts || Date.now());
return d.toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
}
function makeWeeklyTitle(task, convo) {
const base = compactText(task || convo?.title || "周报记录", 28);
return base || "周报记录";
}
function buildWeeklyRecord(messageIndex) {
const c = activeConvo();
const messages = c.messages || [];
const assistantIndex = findWeeklyAssistantIndex(messages, messageIndex);
if (assistantIndex < 0) {
toast("还没有可保存的周报内容");
return null;
}
const answer = messages[assistantIndex];
const taskMsg = findTaskBefore(messages, assistantIndex);
const task = taskMsg?.content || c.title || "";
const contextStart = Math.max(0, assistantIndex - 8);
return {
id: "wr_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
title: makeWeeklyTitle(task, c),
task,
report: answer.content,
messages: messages.slice(contextStart, assistantIndex + 1).map(m => ({
role: m.role,
content: m.content,
ts: m.ts || Date.now(),
agentId: m.agentId || null,
})),
tags: Array.isArray(c.tags) ? c.tags.slice(0, 8) : [],
sourceConvoId: c.id,
sourceAssistantTs: answer.ts || Date.now(),
createdAt: Date.now(),
};
}
function persistWeeklyRecord(record) {
if (!record) return;
const existing = state.weeklyReports.findIndex(r =>
r.sourceConvoId === record.sourceConvoId &&
r.sourceAssistantTs === record.sourceAssistantTs
);
if (existing >= 0) {
state.weeklyReports[existing] = { ...state.weeklyReports[existing], ...record, id: state.weeklyReports[existing].id };
saveWeeklyReports();
renderWeeklyReports();
toast("周报记录已更新");
return;
}
state.weeklyReports.unshift(record);
saveWeeklyReports();
renderWeeklyReports();
toast("周报记录已保存");
}
function saveWeeklyReportFromChat() {
persistWeeklyRecord(buildWeeklyRecord());
}
function saveWeeklyReportFromMessage(index) {
persistWeeklyRecord(buildWeeklyRecord(index));
}
function weeklyById(id) {
return (state.weeklyReports || []).find(r => r.id === id);
}
function renderWeeklyReports() {
const box = document.getElementById("weeklyReportsList");
if (!box) return;
const reports = (state.weeklyReports || []).slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
if (!reports.length) {
box.innerHTML = '<div class="settings-help">还没有保存过周报记录。</div>';
return;
}
box.innerHTML = reports.map(r => `
<div class="weekly-report-card">
<div class="weekly-report-top">
<div>
<div class="weekly-report-title">${escapeHTML(r.title || "周报记录")}</div>
<div class="weekly-report-meta">${escapeHTML(formatRecordTime(r.createdAt))}${r.tags?.length ? " · " + escapeHTML(r.tags.join(" / ")) : ""}</div>
</div>
<span class="weekly-report-badge">已保存</span>
</div>
<div class="weekly-report-section">
<div class="weekly-report-label">任务描述</div>
<div class="weekly-report-text">${escapeHTML(compactText(r.task || "未记录任务描述", 220))}</div>
</div>
<div class="weekly-report-section">
<div class="weekly-report-label">周报内容</div>
<div class="weekly-report-text">${escapeHTML(compactText(r.report, 260))}</div>
</div>
<div class="weekly-report-actions">
<button class="glass-btn-sm" onclick="openWeeklyRecord('${r.id}')">打开</button>
<button class="glass-btn-sm" onclick="copyWeeklyReport('${r.id}')">复制周报</button>
<button class="glass-btn-sm" onclick="copyWeeklyTask('${r.id}')">复制任务</button>
<button class="glass-btn-sm danger-btn" onclick="deleteWeeklyReport('${r.id}')">删除</button>
</div>
</div>
`).join("");
}
function openWeeklyRecord(id) {
const r = weeklyById(id);
if (!r) return;
const cid = createConvo();
const c = state.conversations[cid];
const savedMessages = Array.isArray(r.messages) && r.messages.length ? r.messages : [
{ role: "user", content: r.task || r.title || "周报任务", ts: r.createdAt || Date.now() },
{ role: "assistant", content: r.report || "", ts: r.createdAt || Date.now() },
];
c.title = "周报记录 · " + (r.title || "未命名").slice(0, 32);
c.tags = Array.from(new Set(["周报记录", ...(r.tags || [])]));
c.messages = savedMessages.map(m => ({
role: m.role,
content: m.content,
ts: m.ts || Date.now(),
agentId: m.agentId || null,
}));
c.updatedAt = Date.now();
saveConversations();
renderSidebar();
renderChat();
switchTab("chat");
}
function copyWeeklyReport(id) {
const r = weeklyById(id);
if (r) copyText(r.report || "");
}
function copyWeeklyTask(id) {
const r = weeklyById(id);
if (r) copyText(r.task || "");
}
function deleteWeeklyReport(id) {
const r = weeklyById(id);
if (!r) return;
if (!confirm("删除这条周报记录?")) return;
state.weeklyReports = state.weeklyReports.filter(item => item.id !== id);
saveWeeklyReports();
renderWeeklyReports();
}
// ---------- 每日用量聚合 ----------
let selectedDay = null;
@@ -3310,6 +3525,7 @@ function exportData() {
stream: state.stream,
},
conversations: state.conversations,
weeklyReports: state.weeklyReports,
agents: state.agents,
customSkills: state.customSkills,
flows: state.flows,
@@ -3336,6 +3552,10 @@ function importData(event) {
if (!data.version) { toast("文件格式不对"); return; }
if (!confirm("导入会覆盖当前所有对话、智能体和设置,确定吗?")) return;
if (data.conversations) state.conversations = data.conversations;
if (Array.isArray(data.weeklyReports)) {
state.weeklyReports = data.weeklyReports;
saveWeeklyReports();
}
if (data.agents) state.agents = data.agents;
if (data.customSkills) { state.customSkills = data.customSkills; saveCustomSkillsToLS(); }
if (data.flows) { state.flows = data.flows; saveFlowsToLS(); }