zcbot/web/static/js/main.js

108 lines
4.8 KiB
JavaScript

// 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-email").focus();
}