feat: finish storyforge workbench and runtime closure
This commit is contained in:
@@ -17,6 +17,11 @@ const appState = {
|
||||
selectedAccountRequestToken: 0,
|
||||
selectedWorkspace: null,
|
||||
selectedVideos: { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 },
|
||||
snapshots: [],
|
||||
selectedSnapshotId: "",
|
||||
selectedSnapshotDetail: null,
|
||||
creatorFields: null,
|
||||
analysisReports: [],
|
||||
documents: [],
|
||||
discoveryQuery: "",
|
||||
currentPlatform: localStorage.getItem(STORAGE_KEY + ":currentPlatform") || "",
|
||||
@@ -30,6 +35,7 @@ const appState = {
|
||||
liveRecorderSources: [],
|
||||
liveRecorderStatus: null,
|
||||
liveRecorderFiles: [],
|
||||
liveRecorderHealth: null,
|
||||
storageStatus: null,
|
||||
integrationHealth: null,
|
||||
localModelCatalog: null,
|
||||
@@ -43,6 +49,7 @@ const appState = {
|
||||
tenantQuota: null,
|
||||
tenantUsage: null,
|
||||
adminOpsOverview: null,
|
||||
adminFixRuns: [],
|
||||
busy: false,
|
||||
message: "",
|
||||
lastAction: null,
|
||||
@@ -979,6 +986,11 @@ async function logoutSession() {
|
||||
appState.selectedAssistantId = "";
|
||||
appState.selectedWorkspace = null;
|
||||
appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 };
|
||||
appState.snapshots = [];
|
||||
appState.selectedSnapshotId = "";
|
||||
appState.selectedSnapshotDetail = null;
|
||||
appState.creatorFields = null;
|
||||
appState.analysisReports = [];
|
||||
appState.documents = [];
|
||||
appState.trackingAccounts = [];
|
||||
appState.trackingDigest = null;
|
||||
@@ -992,8 +1004,10 @@ async function logoutSession() {
|
||||
appState.tenantQuota = null;
|
||||
appState.tenantUsage = null;
|
||||
appState.adminOpsOverview = null;
|
||||
appState.adminFixRuns = [];
|
||||
appState.integrationHealth = null;
|
||||
appState.storageStatus = null;
|
||||
appState.liveRecorderHealth = null;
|
||||
appState.backendCapabilities = null;
|
||||
appState.lastAction = null;
|
||||
appState.lastGeneratedCopy = null;
|
||||
@@ -1032,10 +1046,11 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry");
|
||||
const supportsPlatformAgents = backendSupports("/v2/platform-agents");
|
||||
const supportsAdminOps = backendSupports("/v2/admin/ops/overview");
|
||||
const supportsAdminFixRuns = backendSupports("/v2/admin/ops/fix-runs");
|
||||
const supportsTenantQuota = backendSupports("/v2/tenant/quota");
|
||||
const supportsTenantUsage = backendSupports("/v2/tenant/usage");
|
||||
|
||||
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, tenantQuota, tenantUsage, adminOpsOverview] = await Promise.all([
|
||||
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
|
||||
supportsOneLinerProfile
|
||||
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
@@ -1056,7 +1071,10 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
: Promise.resolve(null),
|
||||
supportsAdminOps && isSuperAdmin()
|
||||
? storyforgeFetch("/v2/admin/ops/overview").catch(() => null)
|
||||
: Promise.resolve(null)
|
||||
: Promise.resolve(null),
|
||||
supportsAdminFixRuns && isSuperAdmin()
|
||||
? storyforgeFetch("/v2/admin/ops/fix-runs").catch(() => ({ items: [] }))
|
||||
: Promise.resolve({ items: [] })
|
||||
]);
|
||||
|
||||
appState.onelinerProfile = profile;
|
||||
@@ -1069,6 +1087,7 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
appState.tenantQuota = tenantQuota;
|
||||
appState.tenantUsage = tenantUsage;
|
||||
appState.adminOpsOverview = adminOpsOverview;
|
||||
appState.adminFixRuns = safeArray(adminFixRunsPayload?.items || adminFixRunsPayload);
|
||||
}
|
||||
|
||||
async function loadOneLinerMessages(sessionId) {
|
||||
@@ -1358,6 +1377,8 @@ async function loadPlatformAccount(platform, accountId, requestToken = 0) {
|
||||
appState.snapshots = [];
|
||||
appState.selectedSnapshotId = "";
|
||||
appState.selectedSnapshotDetail = null;
|
||||
appState.creatorFields = null;
|
||||
appState.analysisReports = [];
|
||||
appState.similarSearchDetail = null;
|
||||
return true;
|
||||
}
|
||||
@@ -1371,13 +1392,18 @@ async function loadPlatformAccount(platform, accountId, requestToken = 0) {
|
||||
appState.snapshots = [];
|
||||
appState.selectedSnapshotId = "";
|
||||
appState.selectedSnapshotDetail = null;
|
||||
appState.creatorFields = null;
|
||||
appState.analysisReports = [];
|
||||
appState.similarSearchDetail = null;
|
||||
return true;
|
||||
}
|
||||
const videosPath = getWorkbenchRoute(normalizedPlatform, "videos", accountId);
|
||||
const supportsAccountVideos = videosPath && backendSupports(`/v2/${normalizedPlatform}/accounts/{account_id}/videos`);
|
||||
const supportsAccountSnapshots = normalizedPlatform === "douyin" && backendSupports("/v2/douyin/accounts/{account_id}/snapshots");
|
||||
const supportsCreatorFields = normalizedPlatform === "douyin" && backendSupports("/v2/douyin/accounts/{account_id}/creator-fields");
|
||||
const supportsAnalysisReports = normalizedPlatform === "douyin" && backendSupports("/v2/douyin/accounts/{account_id}/analysis-reports");
|
||||
try {
|
||||
const [workspace, videos] = await Promise.all([
|
||||
const [workspace, videos, snapshotsPayload, creatorFieldsPayload, analysisReportsPayload] = await Promise.all([
|
||||
storyforgeFetch(workspacePath),
|
||||
supportsAccountVideos
|
||||
? storyforgeFetch(videosPath).catch(() => ({
|
||||
@@ -1393,13 +1419,38 @@ async function loadPlatformAccount(platform, accountId, requestToken = 0) {
|
||||
top_scored_video_ids: [],
|
||||
latest_video_ids: [],
|
||||
high_score_threshold: 60
|
||||
})
|
||||
}),
|
||||
supportsAccountSnapshots
|
||||
? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/snapshots`).catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
supportsCreatorFields
|
||||
? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/creator-fields`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
supportsAnalysisReports
|
||||
? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/analysis-reports`).catch(() => [])
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
if (token !== appState.selectedAccountRequestToken) {
|
||||
return false;
|
||||
}
|
||||
appState.selectedWorkspace = workspace;
|
||||
appState.selectedVideos = videos;
|
||||
if (normalizedPlatform === "douyin") {
|
||||
appState.snapshots = safeArray(snapshotsPayload?.items || snapshotsPayload);
|
||||
appState.creatorFields = creatorFieldsPayload;
|
||||
appState.analysisReports = safeArray(analysisReportsPayload?.items || analysisReportsPayload);
|
||||
const nextSnapshotId = appState.snapshots.find((item) => item.id === appState.selectedSnapshotId)?.id || appState.snapshots[0]?.id || "";
|
||||
appState.selectedSnapshotId = nextSnapshotId;
|
||||
appState.selectedSnapshotDetail = nextSnapshotId
|
||||
? await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/snapshots/${encodeURIComponent(nextSnapshotId)}`).catch(() => null)
|
||||
: null;
|
||||
} else {
|
||||
appState.snapshots = [];
|
||||
appState.selectedSnapshotId = "";
|
||||
appState.selectedSnapshotDetail = null;
|
||||
appState.creatorFields = null;
|
||||
appState.analysisReports = [];
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (token !== appState.selectedAccountRequestToken) {
|
||||
@@ -1442,7 +1493,8 @@ async function bootstrap() {
|
||||
const supportsLiveRecorderSources = backendSupports("/v2/live-recorder/sources");
|
||||
const supportsLiveRecorderStatus = backendSupports("/v2/live-recorder/status");
|
||||
const supportsLiveRecorderFiles = backendSupports("/v2/live-recorder/files");
|
||||
const [contentSources, platformPayloads, reviews, integrationHealth, localModelCatalog, liveRecorderSourcesPayload, liveRecorderStatus, liveRecorderFilesPayload] = await Promise.all([
|
||||
const supportsLiveRecorderHealth = backendSupports("/v2/live-recorder/health");
|
||||
const [contentSources, platformPayloads, reviews, integrationHealth, localModelCatalog, liveRecorderSourcesPayload, liveRecorderStatus, liveRecorderFilesPayload, liveRecorderHealth] = await Promise.all([
|
||||
storyforgeFetch("/v2/content-sources").catch(() => []),
|
||||
Promise.all(runtimePlatforms.map(async (platform) => {
|
||||
const accountListPath = getWorkbenchRoute(platform, "accounts");
|
||||
@@ -1484,7 +1536,8 @@ async function bootstrap() {
|
||||
supportsLocalModels ? storyforgeFetch("/v2/integrations/local-models").catch(() => null) : Promise.resolve(null),
|
||||
supportsLiveRecorderSources ? storyforgeFetch("/v2/live-recorder/sources").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }),
|
||||
supportsLiveRecorderStatus ? storyforgeFetch("/v2/live-recorder/status").catch(() => null) : Promise.resolve(null),
|
||||
supportsLiveRecorderFiles ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] })
|
||||
supportsLiveRecorderFiles ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }),
|
||||
supportsLiveRecorderHealth ? storyforgeFetch("/v2/live-recorder/health").catch(() => null) : Promise.resolve(null)
|
||||
]);
|
||||
const mergedAccounts = safeArray(platformPayloads)
|
||||
.flatMap((entry) => safeArray(entry.accounts))
|
||||
@@ -1531,6 +1584,7 @@ async function bootstrap() {
|
||||
appState.liveRecorderFiles = safeArray(liveRecorderFilesPayload?.items || liveRecorderFilesPayload);
|
||||
appState.integrationHealth = integrationHealth;
|
||||
appState.localModelCatalog = localModelCatalog;
|
||||
appState.liveRecorderHealth = liveRecorderHealth;
|
||||
appState.documents = await loadKnowledgeDocuments(dashboard.knowledge_bases);
|
||||
appState.selectedProjectId = appState.selectedProjectId || dashboard.projects?.[0]?.id || "";
|
||||
if (supportsStorageStatus) {
|
||||
@@ -2540,12 +2594,12 @@ function renderAdminOpsPanel() {
|
||||
<div class="panel pad" style="margin-top:18px;">
|
||||
<div class="panel-head"><div><h3>运维与审计 Agent</h3><div class="panel-subtitle">仅平台最高权限用户可见。</div></div></div>
|
||||
<div class="task-item"><h4>尚未拉到概览</h4><p>刷新后会自动读取失败任务、集成健康和待审事件。</p></div>
|
||||
${renderAdminFixRunsPanel()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const incidents = safeArray(overview.incidents).slice(0, 6);
|
||||
const audits = safeArray(overview.recent_audits).slice(0, 5);
|
||||
const fixRuns = safeArray(overview.recent_fix_runs).slice(0, 5);
|
||||
return `
|
||||
<div class="panel pad" style="margin-top:18px;">
|
||||
<div class="panel-head">
|
||||
@@ -2581,24 +2635,6 @@ function renderAdminOpsPanel() {
|
||||
</div>
|
||||
`).join("") || `<div class="task-item"><h4>当前没有待处理事件</h4><p>最近主链比较稳定,继续观察即可。</p></div>`}
|
||||
</div>
|
||||
<div class="list" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>最近修复计划</h4>
|
||||
<p>这里代表运维 Agent 输出的修复方案,必须经过审计 Agent 放行才算闭环。</p>
|
||||
</div>
|
||||
${fixRuns.map((item) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(item.plan?.summary || item.id || "修复计划")}</h4>
|
||||
<p>${escapeHtml(item.plan?.steps?.[0] || "待补充修复步骤")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(item.plan_scope || "plan")}</span>
|
||||
<span class="tag">${escapeHtml(item.audit_status || "pending")}</span>
|
||||
${item.incident_id ? `<span class="tag">事件 ${escapeHtml(brief(item.incident_id, 10))}</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-admin-fix-run-audit" data-run-id="${escapeHtml(item.id)}">审计放行</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>还没有修复计划</h4><p>当运维 Agent 针对故障事件生成 repair plan 后,这里会自动出现。</p></div>`}
|
||||
</div>
|
||||
<div class="list" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>最近审计记录</h4>
|
||||
@@ -2616,6 +2652,7 @@ function renderAdminOpsPanel() {
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>还没有审计记录</h4><p>等管理员做一次扫描或审计处理后,这里会自动出现。</p></div>`}
|
||||
</div>
|
||||
${renderAdminFixRunsPanel()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -2879,6 +2916,334 @@ function renderPlatformSwitchChips(currentPlatform) {
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function getProjectNameById(projectId) {
|
||||
return safeArray(appState.dashboard?.projects).find((project) => project.id === projectId)?.name || projectId || "-";
|
||||
}
|
||||
|
||||
function formatSnapshotFieldValue(value) {
|
||||
if (value == null) return "-";
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return brief(text, 120);
|
||||
}
|
||||
|
||||
function renderSnapshotFieldRows(fields, limit = 8) {
|
||||
return safeArray(fields)
|
||||
.slice(0, limit)
|
||||
.map((field) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(field.field_path || field.path || "field")}</h4>
|
||||
<p>${escapeHtml(formatSnapshotFieldValue(field.field_value_text || field.value || field.summary || ""))}</p>
|
||||
<div class="task-meta">
|
||||
${field.field_type ? `<span class="tag blue">${escapeHtml(field.field_type)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function renderDouyinInsightPanel() {
|
||||
const selected = getSelectedAccount();
|
||||
if (!selected || getAccountPlatform(selected) !== "douyin") {
|
||||
return "";
|
||||
}
|
||||
const snapshots = safeArray(appState.snapshots);
|
||||
const selectedSnapshot = appState.selectedSnapshotDetail
|
||||
|| snapshots.find((item) => item.id === appState.selectedSnapshotId)
|
||||
|| null;
|
||||
const creatorFields = appState.creatorFields || null;
|
||||
const analysisReports = safeArray(appState.analysisReports.length ? appState.analysisReports : appState.selectedWorkspace?.recent_reports);
|
||||
const snapshotSummary = selectedSnapshot?.summary || {};
|
||||
const creatorSummary = creatorFields?.summary || {};
|
||||
const selectedSnapshotFields = safeArray(selectedSnapshot?.fields);
|
||||
const creatorSnapshotFields = safeArray(creatorFields?.fields);
|
||||
return `
|
||||
<div class="panel pad" id="douyin-insight-anchor" style="box-shadow:none; margin-top:16px;">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>抖音快照详情</h3>
|
||||
<div class="panel-subtitle">快照、创作者字段和分析报告统一在这里看</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(formatNumber(snapshots.length))} 个快照</span>
|
||||
<span class="tag">${escapeHtml(formatNumber(creatorSnapshotFields.length || creatorFields?.field_count || 0))} 个字段</span>
|
||||
<span class="tag green">${escapeHtml(formatNumber(analysisReports.length))} 条报告</span>
|
||||
<span class="tag clickable-tag" data-action="refresh-data">刷新</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-grid">
|
||||
<div class="mini-card">
|
||||
<small>快照类型</small>
|
||||
<strong>${escapeHtml(selectedSnapshot?.snapshot_type || "未选中")}</strong>
|
||||
<span>${escapeHtml(selectedSnapshot?.collected_at ? formatDateTime(selectedSnapshot.collected_at) : "等待选择")}</span>
|
||||
</div>
|
||||
<div class="mini-card">
|
||||
<small>字段数</small>
|
||||
<strong>${escapeHtml(formatNumber(selectedSnapshot?.field_count || 0))}</strong>
|
||||
<span>${escapeHtml(selectedSnapshot?.source_url ? brief(selectedSnapshot.source_url, 28) : "暂无来源")}</span>
|
||||
</div>
|
||||
<div class="mini-card">
|
||||
<small>创作者字段</small>
|
||||
<strong>${escapeHtml(formatNumber(creatorFields?.field_count || 0))}</strong>
|
||||
<span>${escapeHtml(creatorFields?.collected_at ? formatDateTime(creatorFields.collected_at) : "尚未拉取")}</span>
|
||||
</div>
|
||||
<div class="mini-card">
|
||||
<small>分析报告</small>
|
||||
<strong>${escapeHtml(formatNumber(analysisReports.length))}</strong>
|
||||
<span>${escapeHtml(analysisReports[0]?.created_at ? formatDateTime(analysisReports[0].created_at) : "暂无报告")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-col" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>快照列表</h4>
|
||||
<p>点击任意快照可以切换右侧详情,便于比对公开页和 creator center 的变化。</p>
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${snapshots.map((snapshot) => `
|
||||
<div class="task-item compact ${snapshot.id === appState.selectedSnapshotId ? "active" : ""}">
|
||||
<h4>${escapeHtml(snapshot.snapshot_type || "snapshot")} · ${escapeHtml(formatDateTime(snapshot.collected_at))}</h4>
|
||||
<p>${escapeHtml(brief(JSON.stringify(snapshot.summary || {}), 96))}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag">${escapeHtml(formatNumber(snapshot.field_count || 0))} 字段</span>
|
||||
<span class="tag clickable-tag" data-action="select-douyin-snapshot" data-snapshot-id="${escapeHtml(snapshot.id)}">查看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>还没有快照</h4><p>同步账号后,这里会自动出现 public profile 和 creator center 快照。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-item compact">
|
||||
<h4>当前快照详情</h4>
|
||||
<p>${escapeHtml(selectedSnapshot ? brief(JSON.stringify(snapshotSummary), 120) : "先从左侧选择一个快照")}</p>
|
||||
<div class="task-meta">
|
||||
${selectedSnapshot?.source_url ? `<a class="tag" href="${escapeHtml(selectedSnapshot.source_url)}" target="_blank" rel="noreferrer">打开来源</a>` : ""}
|
||||
${selectedSnapshot?.snapshot_type ? `<span class="tag blue">${escapeHtml(selectedSnapshot.snapshot_type)}</span>` : ""}
|
||||
</div>
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${selectedSnapshotFields.length ? renderSnapshotFieldRows(selectedSnapshotFields, 6) : `<div class="task-item compact"><h4>暂无字段</h4><p>选中快照后会显示原始字段明细。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-col" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>Creator Fields</h4>
|
||||
<p>${escapeHtml(creatorFields ? brief(JSON.stringify(creatorSummary), 120) : "尚未拉取 creator center 字段")}</p>
|
||||
<div class="task-meta">
|
||||
${creatorFields?.source_url ? `<a class="tag" href="${escapeHtml(creatorFields.source_url)}" target="_blank" rel="noreferrer">打开 creator center</a>` : ""}
|
||||
${creatorFields?.snapshot_type ? `<span class="tag blue">${escapeHtml(creatorFields.snapshot_type)}</span>` : ""}
|
||||
${creatorFields?.field_count != null ? `<span class="tag">${escapeHtml(formatNumber(creatorFields.field_count))} 字段</span>` : ""}
|
||||
</div>
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${creatorSnapshotFields.length ? renderSnapshotFieldRows(creatorSnapshotFields, 6) : `<div class="task-item compact"><h4>还没有 creator 字段</h4><p>等 creator center 快照同步后,这里会展示字段明细。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-item compact">
|
||||
<h4>分析报告</h4>
|
||||
<p>分析报告来自 `/analysis-reports`,可直接对照结论和建议。</p>
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${analysisReports.map((report) => {
|
||||
const suggestion = safeArray(report.suggestions)[0] || null;
|
||||
const summary = suggestion?.parsed_json?.executive_summary || suggestion?.suggestion_text || report.focus_text || "暂无结论";
|
||||
return `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(brief(report.focus_text || "分析报告", 34))}</h4>
|
||||
<p>${escapeHtml(brief(summary, 120))}</p>
|
||||
<div class="task-meta">
|
||||
${report.created_at ? `<span class="tag blue">${escapeHtml(formatDateTime(report.created_at))}</span>` : ""}
|
||||
${suggestion?.model_label ? `<span class="tag">${escapeHtml(suggestion.model_label)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("") || `<div class="task-item compact"><h4>还没有分析报告</h4><p>对当前账号跑一次分析后,这里会自动出现结论和建议。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function openDouyinSnapshotDetailAction(snapshotId) {
|
||||
const selected = getSelectedAccount();
|
||||
if (!selected || getAccountPlatform(selected) !== "douyin") {
|
||||
return;
|
||||
}
|
||||
if (!snapshotId) {
|
||||
return;
|
||||
}
|
||||
setBusy(true, "正在加载快照详情...");
|
||||
try {
|
||||
const detail = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(selected.id)}/snapshots/${encodeURIComponent(snapshotId)}`);
|
||||
appState.selectedSnapshotId = snapshotId;
|
||||
appState.selectedSnapshotDetail = detail;
|
||||
rememberAction("快照已切换", `已打开 ${detail.snapshot_type || "snapshot"} 的完整详情。`, "green", detail);
|
||||
renderAll();
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
function renderLiveRecorderManagementPanel() {
|
||||
const sources = safeArray(appState.liveRecorderSources);
|
||||
const status = appState.liveRecorderStatus || {};
|
||||
const health = getIntegrationDetail("live_recorder");
|
||||
const liveRecorderHealth = appState.liveRecorderHealth || {};
|
||||
const files = safeArray(appState.liveRecorderFiles);
|
||||
const activeItems = safeArray(status.active_recordings);
|
||||
const runtimeBits = [
|
||||
health.available ? health.reachable ? "在线" : (health.configured ? "不可达" : "未配置") : "未拉取",
|
||||
status.running ? `运行中 pid ${status.pid || "-"}` : "未运行",
|
||||
`活动录制 ${formatNumber(activeItems.length)}`,
|
||||
`最近文件 ${formatNumber(files.length)}`
|
||||
];
|
||||
const directHealthText = liveRecorderHealth
|
||||
? (liveRecorderHealth.ok || String(liveRecorderHealth.status || "").toLowerCase() === "ok"
|
||||
? "HTTP 健康:ok"
|
||||
: `HTTP 健康:${liveRecorderHealth.status || liveRecorderHealth.message || "异常"}`)
|
||||
: "HTTP 健康:未拉取";
|
||||
return `
|
||||
<div class="panel pad" id="live-recorder-maintenance-anchor" style="box-shadow:none;">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>Live Recorder 维护面板</h3>
|
||||
<div class="panel-subtitle">编辑录制源、查看健康状态、导入配置和删除源都在这里</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${health.reachable ? "green" : health.configured ? "orange" : "red"}">${escapeHtml(health.reachable ? "健康" : health.configured ? "待检查" : "未配置")}</span>
|
||||
<span class="tag">${escapeHtml(status.running ? "运行中" : "已停止")}</span>
|
||||
<span class="tag clickable-tag" data-action="refresh-data">刷新</span>
|
||||
<span class="tag clickable-tag" data-action="open-live-recorder-create">新增录制源</span>
|
||||
<span class="tag clickable-tag" data-action="import-live-recorder-config">导入 URL 配置</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-grid">
|
||||
${runtimeBits.map((item, index) => `
|
||||
<div class="mini-card">
|
||||
<small>${escapeHtml(["健康", "运行", "活动", "文件"][index])}</small>
|
||||
<strong>${escapeHtml(item)}</strong>
|
||||
<span>${escapeHtml(index === 0 ? (health.url || health.baseUrl || "未拉取健康数据") : index === 1 ? (status.started_at ? formatDateTime(status.started_at) : "暂无启动时间") : index === 2 ? "当前租户录制状态" : "当前租户录像索引")}</span>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
<div class="task-item compact" style="margin-top:14px;">
|
||||
<h4>直连健康</h4>
|
||||
<p>${escapeHtml(directHealthText)}</p>
|
||||
<div class="task-meta">
|
||||
${liveRecorderHealth?.base_url ? `<span class="tag">${escapeHtml(brief(liveRecorderHealth.base_url, 32))}</span>` : ""}
|
||||
${liveRecorderHealth?.url ? `<span class="tag blue">${escapeHtml(brief(liveRecorderHealth.url, 32))}</span>` : ""}
|
||||
${liveRecorderHealth?.pid ? `<span class="tag">${escapeHtml(`pid ${liveRecorderHealth.pid}`)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>录制源列表</h4>
|
||||
<p>默认按当前租户筛选,编辑时可改项目、Agent、标题、清晰度和启停状态。</p>
|
||||
</div>
|
||||
${sources.map((source) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(source.title || source.remote_name || source.source_url || "录制源")}</h4>
|
||||
<p>${escapeHtml(source.source_url || "暂无源链接")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag">${escapeHtml(platformLabel(source.platform || "kuaishou"))}</span>
|
||||
<span class="tag">${escapeHtml(source.quality || "原画")}</span>
|
||||
<span class="tag ${source.enabled ? "green" : "orange"}">${escapeHtml(source.enabled ? "启用" : "停用")}</span>
|
||||
<span class="tag">${escapeHtml(getProjectNameById(source.project_id || ""))}</span>
|
||||
${source.recording_count ? `<span class="tag blue">${escapeHtml(formatNumber(source.recording_count))} 个活动录制</span>` : ""}
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag clickable-tag" data-action="edit-live-recorder-source" data-source-id="${escapeHtml(source.id)}">编辑</span>
|
||||
<span class="tag clickable-tag ${source.enabled ? "orange" : "green"}" data-action="toggle-live-recorder-source" data-source-id="${escapeHtml(source.id)}" data-next-enabled="${escapeHtml(source.enabled ? "false" : "true")}">${escapeHtml(source.enabled ? "停用" : "启用")}</span>
|
||||
<span class="tag clickable-tag" data-action="delete-live-recorder-source" data-source-id="${escapeHtml(source.id)}">删除</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>还没有录制源</h4><p>先导入或新增一个直播源,后端会自动同步到租户视图。</p></div>`}
|
||||
</div>
|
||||
<div class="two-col" style="margin-top:14px;">
|
||||
<div class="task-item compact">
|
||||
<h4>健康检查与运行状态</h4>
|
||||
<p>${escapeHtml([
|
||||
health.available ? `健康接口:${health.reachable ? "在线" : "不可达"}` : "还没有拉取健康接口",
|
||||
status.url_info?.service_url ? `服务地址:${status.url_info.service_url}` : "",
|
||||
activeItems.length ? `活动录制:${activeItems.length}` : "当前没有活动录制"
|
||||
].filter(Boolean).join(" · "))}</p>
|
||||
<div class="task-meta">
|
||||
${status.pid ? `<span class="tag blue">PID ${escapeHtml(status.pid)}</span>` : ""}
|
||||
${status.last_exit_code != null ? `<span class="tag">${escapeHtml(`退出码 ${status.last_exit_code}`)}</span>` : ""}
|
||||
${status.url_info?.base_url ? `<a class="tag" href="${escapeHtml(status.url_info.base_url)}" target="_blank" rel="noreferrer">打开服务</a>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-item compact">
|
||||
<h4>最近文件</h4>
|
||||
<p>文件沿用当前租户视图,支持直接打开查看。</p>
|
||||
<div class="list" style="margin-top:10px;">
|
||||
${files.slice(0, 5).map((file) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(file.title || file.name || file.relative_path || "录像文件")}</h4>
|
||||
<p>${escapeHtml(file.relative_path || file.name || file.content_url || "-")}</p>
|
||||
<div class="task-meta">
|
||||
${file.mtime ? `<span class="tag blue">${escapeHtml(formatDateTime(file.mtime))}</span>` : ""}
|
||||
${file.id ? `<span class="tag clickable-tag" data-action="open-live-recorder-file" data-file-id="${escapeHtml(file.id)}">打开文件</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>还没有文件</h4><p>开始录制后,最新文件会出现在这里。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAdminFixRunsPanel() {
|
||||
if (!isSuperAdmin()) return "";
|
||||
const overview = appState.adminOpsOverview || {};
|
||||
const items = safeArray(appState.adminFixRuns.length ? appState.adminFixRuns : overview.recent_fix_runs);
|
||||
if (!items.length) {
|
||||
return `
|
||||
<div class="panel pad" style="margin-top:14px;">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>修复计划列表</h3>
|
||||
<div class="panel-subtitle">还没有拉到修复计划</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-item"><h4>暂无修复计划</h4><p>生成修复计划后,这里会展示完整的 audit 列表。</p></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="panel pad" style="margin-top:14px;">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>修复计划列表</h3>
|
||||
<div class="panel-subtitle">完整展示最近的 fix runs,并支持直接审计</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(formatNumber(items.length))} 条</span>
|
||||
<span class="tag">${escapeHtml(formatNumber(items.filter((item) => item.audit_status === "approved").length))} 已通过</span>
|
||||
<span class="tag orange">${escapeHtml(formatNumber(items.filter((item) => item.audit_status === "watching").length))} 观察中</span>
|
||||
<span class="tag clickable-tag" data-action="refresh-data">刷新</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
${items.map((item) => {
|
||||
const plan = item.plan || {};
|
||||
const verification = item.verification || {};
|
||||
return `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(plan.summary || item.id || "修复计划")}</h4>
|
||||
<p>${escapeHtml(brief(safeArray(plan.steps).join(";") || verification.summary || "暂无修复步骤", 140))}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(item.plan_scope || "plan")}</span>
|
||||
<span class="tag ${item.audit_status === "approved" ? "green" : item.audit_status === "rejected" ? "red" : "orange"}">${escapeHtml(item.audit_status || "pending")}</span>
|
||||
${item.status ? `<span class="tag">${escapeHtml(item.status)}</span>` : ""}
|
||||
${item.incident_id ? `<span class="tag">${escapeHtml(brief(item.incident_id, 12))}</span>` : ""}
|
||||
${item.updated_at ? `<span class="tag">${escapeHtml(formatDateTime(item.updated_at))}</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-admin-fix-run-detail" data-run-id="${escapeHtml(item.id)}">查看详情</span>
|
||||
<span class="tag clickable-tag" data-action="open-admin-fix-run-audit" data-run-id="${escapeHtml(item.id)}">审计放行</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDashboardScreen() {
|
||||
if (!appState.session) {
|
||||
return screenShell(
|
||||
@@ -3101,7 +3466,7 @@ function renderDiscoveryScreen() {
|
||||
const selectedPlatform = getAccountPlatform(selected);
|
||||
const effectivePlatform = selectedPlatform || currentPlatform;
|
||||
const workbenchReason = !isWorkbenchPlatform(effectivePlatform) ? getPendingWorkbenchReason(effectivePlatform) : "";
|
||||
const reports = safeArray(appState.selectedWorkspace?.recent_reports);
|
||||
const reports = safeArray(appState.analysisReports.length ? appState.analysisReports : appState.selectedWorkspace?.recent_reports);
|
||||
const linkedAccounts = safeArray(appState.selectedWorkspace?.linked_accounts);
|
||||
const videos = safeArray(appState.selectedVideos?.items);
|
||||
const fallbackVideos = safeArray(selected?.video_summary?.videos);
|
||||
@@ -3281,6 +3646,7 @@ function renderDiscoveryScreen() {
|
||||
`).join("") || `<div class="task-item"><h4>还没有最近作品</h4><p>当前账号只同步了基础信息,还没拉到完整作品列表。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
${renderDouyinInsightPanel()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-stack">
|
||||
@@ -3673,6 +4039,9 @@ function renderProductionScreen() {
|
||||
<span class="tag green">AI 视频 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:18px;">
|
||||
${renderLiveRecorderManagementPanel()}
|
||||
</div>
|
||||
<div class="layout-grid grid-main" style="margin-top:18px;">
|
||||
<div class="side-stack">
|
||||
<div class="panel pad">
|
||||
@@ -5087,12 +5456,59 @@ function openAdminRepairPlanAction(incidentId) {
|
||||
});
|
||||
}
|
||||
|
||||
function openAdminFixRunDetailAction(runId) {
|
||||
if (!isSuperAdmin()) {
|
||||
alert("只有平台管理者才能查看修复计划。");
|
||||
return;
|
||||
}
|
||||
const run = safeArray(appState.adminFixRuns.length ? appState.adminFixRuns : appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId);
|
||||
if (!run) {
|
||||
alert("没有找到这条修复计划。");
|
||||
return;
|
||||
}
|
||||
openActionModal({
|
||||
title: "修复计划详情",
|
||||
description: "查看这条修复计划的完整上下文,再决定是否放行。",
|
||||
hideSubmit: true,
|
||||
fields: [
|
||||
{
|
||||
type: "html",
|
||||
label: "详情",
|
||||
html: `
|
||||
<div class="sheet-html">
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(run.plan?.summary || run.id)}</h4>
|
||||
<p>${escapeHtml(safeArray(run.plan?.steps).join(";") || "暂无步骤")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">${escapeHtml(run.plan_scope || "plan")}</span>
|
||||
<span class="tag ${run.audit_status === "approved" ? "green" : run.audit_status === "rejected" ? "red" : "orange"}">${escapeHtml(run.audit_status || "pending")}</span>
|
||||
${run.status ? `<span class="tag">${escapeHtml(run.status)}</span>` : ""}
|
||||
${run.incident_id ? `<span class="tag">${escapeHtml(brief(run.incident_id, 12))}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-col" style="margin-top:12px;">
|
||||
<div class="task-item compact">
|
||||
<h4>Plan</h4>
|
||||
<pre style="margin:0; white-space:pre-wrap; font-family:inherit; color:var(--muted); line-height:1.55;">${escapeHtml(JSON.stringify(run.plan || {}, null, 2))}</pre>
|
||||
</div>
|
||||
<div class="task-item compact">
|
||||
<h4>Verification</h4>
|
||||
<pre style="margin:0; white-space:pre-wrap; font-family:inherit; color:var(--muted); line-height:1.55;">${escapeHtml(JSON.stringify(run.verification || {}, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function openAdminFixRunAuditAction(runId) {
|
||||
if (!isSuperAdmin()) {
|
||||
alert("只有平台管理者才能审计修复计划。");
|
||||
return;
|
||||
}
|
||||
const run = safeArray(appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId);
|
||||
const run = safeArray(appState.adminFixRuns.length ? appState.adminFixRuns : appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId);
|
||||
if (!run) {
|
||||
alert("没有找到这条修复计划。");
|
||||
return;
|
||||
@@ -5110,7 +5526,12 @@ function openAdminFixRunAuditAction(runId) {
|
||||
<div class="sheet-html">
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(run.plan?.summary || run.id)}</h4>
|
||||
<p>${escapeHtml((run.plan?.steps || []).join(";") || "暂无步骤")}</p>
|
||||
<p>${escapeHtml(safeArray(run.plan?.steps).join(";") || "暂无步骤")}</p>
|
||||
<div class="task-meta" style="margin-top:10px;">
|
||||
${run.incident_id ? `<span class="tag">事件 ${escapeHtml(brief(run.incident_id, 12))}</span>` : ""}
|
||||
${run.updated_at ? `<span class="tag blue">${escapeHtml(formatDateTime(run.updated_at))}</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-admin-fix-run-detail" data-run-id="${escapeHtml(run.id)}">查看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -5357,11 +5778,19 @@ function openCreateRealCutAction(defaults = {}) {
|
||||
}
|
||||
|
||||
function openLiveRecorderAction() {
|
||||
setScreen("production");
|
||||
renderAll();
|
||||
window.requestAnimationFrame(() => {
|
||||
document.getElementById("live-recorder-maintenance-anchor")?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
}
|
||||
|
||||
function openLiveRecorderCreateAction() {
|
||||
const status = getIntegrationDetail("live_recorder");
|
||||
const project = getSelectedProject() || appState.dashboard?.projects?.[0] || null;
|
||||
const assistants = getAssistantOptions(project?.id || "");
|
||||
openActionModal({
|
||||
title: "直播录制控制",
|
||||
title: "新增录制源",
|
||||
description: status.reachable
|
||||
? "新增的是你当前租户名下的录制源。文件访问和录制状态也只会回到你的账号视图里。"
|
||||
: "当前 NAS 录制服务不可达,先检查集成健康。",
|
||||
@@ -5404,6 +5833,133 @@ function openLiveRecorderAction() {
|
||||
});
|
||||
}
|
||||
|
||||
function openLiveRecorderSourceAction(sourceId) {
|
||||
const source = safeArray(appState.liveRecorderSources).find((item) => item.id === sourceId);
|
||||
if (!source) {
|
||||
alert("没有找到这条录制源。");
|
||||
return;
|
||||
}
|
||||
const currentProject = getSelectedProject() || safeArray(appState.dashboard?.projects).find((item) => item.id === source.project_id) || appState.dashboard?.projects?.[0] || null;
|
||||
const assistants = getAssistantOptions(currentProject?.id || source.project_id || "");
|
||||
openActionModal({
|
||||
title: "编辑录制源",
|
||||
description: "可以更新项目归属、Agent、标题、清晰度和启停状态;链接本身若要变更,请删除后重建。",
|
||||
submitLabel: "保存修改",
|
||||
fields: [
|
||||
{
|
||||
type: "html",
|
||||
label: "源信息",
|
||||
html: `
|
||||
<div class="sheet-html">
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(source.title || source.remote_name || "录制源")}</h4>
|
||||
<p>${escapeHtml(source.source_url || "暂无链接")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag">${escapeHtml(platformLabel(source.platform || "kuaishou"))}</span>
|
||||
<span class="tag">${escapeHtml(source.quality || "原画")}</span>
|
||||
<span class="tag ${source.enabled ? "green" : "orange"}">${escapeHtml(source.enabled ? "启用" : "停用")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{ name: "projectId", label: "归属项目", type: "select", value: source.project_id || currentProject?.id || "", options: getProjectOptions() },
|
||||
{ name: "assistantId", label: "关联 Agent", type: "select", value: source.assistant_id || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] },
|
||||
{ name: "title", label: "录制名称", value: source.title || "", placeholder: "例如:A 类目直播跟踪" },
|
||||
{ name: "quality", label: "清晰度", type: "select", value: source.quality || "原画", options: ["原画", "蓝光", "超清", "高清", "标清", "流畅"].map((item) => ({ value: item, label: item })) },
|
||||
{ name: "enabled", label: "启用录制源", type: "checkbox", value: Boolean(source.enabled) }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch(`/v2/live-recorder/sources/${encodeURIComponent(source.id)}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
project_id: values.projectId || "",
|
||||
assistant_id: values.assistantId || "",
|
||||
title: values.title || "",
|
||||
quality: values.quality || "原画",
|
||||
enabled: Boolean(values.enabled)
|
||||
}
|
||||
});
|
||||
rememberAction("录制源已更新", `已保存「${saved.item?.title || source.title || "录制源"}」。`, "green", saved);
|
||||
await bootstrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openLiveRecorderImportAction() {
|
||||
const samples = [
|
||||
"https://live.douyin.com/1234567890",
|
||||
"# 关闭的源会以 # 开头",
|
||||
"高清, https://live.kuaishou.com/u/abcdef, 测试录制源"
|
||||
].join("\n");
|
||||
openActionModal({
|
||||
title: "导入 URL 配置",
|
||||
description: "按行粘贴直播源,支持用逗号附带清晰度和标题,注释行会被视为停用源。",
|
||||
submitLabel: "导入并同步",
|
||||
fields: [
|
||||
{
|
||||
name: "raw",
|
||||
label: "配置文本",
|
||||
type: "textarea",
|
||||
rows: 10,
|
||||
value: samples,
|
||||
placeholder: "一行一个 URL,支持 # 注释和 逗号分隔的清晰度/标题"
|
||||
}
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
if (!String(values.raw || "").trim()) throw new Error("请先粘贴配置文本");
|
||||
const saved = await storyforgeFetch("/v2/live-recorder/url-config/import", {
|
||||
method: "POST",
|
||||
body: { raw: values.raw }
|
||||
});
|
||||
rememberAction("URL 配置已导入", `已导入 ${formatNumber(saved.count || 0)} 条录制源。`, "green", saved);
|
||||
await bootstrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleLiveRecorderSourceAction(sourceId, nextEnabled) {
|
||||
const source = safeArray(appState.liveRecorderSources).find((item) => item.id === sourceId);
|
||||
if (!source) {
|
||||
alert("没有找到这条录制源。");
|
||||
return;
|
||||
}
|
||||
setBusy(true, nextEnabled ? "正在启用录制源..." : "正在停用录制源...");
|
||||
try {
|
||||
await storyforgeFetch(`/v2/live-recorder/sources/${encodeURIComponent(source.id)}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
enabled: Boolean(nextEnabled)
|
||||
}
|
||||
});
|
||||
rememberAction(nextEnabled ? "录制源已启用" : "录制源已停用", `${source.title || source.source_url || "录制源"} 已更新。`, "green");
|
||||
await bootstrap();
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLiveRecorderSourceAction(sourceId) {
|
||||
const source = safeArray(appState.liveRecorderSources).find((item) => item.id === sourceId);
|
||||
if (!source) {
|
||||
alert("没有找到这条录制源。");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确认删除「${source.title || source.source_url || "录制源"}」吗?删除后需要重新导入。`)) {
|
||||
return;
|
||||
}
|
||||
setBusy(true, "正在删除录制源...");
|
||||
try {
|
||||
await storyforgeFetch(`/v2/live-recorder/sources/${encodeURIComponent(source.id)}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
rememberAction("录制源已删除", `${source.title || source.source_url || "录制源"} 已从租户视图中移除。`, "green");
|
||||
await bootstrap();
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
async function openLiveRecorderFileAction(fileId) {
|
||||
const target = safeArray(appState.liveRecorderFiles).find((item) => item.id === fileId);
|
||||
if (!target?.content_url) {
|
||||
@@ -5608,6 +6164,26 @@ document.addEventListener("click", async (event) => {
|
||||
openLiveRecorderAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-live-recorder-create") {
|
||||
openLiveRecorderCreateAction();
|
||||
return;
|
||||
}
|
||||
if (name === "import-live-recorder-config") {
|
||||
openLiveRecorderImportAction();
|
||||
return;
|
||||
}
|
||||
if (name === "edit-live-recorder-source") {
|
||||
openLiveRecorderSourceAction(action.dataset.sourceId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "toggle-live-recorder-source") {
|
||||
await toggleLiveRecorderSourceAction(action.dataset.sourceId || "", action.dataset.nextEnabled === "true");
|
||||
return;
|
||||
}
|
||||
if (name === "delete-live-recorder-source") {
|
||||
await deleteLiveRecorderSourceAction(action.dataset.sourceId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "open-live-recorder-file") {
|
||||
await openLiveRecorderFileAction(action.dataset.fileId || "");
|
||||
return;
|
||||
@@ -5836,6 +6412,14 @@ document.addEventListener("click", async (event) => {
|
||||
openAdminFixRunAuditAction(action.dataset.runId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "open-admin-fix-run-detail") {
|
||||
openAdminFixRunDetailAction(action.dataset.runId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "select-douyin-snapshot") {
|
||||
await openDouyinSnapshotDetailAction(action.dataset.snapshotId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "job-to-ai-video") {
|
||||
const jobId = action.dataset.jobId || "";
|
||||
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
|
||||
|
||||
Reference in New Issue
Block a user