feat: add nas storage status panel
This commit is contained in:
@@ -72,6 +72,10 @@
|
||||
- 录制源按当前账号和项目归属保存
|
||||
- 录像文件只通过当前租户的后端代理访问
|
||||
- 前端不再直接暴露 NAS 全局配置和下载根地址
|
||||
- 存储状态面板已接上:
|
||||
- 当前项目和当前账号的缓存占用
|
||||
- 数据库本机 / 分析缓存 NAS / 下载缓存 NAS 的目录策略
|
||||
- 最近写入 NAS 的缓存样本路径
|
||||
- 会先识别后端是否具备 `tracking / reviews / integrations` 路由,再决定是否请求,避免不同版本 live collector 刷 404
|
||||
- 依赖不可达时,自动拦住 AI 视频 / 实拍剪辑动作并展示原因
|
||||
- 使用 Agent 生成文案
|
||||
@@ -104,5 +108,9 @@ python3 -m http.server 3918
|
||||
- 把全局搜索和页内搜索合并成统一搜索体验
|
||||
- 为 `生产中心 / 发布与复盘` 接入更完整的成片预览与封面对象
|
||||
- 如果后续要开放外网多租户录像访问,继续沿用 collector 的鉴权代理,不要把 NAS 下载目录直接暴露给浏览器
|
||||
- 现在的推荐策略是:
|
||||
- 数据库继续留本机
|
||||
- `jobs / downloads` 这类大文件缓存优先放 NAS
|
||||
- 如果后续出现速度或稳定性问题,再切到 OSS
|
||||
- 不要把这套页面重新塞回 `scripts/douyin-browser-capture/control_panel.mjs`
|
||||
- 抖音采集控制台仍作为独立工具存在,这里才是正式业务应用壳
|
||||
|
||||
@@ -27,6 +27,7 @@ const appState = {
|
||||
liveRecorderSources: [],
|
||||
liveRecorderStatus: null,
|
||||
liveRecorderFiles: [],
|
||||
storageStatus: null,
|
||||
integrationHealth: null,
|
||||
localModelCatalog: null,
|
||||
backendCapabilities: null,
|
||||
@@ -288,6 +289,20 @@ function formatNumber(value) {
|
||||
return String(Math.round(num * 10) / 10);
|
||||
}
|
||||
|
||||
function formatBytes(value) {
|
||||
const num = Number(value || 0);
|
||||
if (!Number.isFinite(num) || num <= 0) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = num;
|
||||
let idx = 0;
|
||||
while (size >= 1024 && idx < units.length - 1) {
|
||||
size /= 1024;
|
||||
idx += 1;
|
||||
}
|
||||
const fixed = size >= 10 || idx === 0 ? size.toFixed(0) : size.toFixed(1);
|
||||
return `${fixed}${units[idx]}`;
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
@@ -764,6 +779,7 @@ async function logoutSession() {
|
||||
appState.trackingDigest = null;
|
||||
appState.reviews = [];
|
||||
appState.integrationHealth = null;
|
||||
appState.storageStatus = null;
|
||||
appState.backendCapabilities = null;
|
||||
appState.lastAction = null;
|
||||
appState.lastGeneratedCopy = null;
|
||||
@@ -784,6 +800,17 @@ async function loadKnowledgeDocuments(knowledgeBases) {
|
||||
return groups.flat().slice(0, 12);
|
||||
}
|
||||
|
||||
async function loadStorageStatus(projectId = "") {
|
||||
if (!backendSupports("/v2/storage/status")) {
|
||||
appState.storageStatus = null;
|
||||
return null;
|
||||
}
|
||||
const suffix = projectId ? `?project_id=${encodeURIComponent(projectId)}` : "";
|
||||
const payload = await storyforgeFetch(`/v2/storage/status${suffix}`).catch(() => null);
|
||||
appState.storageStatus = payload;
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function loadPlatformAccount(platform, accountId) {
|
||||
if (!accountId) return;
|
||||
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
|
||||
@@ -852,6 +879,7 @@ async function bootstrap() {
|
||||
const supportsReviews = backendSupports("/v2/reviews");
|
||||
const supportsIntegrationHealth = backendSupports("/v2/integrations/health");
|
||||
const supportsLocalModels = backendSupports("/v2/integrations/local-models");
|
||||
const supportsStorageStatus = backendSupports("/v2/storage/status");
|
||||
const supportsLiveRecorderSources = backendSupports("/v2/live-recorder/sources");
|
||||
const supportsLiveRecorderStatus = backendSupports("/v2/live-recorder/status");
|
||||
const supportsLiveRecorderFiles = backendSupports("/v2/live-recorder/files");
|
||||
@@ -896,6 +924,11 @@ async function bootstrap() {
|
||||
appState.localModelCatalog = localModelCatalog;
|
||||
appState.documents = await loadKnowledgeDocuments(dashboard.knowledge_bases);
|
||||
appState.selectedProjectId = appState.selectedProjectId || dashboard.projects?.[0]?.id || "";
|
||||
if (supportsStorageStatus) {
|
||||
await loadStorageStatus(appState.selectedProjectId || "");
|
||||
} else {
|
||||
appState.storageStatus = null;
|
||||
}
|
||||
const selectedAssistantExists = safeArray(dashboard.assistants).some((item) => item.id === appState.selectedAssistantId);
|
||||
appState.selectedAssistantId = selectedAssistantExists ? appState.selectedAssistantId : (dashboard.assistants?.[0]?.id || "");
|
||||
const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId);
|
||||
@@ -1352,6 +1385,71 @@ function renderLiveRecorderSummaryHtml() {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderStorageStatusPanel() {
|
||||
const storage = appState.storageStatus;
|
||||
if (!storage) {
|
||||
return `
|
||||
<div class="panel pad">
|
||||
<div class="panel-head"><div><h3>存储状态</h3><div class="panel-subtitle">当前后端暂未接入存储状态接口</div></div></div>
|
||||
<div class="task-item"><h4>未拉取</h4><p>等 live collector 更新后,这里会显示 NAS 缓存、当前项目占用和目录策略。</p></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const strategy = storage.strategy || {};
|
||||
const disk = storage.disk || {};
|
||||
const usage = storage.tenant_usage || {};
|
||||
const recentJobs = safeArray(usage.recent_jobs);
|
||||
const strategyTags = [
|
||||
`数据库 ${strategy.database?.mode || "local"}`,
|
||||
`分析缓存 ${strategy.jobs?.mode || "local"}`,
|
||||
`下载缓存 ${strategy.downloads?.mode || "local"}`,
|
||||
`直播录制 ${strategy.live_recorder?.mode || "nas_service"}`,
|
||||
];
|
||||
const usageCards = [
|
||||
{ label: "当前项目缓存", value: formatBytes(usage.project_jobs?.bytes), sub: `文件 ${formatNumber(usage.project_jobs?.file_count)}` },
|
||||
{ label: "当前项目下载", value: formatBytes(usage.project_downloads?.bytes), sub: `文件 ${formatNumber(usage.project_downloads?.file_count)}` },
|
||||
{ label: "当前账号缓存", value: formatBytes(usage.account_jobs?.bytes), sub: `文件 ${formatNumber(usage.account_jobs?.file_count)}` },
|
||||
{ label: "NAS 剩余", value: formatBytes(disk.jobs?.free_bytes), sub: `总量 ${formatBytes(disk.jobs?.total_bytes)}` }
|
||||
];
|
||||
return `
|
||||
<div class="panel pad">
|
||||
<div class="panel-head"><div><h3>存储状态</h3><div class="panel-subtitle">数据库留本机,大文件缓存优先走 NAS</div></div><span class="tag blue">${escapeHtml((strategy.jobs?.mode || "local").toUpperCase())}</span></div>
|
||||
<div class="task-meta">
|
||||
${strategyTags.map((item) => `<span class="tag">${escapeHtml(item)}</span>`).join("")}
|
||||
</div>
|
||||
<div class="mini-grid" style="margin-top:14px;">
|
||||
${usageCards.map((item) => `
|
||||
<div class="mini-card">
|
||||
<small>${escapeHtml(item.label)}</small>
|
||||
<strong>${escapeHtml(item.value)}</strong>
|
||||
<span>${escapeHtml(item.sub)}</span>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
<div class="list" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>当前项目缓存根目录</h4>
|
||||
<p>${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")}</p>
|
||||
</div>
|
||||
<div class="task-item compact">
|
||||
<h4>下载缓存根目录</h4>
|
||||
<p>${escapeHtml(usage.project_downloads?.path || strategy.downloads?.path || "-")}</p>
|
||||
</div>
|
||||
${recentJobs.slice(0, 3).map((item) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(item.title || item.job_id)}</h4>
|
||||
<p>${escapeHtml(item.paths?.[0]?.path || item.job_id)}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag">${escapeHtml(item.status || "-")}</span>
|
||||
<span class="tag">${escapeHtml(formatDateTime(item.updated_at))}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>还没有缓存样本</h4><p>上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getIntegrationOverview() {
|
||||
const cards = getIntegrationCards();
|
||||
const reachableCount = cards.filter((item) => item.detail.available && item.detail.reachable).length;
|
||||
@@ -1701,6 +1799,7 @@ function renderDashboardScreen() {
|
||||
<div class="mini-card"><small>来源</small><strong>${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).sources.length : 0))}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
${renderStorageStatusPanel()}
|
||||
<div class="panel pad">
|
||||
<div class="panel-head"><div><h3>跟踪摘要</h3><div class="panel-subtitle">按最近同步的账号作品生成</div></div><span class="tag blue">${escapeHtml(daysSince(appState.lastSeenAt))} 天汇总</span></div>
|
||||
<div class="list">
|
||||
@@ -3771,6 +3870,14 @@ document.addEventListener("click", async (event) => {
|
||||
}
|
||||
if (name === "select-project") {
|
||||
appState.selectedProjectId = action.dataset.projectId || "";
|
||||
if (backendSupports("/v2/storage/status")) {
|
||||
setBusy(true, "正在切换项目存储视图...");
|
||||
try {
|
||||
await loadStorageStatus(appState.selectedProjectId || "");
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
}
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user