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

@@ -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
}
]
}

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(); }

View File

@@ -309,6 +309,7 @@
<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
</select>
</div>
<button class="chat-clear" onclick="saveWeeklyReportFromChat()" title="保存最近一条回答为周报记录">存周报</button>
<button class="chat-clear" id="clearBtn" title="清空对话">清空</button>
</div>
@@ -1011,6 +1012,10 @@ git push # Gitea kangwan/hermes-glass-ui-personal
<input type="password" id="apiKey" value="hermes-mini-local-key-2026">
<div class="settings-help">任意字符串,只要和 Hermes <code>.env</code> 里的 <code>API_SERVER_KEY</code> 一致</div>
</div>
<div class="settings-actions">
<button class="glass-btn-sm" onclick="saveSettings()">保存 API</button>
<button class="glass-btn-sm" onclick="testApiConnection()">测试连接</button>
</div>
</div>
</div>
@@ -1088,6 +1093,28 @@ git push # Gitea kangwan/hermes-glass-ui-personal
</div>
</div>
<!-- 周报记录 -->
<div class="settings-group wide" id="weeklyReportGroup">
<div class="settings-group-head">
<div class="settings-group-icon">
<svg viewBox="0 0 24 24" 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>
</div>
<div>
<div class="settings-group-title">周报记录</div>
<div class="settings-group-desc">保存任务描述、优化过程和最终周报</div>
</div>
</div>
<div class="settings-group-body">
<div class="weekly-toolbar">
<div class="settings-help">对话页点「存周报」会把最近一次任务描述和回答存到这里,之后可以打开、复制或删除。</div>
<button class="glass-btn-sm" onclick="renderWeeklyReports()">刷新</button>
</div>
<div class="weekly-report-list" id="weeklyReportsList">
<div class="settings-help">还没有保存过周报记录。</div>
</div>
</div>
</div>
<!-- 飞书集成 -->
<div class="settings-group wide" id="feishuSettingsGroup">
<div class="settings-group-head">

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
// 爱马仕 Hermes · 轻量 Service Worker
// 静态壳走 network-first拿不到再回退缓存API 直通
const CACHE = "hermes-ui-v12";
const CACHE = "hermes-ui-v13";
const ASSETS = [
"./",
"./index.html",