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 = '
.env 里的 API_SERVER_KEY 一致