From ecff573dcfe42ba1c71a6cbed53b53e9abd0e01e Mon Sep 17 00:00:00 2001 From: kang Date: Sat, 9 May 2026 18:47:47 +0800 Subject: [PATCH] auto-save 2026-05-09 18:47 (~5) --- .memory/worklog.json | 27 +++--- src/app.js | 226 ++++++++++++++++++++++++++++++++++++++++++- src/index.html | 27 ++++++ src/styles.css | 83 ++++++++++++++++ src/sw.js | 2 +- 5 files changed, 347 insertions(+), 18 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 789318d..a1fb56e 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "63c3641", - "message": "auto-save 2026-05-07 17:27 (~1)", - "ts": "2026-05-07T17:27:18+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "0d5ecef", - "message": "auto-save 2026-05-07 17:33 (~1)", - "ts": "2026-05-07T17:33:27+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "42a4582", @@ -3463,6 +3449,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-09 18:36 (~3)", "files_changed": 1 + }, + { + "ts": "2026-05-09T18:42:16+08:00", + "type": "commit", + "message": "auto-save 2026-05-09 18:42 (~1)", + "hash": "55806ef", + "files_changed": 1 + }, + { + "ts": "2026-05-09T10:45:53Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 3 项未提交变更 · 最近提交:auto-save 2026-05-09 18:42 (~1)", + "files_changed": 3 } ] } diff --git a/src/app.js b/src/app.js index cc26238..659115b 100644 --- a/src/app.js +++ b/src/app.js @@ -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 = '存周报'; + 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 = '
还没有保存过周报记录。
'; + return; + } + box.innerHTML = reports.map(r => ` +
+
+
+
${escapeHTML(r.title || "周报记录")}
+
${escapeHTML(formatRecordTime(r.createdAt))}${r.tags?.length ? " · " + escapeHTML(r.tags.join(" / ")) : ""}
+
+ 已保存 +
+
+
任务描述
+
${escapeHTML(compactText(r.task || "未记录任务描述", 220))}
+
+
+
周报内容
+
${escapeHTML(compactText(r.report, 260))}
+
+
+ + + + +
+
+ `).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(); } diff --git a/src/index.html b/src/index.html index 2a945c9..be456f4 100644 --- a/src/index.html +++ b/src/index.html @@ -309,6 +309,7 @@ + @@ -1011,6 +1012,10 @@ git push # Gitea kangwan/hermes-glass-ui-personal
任意字符串,只要和 Hermes .env 里的 API_SERVER_KEY 一致
+
+ + +
@@ -1088,6 +1093,28 @@ git push # Gitea kangwan/hermes-glass-ui-personal + +
+
+
+ +
+
+
周报记录
+
保存任务描述、优化过程和最终周报
+
+
+
+
+
对话页点「存周报」会把最近一次任务描述和回答存到这里,之后可以打开、复制或删除。
+ +
+
+
还没有保存过周报记录。
+
+
+
+
diff --git a/src/styles.css b/src/styles.css index d5dd2bc..63cdda7 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2296,6 +2296,89 @@ a { color: var(--orange-3); text-decoration: none; } padding: 10px 16px; font-size: 12px; } +.weekly-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} +.weekly-toolbar .settings-help { flex: 1 1 280px; } +.weekly-report-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 12px; +} +.weekly-report-card { + border: 1px solid var(--line); + background: rgba(255,255,255,0.04); + border-radius: 12px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} +[data-theme="light"] .weekly-report-card { background: rgba(15,22,40,0.035); } +.weekly-report-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + min-width: 0; +} +.weekly-report-title { + font-size: 13px; + line-height: 1.45; + font-weight: 800; + color: var(--text); + overflow-wrap: anywhere; +} +.weekly-report-meta { + margin-top: 3px; + font-size: 11px; + color: var(--text-dim2); +} +.weekly-report-badge { + flex: 0 0 auto; + border: 1px solid rgba(255,178,89,0.32); + background: rgba(255,178,89,0.1); + color: var(--orange-3); + border-radius: 999px; + padding: 4px 8px; + font-size: 11px; + font-weight: 800; +} +.weekly-report-section { + border: 1px solid var(--line); + border-radius: 10px; + background: rgba(0,0,0,0.12); + padding: 10px; + min-width: 0; +} +[data-theme="light"] .weekly-report-section { background: rgba(15,22,40,0.04); } +.weekly-report-label { + font-size: 10px; + font-weight: 800; + color: var(--text-dim2); + margin-bottom: 5px; +} +.weekly-report-text { + font-size: 12px; + line-height: 1.65; + color: var(--text-dim); + overflow-wrap: anywhere; +} +.weekly-report-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.weekly-report-actions .glass-btn-sm { + margin: 0; + padding: 8px 11px; + font-size: 11px; +} .feishu-toolbar { display: flex; align-items: center; diff --git a/src/sw.js b/src/sw.js index 8c4c715..339fa34 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,6 +1,6 @@ // 爱马仕 Hermes · 轻量 Service Worker // 静态壳走 network-first(拿不到再回退缓存),API 直通 -const CACHE = "hermes-ui-v12"; +const CACHE = "hermes-ui-v13"; const ASSETS = [ "./", "./index.html",