// zcbot dev 控制台入口 + 编排:enterApp(应用初始化)、loadStorage(存储用量)、 // Esc 关弹窗栈、boot。功能逻辑全在各模块(chat/files/preview/media/auth/newtask/embed/layout/…), // 经各模块顶层 import 拉入依赖图(layout/markdown/media 由 chat 等引入,副作用照常)。 import { state, EMBED } from "./state.js"; import { humanSize, fmtTime } from "./format.js"; import { $ } from "./dom.js"; import { api } from "./api.js"; import { closeChpwModal } from "./auth.js"; import { closeSkillsModal } from "./skills.js"; import { closeMemoryModal } from "./memory.js"; import { closeCronsModal } from "./crons.js"; import { closeFilePreview, closeMiniPreview } from "./preview.js"; import { closeSrcPicker, loadFiles } from "./files.js"; import { loadFolderSuggestions } from "./newtask.js"; import { embedInit } from "./embed.js"; import { loadTaskList, loadModels } from "./chat.js"; // ───── enter app ───── export function enterApp() { $("login").style.display = "none"; $("app").classList.add("ready"); // 显示「name · uuid 前 8 位」;name 缺失(老 token 升级前)只显 uuid const uid8 = (state.userId || "").slice(0, 8); $("hd-who").textContent = state.userName ? `${state.userName} · ${uid8}` : state.userId; $("hd-who").title = state.userId; loadTaskList(); loadFiles(); // 文件面板与 task 解耦 — 启动即拉 user_root loadModels(); // 模型清单缓存:chat-meta 下拉 + 新建对话框 + 历史小标 loadFolderSuggestions(); // 灌 filter-wd select(modal 打开时会重拉,这里让左 pane 先有选项) loadStorage(); // 顶栏存储用量(后台扫描快照,非实时) loadRole(); // 拉 /v1/me,admin 才显「管理」入口(/static/admin.html) } // 当前用户角色:/v1/me 返 {user_id, role}。admin → 显顶栏「管理」链接。 // 失败静默(入口是增量功能,拉不到就当普通用户,不挡主流程)。 async function loadRole() { const link = $("hd-admin"); if (!link) return; try { const me = await api("GET", "/v1/me"); link.style.display = (me && me.role === "admin") ? "" : "none"; } catch (e) { link.style.display = "none"; } } // 存储用量:拉 /v1/user/storage 渲染文件面板底部进度条。用量来自后台 15min 扫描, // 故无需高频刷新 —— enterApp 拉一次即可。无配额上限时只显已用、不画进度条(nolimit)。 async function loadStorage() { let s; try { s = await api("GET", "/v1/user/storage"); } catch (e) { return; } const el = $("storage-foot"); const used = s.bytes_used || 0; const limit = s.limit_bytes; if (limit && limit > 0) { const pct = Math.min(100, Math.round(used / limit * 100)); $("storage-foot-bar").style.width = pct + "%"; $("storage-foot-txt").textContent = `${humanSize(used)} / ${humanSize(limit)}`; el.classList.remove("nolimit"); el.classList.toggle("over", used >= limit); } else { // 不限额:只显已用,隐藏进度条 $("storage-foot-txt").textContent = humanSize(used); el.classList.add("nolimit"); el.classList.remove("over"); } const when = s.scanned_at ? fmtTime(s.scanned_at) : "尚未统计"; el.title = `已用 ${humanSize(used)} · ${s.file_count || 0} 个文件\n统计于 ${when}(后台每 15 分钟扫描,非实时)`; el.classList.add("show"); } // 版本号:/healthz 是 auth 豁免接口,embed / 未登录都拿得到 → boot 时无条件拉一次填进左栏底部。 // 失败静默(版本号是装饰性信息,不该因网络抖动报错)。 async function loadVersion() { const el = $("app-version"); if (!el) return; try { const r = await fetch("/healthz"); const d = await r.json(); if (d && d.version) el.textContent = "v" + d.version; } catch (e) {} } // ───── Esc 关弹窗栈(跨模块协调:chpw/选入/文件预览/小预览)───── document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; // 多模态共存:优先关靠前栈顶 — 小预览(z 96)→ 选入(z 95)→ 文件预览(z 90)→ 新任务(z 80) if ($("chpw-modal").classList.contains("show")) { closeChpwModal(); return; } if ($("skills-modal").classList.contains("show")) { closeSkillsModal(); return; } if ($("memory-modal").classList.contains("show")) { closeMemoryModal(); return; } if ($("crons-modal").classList.contains("show")) { closeCronsModal(); return; } if ($("mini-preview-modal").classList.contains("show")) { closeMiniPreview(); return; } if ($("src-picker-modal").classList.contains("show")) { closeSrcPicker(); return; } if ($("file-preview-modal").classList.contains("show")) { closeFilePreview(); return; } }); // ───── boot ───── loadVersion(); // 与登录态无关,立即拉 if (EMBED) { embedInit(); } else if (state.token) { // 已有 token:试探一下,失败回登录页 enterApp(); } else { $("li-token").focus(); }