Files
storyforge/web/storyforge-web-v4/assets/app.js

10457 lines
489 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const STORAGE_KEY = "storyforge-web-v4-session";
const SESSION_STORE = StoryForgeSessionStore.create(STORAGE_KEY);
const DEFAULT_BACKEND_URL = StoryForgeApiClient.detectDefaultBackendUrl();
const RECOVERY_HISTORY_KEY = STORAGE_KEY + ":recovery-history";
const navButtons = document.querySelectorAll("[data-screen-target]");
const screens = Array.from(document.querySelectorAll("[data-screen]"));
const screenMap = Object.fromEntries(screens.map((screen) => [screen.dataset.screen, screen]));
const mobileScreenTitle = document.querySelector('[data-role="mobile-screen-title"]');
const mobileProjectTitle = document.querySelector('[data-role="mobile-project-title"]');
const mobileShellStatus = document.querySelector(".mobile-shell-status");
const SCREEN_LABELS = {
dashboard: "项目总台",
intake: "我的项目",
discovery: "找对标",
tracking: "跟踪账号",
owned: "我的账号",
playbook: "Agent",
strategy: "我的策略",
production: "生产中心",
review: "发布与复盘",
automation: "自动流程",
credits: "额度",
settings: "设置",
"admin-workbench": "管理员配置台"
};
const appState = {
screen: window.location.hash.replace("#", "") || "dashboard",
session: SESSION_STORE.loadStoredSession(),
me: null,
dashboard: null,
contentSources: [],
accounts: [],
selectedAccountId: "",
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") || "",
selectedProjectId: "",
dashboardOverviewTab: "project_progress",
discoveryDetailTab: "overview",
playbookDetailTab: "workspace",
strategyDetailTab: "effective",
productionDetailTab: "queue",
automationDetailTab: "health",
adminWorkbenchTab: "integrations",
settingsDetailTab: "workspace",
selectedAssistantId: "",
lastSeenAt: SESSION_STORE.getLastSeenAt(Date.now()),
trackingCursorMap: {},
trackingAccounts: [],
trackingDigest: null,
reviews: [],
liveRecorderSources: [],
liveRecorderStatus: null,
liveRecorderFiles: [],
liveRecorderHealth: null,
storageStatus: null,
integrationHealth: null,
localModelCatalog: null,
backendCapabilities: null,
onelinerProfile: null,
onelinerSessions: [],
selectedOnelinerSessionId: "",
onelinerRuns: [],
onelinerRunFilter: "focus",
selectedOnelinerRunId: "",
lastCompletedOnelinerRunId: "",
onelinerMessages: [],
onelinerActionRegistry: [],
platformAgents: [],
onelinerGovernanceEffective: null,
userGlobalPolicy: null,
userCurrentPlatformPolicy: null,
userPolicyAudits: [],
adminSystemMainPolicy: null,
adminSystemPlatformPolicies: [],
adminGovernanceDirectory: null,
adminOverrideTarget: null,
adminOverridePolicy: null,
adminPolicyAudits: [],
tenantQuota: null,
tenantUsage: null,
adminOpsOverview: null,
adminFixRuns: [],
recoveryRecords: [],
autoConnectAttempted: false,
autoConnectSuppressed: false,
autoConnectError: "",
dashboardActionReason: null,
busy: false,
message: "",
lastAction: null,
mainAgentLanding: null,
lastGeneratedCopy: null,
lastSimilaritySearch: null,
lastJobDetail: null
};
let PLATFORM_RUNTIME = null;
let onelinerRunPollTimer = null;
const API_CLIENT = StoryForgeApiClient.create({
getSession: () => appState.session,
defaultBackendUrl: DEFAULT_BACKEND_URL,
getCapabilities: () => appState.backendCapabilities
});
const INTEGRATION_ORDER = ["local_model", "live_recorder", "cutvideo", "huobao", "n8n", "asr"];
const ACTIVE_PLATFORMS = [
{ value: "douyin", label: "抖音" },
{ value: "xiaohongshu", label: "小红书" },
{ value: "bilibili", label: "哔哩哔哩" },
{ value: "kuaishou", label: "快手" },
{ value: "wechat_video", label: "微信视频号" }
];
const makePlatformRoutes = StoryForgePlatformRuntime.makePlatformRoutes;
const PLATFORM_REGISTRY = {
douyin: {
label: "抖音",
shortLabel: "抖音",
workbenchReady: true,
routes: makePlatformRoutes("douyin")
},
xiaohongshu: {
label: "小红书",
shortLabel: "小红书",
workbenchReady: true,
routes: makePlatformRoutes("xiaohongshu")
},
bilibili: {
label: "哔哩哔哩",
shortLabel: "B站",
workbenchReady: true,
routes: makePlatformRoutes("bilibili")
},
kuaishou: {
label: "快手",
shortLabel: "快手",
workbenchReady: true,
routes: makePlatformRoutes("kuaishou")
},
wechat_video: {
label: "微信视频号",
shortLabel: "视频号",
workbenchReady: true,
routes: makePlatformRoutes("wechat_video")
}
};
PLATFORM_RUNTIME = StoryForgePlatformRuntime.create({
activePlatforms: ACTIVE_PLATFORMS,
platformRegistry: PLATFORM_REGISTRY,
appState,
storage: localStorage,
storageKey: STORAGE_KEY
});
const INTEGRATION_META = {
local_model: {
label: "本机模型",
hint: "OpenAI-compatible",
impacts: ["账号分析", "高分分析", "文案生成"]
},
live_recorder: {
label: "直播录制",
hint: "fnOS NAS",
impacts: ["直播源导入", "录制控制"]
},
cutvideo: {
label: "自动剪辑",
hint: "cutvideo 直连",
impacts: ["实拍剪辑"]
},
huobao: {
label: "AI 视频",
hint: "huobao-drama",
impacts: ["AI 视频"]
},
n8n: {
label: "编排",
hint: "n8n workflow",
impacts: ["AI 视频", "实拍剪辑", "自动链路"]
},
asr: {
label: "ASR",
hint: "素材转写",
impacts: ["分析转写"]
}
};
const PIPELINE_GUARDS = {
aiVideo: {
label: "AI 视频",
openAction: "open-ai-video",
jobAction: "job-to-ai-video",
dependencies: ["n8n", "huobao"]
},
realCut: {
label: "实拍剪辑",
openAction: "open-real-cut",
jobAction: "job-to-real-cut",
dependencies: ["n8n", "cutvideo"]
}
};
const ONELINER_INTENT_LABELS = {
create_project: "创建项目",
create_assistant: "创建 Agent",
import_homepage: "导入主页",
track_account: "跟踪账号",
analyze_account: "分析账号",
analyze_top_videos: "分析高分作品",
generate_copy: "生成文案",
ai_video: "AI 视频",
real_cut: "实拍剪辑",
review: "发布复盘",
live_recorder: "直播录制",
storage_status: "存储状态",
ops_admin: "运维巡检",
custom: "自定义任务"
};
function safeArray(value) {
return Array.isArray(value) ? value : [];
}
function parseJsonSafe(value, fallback) {
if (typeof value !== "string" || !value.trim()) return fallback;
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
function getRuntimePlatformValues() {
return PLATFORM_RUNTIME.getRuntimePlatformValues();
}
function getPlatformOptions() {
return PLATFORM_RUNTIME.getPlatformOptions();
}
function normalizePlatformValue(value, fallback = "douyin") {
return PLATFORM_RUNTIME.normalizePlatformValue(value, fallback);
}
function platformLabel(value) {
return PLATFORM_RUNTIME.platformLabel(value);
}
function getPlatformMeta(value) {
return PLATFORM_RUNTIME.getPlatformMeta(value);
}
function getPlatformShortLabel(value) {
return PLATFORM_RUNTIME.getPlatformShortLabel(value);
}
function getPlatformChips() {
return PLATFORM_RUNTIME.getPlatformChips();
}
function isWorkbenchPlatform(value) {
return PLATFORM_RUNTIME.isWorkbenchPlatform(value);
}
function getWorkbenchRoute(platform, key, ...args) {
return PLATFORM_RUNTIME.getWorkbenchRoute(platform, key, ...args);
}
function setCurrentPlatform(value) {
return PLATFORM_RUNTIME.setCurrentPlatform(value);
}
function getAccountPlatform(account) {
return PLATFORM_RUNTIME.getAccountPlatform(account);
}
function getAccountHandle(account) {
return PLATFORM_RUNTIME.getAccountHandle(account);
}
function getAccountProfileUrl(account) {
return PLATFORM_RUNTIME.getAccountProfileUrl(account);
}
function getAccountName(account) {
return PLATFORM_RUNTIME.getAccountName(account);
}
function getAccountSubtitle(account) {
return PLATFORM_RUNTIME.getAccountSubtitle(account);
}
function getPendingWorkbenchReason(platform) {
return PLATFORM_RUNTIME.getPendingWorkbenchReason(platform);
}
function getAccountWorkbenchGate(account) {
return PLATFORM_RUNTIME.getAccountWorkbenchGate(account);
}
function getPreferredPlatform() {
return PLATFORM_RUNTIME.getPreferredPlatform();
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function brief(value, max = 88) {
const text = String(value ?? "").trim();
if (!text) return "暂无";
return text.length > max ? text.slice(0, max).trimEnd() + "…" : text;
}
function formatNumber(value) {
const num = Number(value || 0);
if (!Number.isFinite(num)) return "-";
if (num >= 100000000) return (num / 100000000).toFixed(1).replace(/\.0$/, "") + "亿";
if (num >= 10000) return (num / 10000).toFixed(1).replace(/\.0$/, "") + "w";
if (num >= 1000) return num.toLocaleString("zh-CN");
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);
if (Number.isNaN(date.getTime())) return String(value);
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
}).format(date);
}
function formatDate(value) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit"
}).format(date);
}
function daysSince(value) {
if (!value) return "-";
const time = new Date(value).getTime();
if (!Number.isFinite(time)) return "-";
const diff = Date.now() - time;
return Math.max(0, Math.floor(diff / 86400000));
}
function initials(value) {
const raw = String(value || "").trim();
if (!raw) return "SF";
return raw.slice(0, 2).toUpperCase();
}
function statusTone(status) {
const normalized = String(status || "").toLowerCase();
if (["completed", "ready", "approved", "ok"].includes(normalized)) return "green";
if (["failed", "error", "rejected"].includes(normalized)) return "red";
if (["worth_scaling", "good_reference"].includes(normalized)) return "green";
if (["needs_rework"].includes(normalized)) return "red";
if (["hold"].includes(normalized)) return "orange";
if (["running", "processing", "pending", "queued"].includes(normalized)) return "orange";
return "blue";
}
function readRecoveryRecords() {
try {
const raw = localStorage.getItem(RECOVERY_HISTORY_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return safeArray(parsed);
} catch {
return [];
}
}
function persistRecoveryRecords(records) {
try {
localStorage.setItem(RECOVERY_HISTORY_KEY, JSON.stringify(safeArray(records).slice(0, 40)));
} catch {}
}
function normalizeRecoveryRecord(record) {
if (!record || typeof record !== "object") return null;
return {
id: String(record.id || `recovery_${Date.now()}`),
created_at: record.created_at || new Date().toISOString(),
account_id: String(record.account_id || ""),
project_id: String(record.project_id || ""),
job_id: String(record.job_id || ""),
job_title: String(record.job_title || ""),
job_line_type: String(record.job_line_type || ""),
job_source_type: String(record.job_source_type || ""),
job_status: String(record.job_status || "failed"),
action_key: String(record.action_key || "retry-job"),
mode: String(record.mode || "single"),
summary: String(record.summary || ""),
reason: String(record.reason || ""),
target_job_id: String(record.target_job_id || ""),
target_job_title: String(record.target_job_title || ""),
target_status: String(record.target_status || ""),
result_label: String(record.result_label || ""),
result_reason: String(record.result_reason || ""),
user_feedback: String(record.user_feedback || "")
};
}
function getRecoveryRecords() {
const accountId = String(appState.session?.account?.id || "");
return readRecoveryRecords()
.map((item) => normalizeRecoveryRecord(item))
.filter(Boolean)
.filter((item) => !accountId || item.account_id === accountId)
.sort((left, right) => compareDateDesc(left.created_at, right.created_at));
}
function recordRecoveryEvent(record) {
const normalized = normalizeRecoveryRecord({
...record,
account_id: record.account_id || appState.session?.account?.id || "",
project_id: record.project_id || appState.selectedProjectId || "",
created_at: record.created_at || new Date().toISOString()
});
if (!normalized) return null;
const nextRecords = [normalized, ...readRecoveryRecords()]
.filter((item, index, list) => item && list.findIndex((candidate) => candidate.id === item.id) === index)
.sort((left, right) => compareDateDesc(left.created_at, right.created_at))
.slice(0, 40);
persistRecoveryRecords(nextRecords);
appState.recoveryRecords = getRecoveryRecords();
return normalized;
}
function clearRecoveryRecords() {
appState.recoveryRecords = [];
}
function isQuotaRelatedMessage(message) {
const text = String(message || "");
return /配额|额度|预算|storage_over_limit|quota/i.test(text);
}
function describeActionError(error, fallbackTitle = "执行失败") {
const raw = String(error?.message || error || "").trim();
const status = Number(error?.status || 0);
const result = {
title: fallbackTitle,
summary: raw || "请稍后重试",
tone: status === 403 ? "orange" : "red",
hint: ""
};
if (!raw) return result;
const quotaPatterns = [
{
pattern: /analysis 配额已用完/i,
title: "分析额度已用完",
summary: "当前租户本周期的分析额度已经用完,暂时不能继续发起分析类任务。",
hint: "可以先补额度,或者等待下个周期再恢复。"
},
{
pattern: /copy 配额已用完/i,
title: "文案额度已用完",
summary: "当前租户本周期的文案额度已经用完,暂时不能继续生成文案。",
hint: "可以先补额度,或者改用已有产物继续复盘。"
},
{
pattern: /ai_video 配额已用完/i,
title: "AI 视频额度已用完",
summary: "当前租户本周期的 AI 视频额度已经用完,暂时不能继续发起 AI 视频任务。",
hint: "可以先补额度,再恢复这条任务。"
},
{
pattern: /real_cut 配额已用完/i,
title: "实拍剪辑额度已用完",
summary: "当前租户本周期的实拍剪辑额度已经用完,暂时不能继续发起剪辑任务。",
hint: "可以先补额度,或者改为只做分析类恢复。"
},
{
pattern: /recorder 配额已用完/i,
title: "录制额度已用完",
summary: "当前租户本周期的录制额度已经用完,暂时不能新增直播录制源。",
hint: "可以先补额度,再继续新增录制源。"
},
{
pattern: /预算不足|monthly budget/i,
title: "本周期预算不足",
summary: "当前租户本周期预算已经不够继续执行这项动作。",
hint: "可以先扩容预算,再重新发起恢复。"
},
{
pattern: /存储额度已满|storage_over_limit/i,
title: "存储额度已满",
summary: "当前租户存储额度已触顶,系统已经停止继续产生大文件缓存。",
hint: "建议先清理旧产物或提升存储上限。"
}
];
for (const item of quotaPatterns) {
if (item.pattern.test(raw)) {
return {
...result,
title: item.title,
summary: item.summary,
hint: item.hint,
tone: "orange"
};
}
}
if (status === 403 && /permission|not allowed|required|admin/i.test(raw)) {
return {
...result,
title: "权限不足",
summary: "当前账号没有执行这项动作的权限。",
hint: "请确认账号审批状态,或者切换到超级管理员账号。",
tone: "orange"
};
}
if (status === 409) {
return {
...result,
title: "任务状态冲突",
summary: raw,
hint: "请刷新后重新查看当前任务状态。",
tone: "orange"
};
}
if (status === 404) {
return {
...result,
title: "资源不存在",
summary: raw,
hint: "请刷新页面后再试一次。",
tone: "orange"
};
}
if (isQuotaRelatedMessage(raw)) {
return {
...result,
title: "额度拦截",
summary: raw,
hint: "系统已经阻止继续执行,先补额度或清理存储再恢复。",
tone: "orange"
};
}
return result;
}
function formatActionErrorMessage(error, fallbackTitle = "执行失败") {
const detail = describeActionError(error, fallbackTitle);
return [detail.title, detail.summary, detail.hint].filter(Boolean).join(" · ");
}
function presentActionFailure(error, fallbackTitle = "执行失败") {
const detail = describeActionError(error, fallbackTitle);
rememberAction(detail.title, [detail.summary, detail.hint].filter(Boolean).join(" · "), detail.tone);
renderAll();
return detail;
}
function loadStoredSession() {
return SESSION_STORE.loadStoredSession();
}
function persistSession(session) {
appState.session = session;
SESSION_STORE.persistSession(session);
}
function setLastSeenAt(value) {
appState.lastSeenAt = SESSION_STORE.setLastSeenAt(value);
}
function normalizeBackendUrlValue(value) {
return String(value || "").trim().replace(/\/$/, "");
}
function hasSessionBackendMismatch(expectedBackendUrl = DEFAULT_BACKEND_URL) {
if (!appState.session) return false;
const expected = normalizeBackendUrlValue(expectedBackendUrl);
const current = normalizeBackendUrlValue(appState.session.backendUrl);
return Boolean(expected && current && expected !== current);
}
function compareDateDesc(leftValue, rightValue) {
return new Date(rightValue || 0).getTime() - new Date(leftValue || 0).getTime();
}
function buildAssistantNameMap(items) {
return new Map(
safeArray(items)
.filter((item) => item?.id)
.map((item) => [item.id, item.name || ""])
);
}
function getTrackingCursorForPlatform(platform = getCurrentPlatformValue()) {
const normalizedPlatform = normalizePlatformValue(platform, "");
return appState.trackingCursorMap?.[normalizedPlatform] || "";
}
function getTrackingSinceIso(platform = getCurrentPlatformValue()) {
const cursor = getTrackingCursorForPlatform(platform) || appState.lastSeenAt || Date.now();
const date = new Date(cursor);
if (Number.isNaN(date.getTime())) return new Date(Date.now() - 86400000).toISOString();
return date.toISOString();
}
function enrichTrackingAccounts(items, accountIndex, assistantNameMap, fallbackPlatform) {
return safeArray(items)
.map((item) => {
const account = accountIndex.get(item.tracked_account_id) || item.account || null;
const normalizedPlatform = normalizePlatformValue(
item.platform || account?.platform || fallbackPlatform || "",
fallbackPlatform || "douyin"
);
return {
...item,
platform: normalizedPlatform,
assistant_name: item.assistant_name || assistantNameMap.get(item.assistant_id) || "",
account
};
})
.sort((left, right) => compareDateDesc(left.updated_at || left.created_at, right.updated_at || right.created_at));
}
function enrichTrackingDigestItems(items, accountIndex, fallbackPlatform) {
return safeArray(items)
.map((item) => {
const account = item.account || accountIndex.get(item.tracked_account_id) || null;
const platform = normalizePlatformValue(
item.platform || item.video?.platform || account?.platform || fallbackPlatform || "",
fallbackPlatform || "douyin"
);
const performanceScore = Number(item.video?.score?.performance_score || 0);
return {
...item,
platform,
account,
summary: item.summary || item.summary_text || item.video?.description || item.video?.title || "已发现更新内容",
is_high_value: typeof item.is_high_value === "boolean" ? item.is_high_value : performanceScore >= 60
};
})
.sort((left, right) => compareDateDesc(
left.video?.published_at || left.created_at,
right.video?.published_at || right.created_at
));
}
function markSeenNow() {
setLastSeenAt(Date.now());
}
function setBusy(next, message = "") {
appState.busy = next;
appState.message = message;
renderAuthUi();
}
function getScreenLabel(screenId = appState.screen) {
return SCREEN_LABELS[screenId] || "StoryForge";
}
function getMobileProjectLabel() {
const selectedProject = typeof getSelectedProject === "function" ? getSelectedProject() : null;
if (selectedProject?.name) return selectedProject.name;
if (appState.selectedWorkspace?.project?.name) return appState.selectedWorkspace.project.name;
if (appState.selectedWorkspace?.account?.display_name) return appState.selectedWorkspace.account.display_name;
return appState.me?.display_name || appState.me?.username || "当前工作区";
}
function setMobileSidebarOpen(next) {
document.body.classList.toggle("mobile-sidebar-open", Boolean(next));
}
function syncMobileShell() {
if (mobileScreenTitle) mobileScreenTitle.textContent = getScreenLabel();
if (mobileProjectTitle) mobileProjectTitle.textContent = getMobileProjectLabel();
if (mobileShellStatus) {
mobileShellStatus.textContent = appState.busy ? "同步中" : (appState.session ? "已连接" : "连接状态");
}
}
function getScreenFromHash() {
const next = window.location.hash.replace("#", "");
return screenMap[next] ? next : "dashboard";
}
function setScreen(id, options = {}) {
const { updateHash = true } = options;
const resolvedId = screenMap[id] ? id : "dashboard";
setMobileSidebarOpen(false);
appState.screen = resolvedId;
navButtons.forEach((button) => {
const active = button.dataset.screenTarget === resolvedId;
button.classList.toggle("is-active", active);
});
screens.forEach((screen) => {
screen.classList.toggle("is-active", screen.dataset.screen === resolvedId);
});
if (updateHash && window.location.hash !== `#${resolvedId}`) {
window.location.hash = resolvedId;
}
syncMobileShell();
}
function ensureAuthUi() {
const topbarRight = document.querySelector(".topbar-right");
if (!topbarRight) return;
if (!document.querySelector(".auth-inline")) {
const inline = document.createElement("div");
inline.className = "auth-inline";
inline.innerHTML = `
<button class="btn btn-secondary" type="button" data-action="open-auth">连接状态</button>
<button class="btn btn-secondary" type="button" data-action="logout-session">退出</button>
<span class="auth-status"></span>
`;
topbarRight.insertBefore(inline, topbarRight.firstChild);
}
if (!document.querySelector(".auth-modal")) {
const modal = document.createElement("div");
modal.className = "auth-modal-backdrop hidden";
modal.innerHTML = `
<div class="auth-modal">
<form class="auth-form" data-role="auth-form">
<div class="auth-head">
<div>
<h3>连接 StoryForge</h3>
<p>当前站点会直接向后端请求自动会话,不再要求用户输入账号密码。</p>
</div>
<button class="btn btn-secondary" type="button" data-action="close-auth">关闭</button>
</div>
<div class="field-stack">
<label>后端地址</label>
<input type="text" data-auth-field="backendUrl" placeholder="${DEFAULT_BACKEND_URL}" autocomplete="url" readonly />
</div>
<div class="task-item compact">
<h4>自动连接说明</h4>
<p>前端只会请求固定后端的自动会话接口;如果当前部署没有开启自动建会话,这里会直接显示服务端返回的原因。</p>
</div>
<div class="helper-text" data-role="auth-message"></div>
<div class="auth-actions">
<button class="btn btn-secondary" type="button" data-action="auth-refresh">重新加载</button>
<button class="btn btn-primary" type="submit" data-action="submit-auth">自动连接</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
}
}
function renderAuthUi() {
ensureAuthUi();
const session = appState.session;
const openButton = document.querySelector('[data-action="open-auth"]');
const logoutButton = document.querySelector('[data-action="logout-session"]');
const status = document.querySelector(".auth-status");
const message = document.querySelector('[data-role="auth-message"]');
if (openButton) openButton.textContent = session ? "连接状态" : "自动连接";
if (logoutButton) logoutButton.hidden = !session;
if (status) {
status.textContent = appState.busy
? appState.message || "正在加载..."
: session
? `${session.account?.display_name || session.account?.username || "已连接"} · ${session.backendUrl}`
: appState.autoConnectError || "等待自动连接";
}
if (message) {
message.textContent = appState.busy ? appState.message : (appState.autoConnectError || "");
}
syncMobileShell();
}
function openAuthModal() {
ensureAuthUi();
const modal = document.querySelector(".auth-modal-backdrop");
if (!modal) return;
const session = appState.session;
modal.classList.remove("hidden");
setAuthField("backendUrl", session?.backendUrl || DEFAULT_BACKEND_URL);
}
function closeAuthModal() {
document.querySelector(".auth-modal-backdrop")?.classList.add("hidden");
}
function setAuthField(name, value) {
const input = document.querySelector(`[data-auth-field="${name}"]`);
if (input) input.value = value ?? "";
}
function readAuthForm() {
return {
backendUrl: document.querySelector('[data-auth-field="backendUrl"]')?.value?.trim() || DEFAULT_BACKEND_URL
};
}
let currentActionConfig = null;
function ensureActionUi() {
if (document.querySelector(".action-modal-backdrop")) return;
const modal = document.createElement("div");
modal.className = "action-modal-backdrop hidden";
modal.innerHTML = `
<div class="action-modal">
<div class="auth-head">
<div>
<h3 data-role="action-title">快速操作</h3>
<p data-role="action-description">根据当前工作区执行动作。</p>
</div>
<button class="btn btn-secondary" type="button" data-action="close-sheet">关闭</button>
</div>
<div class="field-stack" data-role="action-fields"></div>
<div class="helper-text" data-role="action-message"></div>
<div class="auth-actions">
<button class="btn btn-secondary" type="button" data-action="close-sheet">取消</button>
<button class="btn btn-primary" type="button" data-action="submit-sheet">执行</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
function renderActionFields(fields) {
return fields.map((field) => {
const common = `data-action-field="${escapeHtml(field.name)}"`;
if (field.type === "html") {
return `
<div class="field-stack">
<label>${escapeHtml(field.label || "")}</label>
<div class="sheet-html">${field.html || ""}</div>
</div>
`;
}
if (field.type === "textarea") {
return `
<div class="field-stack">
<label>${escapeHtml(field.label)}</label>
<textarea ${common} rows="${escapeHtml(field.rows || 4)}" placeholder="${escapeHtml(field.placeholder || "")}">${escapeHtml(field.value || "")}</textarea>
</div>
`;
}
if (field.type === "select") {
return `
<div class="field-stack">
<label>${escapeHtml(field.label)}</label>
<select ${common}>
${(field.options || []).map((option) => `
<option value="${escapeHtml(option.value)}" ${String(option.value) === String(field.value ?? "") ? "selected" : ""}>${escapeHtml(option.label)}</option>
`).join("")}
</select>
</div>
`;
}
if (field.type === "checkbox") {
return `
<label class="checkbox-row">
<input type="checkbox" ${common} ${field.value ? "checked" : ""} />
<span>${escapeHtml(field.label)}</span>
</label>
`;
}
if (field.type === "file") {
return `
<div class="field-stack">
<label>${escapeHtml(field.label)}</label>
<input type="file" ${common} accept="${escapeHtml(field.accept || "")}" />
</div>
`;
}
return `
<div class="field-stack">
<label>${escapeHtml(field.label)}</label>
<input
type="${escapeHtml(field.type || "text")}"
${common}
value="${escapeHtml(field.value || "")}"
placeholder="${escapeHtml(field.placeholder || "")}"
${field.min != null ? `min="${escapeHtml(field.min)}"` : ""}
${field.max != null ? `max="${escapeHtml(field.max)}"` : ""}
/>
</div>
`;
}).join("");
}
function openActionModal(config) {
ensureActionUi();
currentActionConfig = config;
const modal = document.querySelector(".action-modal-backdrop");
const title = document.querySelector('[data-role="action-title"]');
const description = document.querySelector('[data-role="action-description"]');
const fields = document.querySelector('[data-role="action-fields"]');
const message = document.querySelector('[data-role="action-message"]');
const submit = document.querySelector('[data-action="submit-sheet"]');
if (!modal || !title || !description || !fields || !message || !submit) return;
title.textContent = config.title || "快速操作";
description.textContent = config.description || "";
fields.innerHTML = renderActionFields(config.fields || []);
message.textContent = "";
submit.textContent = config.submitLabel || "执行";
submit.disabled = false;
submit.hidden = Boolean(config.hideSubmit);
modal.classList.remove("hidden");
if (typeof config.onOpen === "function") {
config.onOpen({ modal, title, description, fields, message, submit });
}
}
function closeActionModal() {
currentActionConfig = null;
document.querySelector(".action-modal-backdrop")?.classList.add("hidden");
}
function ensureOneLinerUi() {
if (!document.querySelector(".oneliner-fab")) {
const fab = document.createElement("button");
fab.className = "oneliner-fab";
fab.type = "button";
fab.dataset.action = "open-oneliner";
fab.innerHTML = `
<span class="oneliner-fab-mark">1</span>
<span class="oneliner-fab-text">OneLiner</span>
`;
document.body.appendChild(fab);
}
if (!document.querySelector(".oneliner-backdrop")) {
const panel = document.createElement("div");
panel.className = "oneliner-backdrop hidden";
panel.innerHTML = `
<div class="oneliner-panel">
<div class="oneliner-head">
<div>
<h3>OneLiner</h3>
<p>前端没上的需求,先由总控主 Agent 承接。</p>
</div>
<div class="task-meta">
<button class="btn btn-secondary" type="button" data-action="open-oneliner-profile">配置</button>
<button class="btn btn-secondary" type="button" data-action="close-oneliner">关闭</button>
</div>
</div>
<div class="oneliner-meta" data-role="oneliner-meta"></div>
<div class="oneliner-runs" data-role="oneliner-runs"></div>
<div class="oneliner-sessions" data-role="oneliner-sessions"></div>
<div class="oneliner-messages" data-role="oneliner-messages"></div>
<form class="oneliner-composer" data-role="oneliner-form">
<textarea data-role="oneliner-input" rows="4" placeholder="比如:帮我把这个账号加入跟踪并绑定快手 Agent或者这个需求前端还没有你直接帮我拆任务。"></textarea>
<div class="oneliner-actions">
<div class="helper-text" data-role="oneliner-status"></div>
<button class="btn btn-primary" type="submit" data-action="submit-oneliner">发送给 OneLiner</button>
</div>
</form>
</div>
`;
document.body.appendChild(panel);
}
}
function renderOneLinerSessionTabs() {
const sessions = safeArray(appState.onelinerSessions).slice(0, 6);
if (!sessions.length) {
return `<div class="task-meta"><span class="tag blue">还没有会话</span><span class="tag">发送第一条需求后自动创建</span></div>`;
}
const currentId = getCurrentOneLinerSession()?.id || "";
return `
<div class="chip-row">
${sessions.map((session) => `
<span class="chip clickable-tag ${session.id === currentId ? "active" : ""}" data-action="select-oneliner-session" data-session-id="${escapeHtml(session.id)}">
${escapeHtml(brief(session.title || "新会话", 14))}
</span>
`).join("")}
</div>
`;
}
function renderOneLinerRunsHtml() {
const runs = safeArray(appState.onelinerRuns);
const currentRun = getCurrentOneLinerRun();
if (!runs.length || !currentRun) {
return `
<div class="task-item compact">
<h4>还没有主 Agent 运行任务</h4>
<p>你在首页、策略页或 Agent 页点击“交给主 Agent”后这里会先出现待确认执行卡。</p>
</div>
`;
}
const runEvents = safeArray(currentRun.events).slice(-3);
const planSteps = safeArray(currentRun.plan?.steps).slice(0, 4);
const resultPayload = currentRun.result && typeof currentRun.result === "object" ? currentRun.result : null;
const previewAction = currentRun.recommended_preview_action || null;
const recommendedAction = resultPayload?.recommended_action || null;
const pendingRunCount = safeArray(runs).filter((item) => item.run_status === "needs_confirmation").length;
const activeRunCount = safeArray(runs).filter((item) => ["queued", "running", "blocked"].includes(item.run_status)).length;
const completedRunCount = safeArray(runs).filter((item) => item.run_status === "done").length;
const problemRunCount = safeArray(runs).filter((item) => ["blocked", "failed", "cancelled"].includes(item.run_status)).length;
const filterKey = String(appState.onelinerRunFilter || "").trim() || (activeRunCount || pendingRunCount ? "focus" : completedRunCount ? "done" : "problems");
const runFilterPredicates = {
focus: (item) => ["needs_confirmation", "queued", "running", "blocked"].includes(item.run_status),
done: (item) => item.run_status === "done",
problems: (item) => ["blocked", "failed", "cancelled"].includes(item.run_status),
all: () => true
};
const filteredRuns = safeArray(runs).filter(runFilterPredicates[filterKey] || runFilterPredicates.focus);
const visibleRuns = (filteredRuns.length ? filteredRuns : runs).slice(0, 8);
const recentCompletedRuns = safeArray(runs)
.filter((item) => item.run_status === "done" && item.id !== currentRun.id)
.slice(0, 3);
const previewLandingAttrs = buildMainAgentLandingAttrs({
runId: currentRun.id || "",
screen: previewAction?.screen || "",
title: currentRun.title || currentRun.plan?.goal || "主 Agent 任务",
summary: previewAction?.summary || currentRun.summary || ""
});
const resultLandingAttrs = buildMainAgentLandingAttrs({
runId: currentRun.id || "",
screen: recommendedAction?.screen || "",
title: currentRun.title || currentRun.plan?.goal || "主 Agent 任务",
summary: recommendedAction?.summary || currentRun.status_summary || ""
});
const hasResultPayload = Boolean(resultPayload && Object.keys(resultPayload).length);
const runStatusLabel = {
needs_confirmation: "待确认",
queued: "排队中",
running: "执行中",
blocked: "已阻塞",
done: "已完成",
failed: "已失败",
cancelled: "已取消"
}[currentRun.run_status] || currentRun.run_status || "运行中";
const statusTone = currentRun.run_status === "needs_confirmation"
? "blue"
: currentRun.run_status === "running"
? "green"
: currentRun.run_status === "queued"
? "orange"
: currentRun.run_status === "failed"
? "orange"
: "";
const canRetryCurrentRun = ["blocked", "failed", "cancelled"].includes(currentRun.run_status);
return `
<div class="task-item compact" style="margin-bottom:10px;">
<h4>近期运行概况</h4>
<p>先看待确认和执行中的任务,再切回当前 run 继续推进。</p>
<div class="task-meta">
<span class="tag blue">待确认 ${escapeHtml(formatNumber(pendingRunCount))}</span>
<span class="tag green">执行中 ${escapeHtml(formatNumber(activeRunCount))}</span>
<span class="tag">已完成 ${escapeHtml(formatNumber(completedRunCount))}</span>
<span class="tag orange">异常 ${escapeHtml(formatNumber(problemRunCount))}</span>
</div>
<div class="task-meta" style="margin-top:10px;">
<span class="tag clickable-tag ${filterKey === "focus" ? "blue" : ""}" data-action="select-oneliner-run-filter" data-run-filter="focus">重点运行</span>
<span class="tag clickable-tag ${filterKey === "done" ? "green" : ""}" data-action="select-oneliner-run-filter" data-run-filter="done">已完成</span>
<span class="tag clickable-tag ${filterKey === "problems" ? "orange" : ""}" data-action="select-oneliner-run-filter" data-run-filter="problems">异常运行</span>
<span class="tag clickable-tag ${filterKey === "all" ? "orange" : ""}" data-action="select-oneliner-run-filter" data-run-filter="all">全部</span>
</div>
${runs.length > 1 ? `
<div class="chip-row" style="margin-top:10px;">
${visibleRuns.map((item) => `
<span class="chip clickable-tag ${item.id === currentRun.id ? "active" : ""}" data-action="select-oneliner-run" data-run-id="${escapeHtml(item.id)}">
${escapeHtml(brief(item.title || item.plan?.goal || "主 Agent 任务", 14))} · ${escapeHtml({
needs_confirmation: "待确认",
queued: "排队中",
running: "执行中",
blocked: "阻塞",
done: "已完成",
failed: "失败",
cancelled: "已取消"
}[item.run_status] || item.run_status || "运行中")}
</span>
`).join("")}
</div>
${!filteredRuns.length ? `
<div class="panel-subtitle" style="margin-top:8px;">当前筛选下还没有对应运行,已临时显示全部任务。</div>
` : ""}
` : ""}
</div>
<div class="task-item compact oneliner-run-card">
<div class="panel-head">
<div>
<h4>${escapeHtml(currentRun.title || currentRun.plan?.goal || "主 Agent 任务")}</h4>
<div class="panel-subtitle">${escapeHtml(currentRun.summary || currentRun.status_summary || "主 Agent 会先给你一张确认卡,再继续执行。")}</div>
</div>
<div class="task-meta">
<span class="tag ${statusTone}">${escapeHtml(runStatusLabel)}</span>
${currentRun.platform_label ? `<span class="tag">${escapeHtml(currentRun.platform_label)}</span>` : ""}
<span class="tag">${escapeHtml(onelinerIntentLabel(currentRun.intent_key))}</span>
${currentRun.source_screen ? `<span class="tag">${escapeHtml(currentRun.source_screen)}</span>` : ""}
</div>
</div>
${currentRun.active_admin_override_notice?.title ? `
<div class="task-item compact" style="margin-top:10px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>管理员覆盖生效中</h4>
<p>${escapeHtml(currentRun.active_admin_override_notice.summary || "当前运行会优先遵循管理员覆盖层。")}</p>
</div>
` : ""}
<div class="task-item compact" style="margin-top:10px;">
<h4>当前计划</h4>
<p>${escapeHtml(currentRun.plan?.summary || currentRun.summary || "主 Agent 会先按这张确认卡理解目标,再继续执行。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(currentRun.source_action_key || "manual-handoff")}</span>
<span class="tag">${escapeHtml(currentRun.platform_scope === "all_platforms" ? "全平台" : "单平台")}</span>
${currentRun.delivery_mode ? `<span class="tag">${escapeHtml(currentRun.delivery_mode)}</span>` : ""}
${previewAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(previewAction.action)}" ${previewLandingAttrs}>${escapeHtml(`预计落点 · ${previewAction.label || "对应页面"}`)}</span>` : ""}
</div>
</div>
${!hasResultPayload && previewAction?.summary ? `<div class="panel-subtitle" style="margin-top:8px;">${escapeHtml(previewAction.summary)}</div>` : ""}
${planSteps.length ? `
<div class="list" style="margin-top:10px;">
${planSteps.map((step, index) => `
<div class="task-item compact">
<h4>步骤 ${escapeHtml(formatNumber(index + 1))}</h4>
<p>${escapeHtml(step)}</p>
</div>
`).join("")}
</div>
` : ""}
<div class="task-meta" style="margin-top:10px;">
${currentRun.run_status === "needs_confirmation" ? `
<span class="tag clickable-tag" data-action="confirm-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">确认执行</span>
<span class="tag clickable-tag" data-action="cancel-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">取消本轮</span>
` : `
<span class="tag ${statusTone}">${escapeHtml(currentRun.status_summary || "主 Agent 正在推进中")}</span>
`}
${canRetryCurrentRun ? `<span class="tag clickable-tag" data-action="retry-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">重新执行</span>` : ""}
${hasResultPayload ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(currentRun.id)}">查看结果</span>` : ""}
${recommendedAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(recommendedAction.action)}" ${resultLandingAttrs}>${escapeHtml(recommendedAction.label || "回到对应页面")}</span>` : ""}
</div>
${recommendedAction?.summary ? `<div class="panel-subtitle" style="margin-top:8px;">${escapeHtml(recommendedAction.summary)}</div>` : ""}
${hasResultPayload ? `
<div class="task-item compact" style="margin-top:10px;">
<h4>执行结果</h4>
<div class="sheet-html">${renderOneLinerExecutionPayloadHtml(currentRun.result)}</div>
</div>
` : ""}
${runEvents.length ? `
<div class="list" style="margin-top:10px;">
${runEvents.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.event_type || "run.progress")}</h4>
<p>${escapeHtml(item.summary || "运行状态已更新。")}</p>
</div>
`).join("")}
</div>
` : ""}
${recentCompletedRuns.length ? `
<div class="task-item compact" style="margin-top:10px;">
<h4>最近完成</h4>
<div class="list" style="margin-top:10px;">
${recentCompletedRuns.map((item) => {
const doneAction = item.result?.recommended_action || null;
const doneLandingAttrs = buildMainAgentLandingAttrs({
runId: item.id || "",
screen: doneAction?.screen || "",
title: item.title || item.plan?.goal || "主 Agent 任务",
summary: doneAction?.summary || item.status_summary || ""
});
return `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.plan?.goal || "主 Agent 任务")}</h4>
<p>${escapeHtml(item.status_summary || item.summary || "已完成,可继续回看结果。")}</p>
<div class="task-meta">
<span class="tag green">已完成</span>
<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(item.id)}">查看结果</span>
${doneAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(doneAction.action)}" ${doneLandingAttrs}>${escapeHtml(doneAction.label || "回到对应页面")}</span>` : ""}
</div>
</div>
`;
}).join("")}
</div>
</div>
` : ""}
</div>
`;
}
function renderOneLinerMessagesHtml() {
const messages = safeArray(appState.onelinerMessages);
if (!messages.length) {
return `
<div class="task-item">
<h4>还没有对话</h4>
<p>你可以直接说目标不用先理解平台有什么按钮。OneLiner 会先拆目标,再决定交给哪个平台 Agent。</p>
</div>
`;
}
return messages.map((message) => {
const roleClass = message.role === "assistant" ? "assistant" : "user";
const result = message.result || {};
const plan = message.plan || {};
const executionCard = result.execution_card || {};
const activeAdminOverrideNotice = executionCard.active_admin_override_notice || null;
const actions = safeArray(plan.suggested_actions);
const secondaryActions = safeArray(executionCard.secondary_actions);
return `
<div class="oneliner-message ${roleClass}">
<div class="oneliner-bubble">
<strong>${escapeHtml(message.role === "assistant" ? "OneLiner" : "你")}</strong>
<p>${escapeHtml(message.content || result.summary_text || "")}</p>
${plan.intent_key ? `
<div class="task-meta">
<span class="tag blue">${escapeHtml(onelinerIntentLabel(plan.intent_key))}</span>
${plan.platform_label ? `<span class="tag">${escapeHtml(plan.platform_label)}</span>` : ""}
${plan.delivery_mode ? `<span class="tag ${plan.delivery_mode === "oneliner" ? "orange" : "green"}">${escapeHtml(plan.delivery_mode === "oneliner" ? "对话承接" : "可走前端")}</span>` : ""}
</div>
` : ""}
${actions.length ? `
<div class="task-meta" style="margin-top:10px;">
${actions.map((item) => `<span class="tag clickable-tag" data-action="${escapeHtml(item.key)}">${escapeHtml(item.label)}</span>`).join("")}
</div>
` : ""}
${message.role === "assistant" && (executionCard.intent_label || executionCard.platform_label || executionCard.primary_action?.label || safeArray(executionCard.evidence).length) ? `
<div class="task-item compact" style="margin-top:12px;">
<h4>${escapeHtml(executionCard.intent_label || "本轮执行建议")}</h4>
<p>${escapeHtml(executionCard.blocked_reason || `${executionCard.platform_label || "待判断平台"} · ${executionCard.delivery_mode === "ui" ? "优先走前端固定动作" : "优先由 OneLiner 对话承接"}`)}</p>
<div class="task-meta">
${executionCard.platform_label ? `<span class="tag blue">${escapeHtml(executionCard.platform_label)}</span>` : ""}
${executionCard.platform_agent_name ? `<span class="tag">${escapeHtml(executionCard.platform_agent_name)}</span>` : ""}
${executionCard.assistant_name ? `<span class="tag green">${escapeHtml(executionCard.assistant_name)}</span>` : ""}
${executionCard.readiness_label ? `<span class="tag ${executionCard.readiness_score >= 75 ? "green" : executionCard.readiness_score >= 50 ? "blue" : "orange"}">${escapeHtml(executionCard.readiness_label)} ${escapeHtml(formatNumber(executionCard.readiness_score || 0))}</span>` : ""}
${executionCard.primary_action?.key ? `<span class="tag clickable-tag" data-action="${escapeHtml(executionCard.primary_action.key)}">${escapeHtml(executionCard.primary_action.label || "执行下一步")}</span>` : ""}
</div>
${activeAdminOverrideNotice?.title ? `
<div class="task-item compact" style="margin-top:10px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>管理员覆盖生效中</h4>
<p>${escapeHtml(activeAdminOverrideNotice.summary || "当前这轮执行会优先遵循管理员覆盖,再叠加你的个人策略。")}</p>
<div class="task-meta">
<span class="tag orange">${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}</span>
${activeAdminOverrideNotice.platform_label ? `<span class="tag">${escapeHtml(activeAdminOverrideNotice.platform_label)}</span>` : ""}
</div>
</div>
` : ""}
${safeArray(executionCard.evidence).length ? `
<div class="list" style="margin-top:10px;">
${safeArray(executionCard.evidence).slice(0, 2).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.kind === "skill" ? "技能证据" : "记忆证据")} · ${escapeHtml(item.title || "未命名")}</h4>
<p>${escapeHtml(item.summary || "暂无摘要")}</p>
</div>
`).join("")}
</div>
` : ""}
${safeArray(executionCard.next_steps).length ? `
<div class="task-meta" style="margin-top:10px;">
${safeArray(executionCard.next_steps).slice(0, 3).map((item) => `<span class="tag">${escapeHtml(item)}</span>`).join("")}
</div>
` : ""}
${secondaryActions.length ? `
<div class="task-meta" style="margin-top:10px;">
${secondaryActions.map((item) => actionTag(
item.label || item.key || "执行",
item.key || "",
[
item.executor_key ? `data-executor-key="${escapeHtml(item.executor_key)}"` : "",
item.platform ? `data-platform="${escapeHtml(item.platform)}"` : "",
message.session_id ? `data-session-id="${escapeHtml(message.session_id)}"` : "",
...Object.entries(item.payload || {}).map(([payloadKey, payloadValue]) => {
const attrKey = String(payloadKey || "")
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.replace(/_/g, "-")
.toLowerCase();
const serialized = typeof payloadValue === "string"
? payloadValue
: JSON.stringify(payloadValue);
return `data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`;
})
].filter(Boolean).join(" ")
,
{ disabledReason: item.disabled_reason || "" }
)).join("")}
</div>
` : ""}
</div>
` : ""}
</div>
</div>
`;
}).join("");
}
function renderOneLinerUi() {
ensureOneLinerUi();
const fab = document.querySelector(".oneliner-fab");
const meta = document.querySelector('[data-role="oneliner-meta"]');
const runs = document.querySelector('[data-role="oneliner-runs"]');
const sessions = document.querySelector('[data-role="oneliner-sessions"]');
const messages = document.querySelector('[data-role="oneliner-messages"]');
const status = document.querySelector('[data-role="oneliner-status"]');
const input = document.querySelector('[data-role="oneliner-input"]');
const profile = appState.onelinerProfile;
const effective = appState.onelinerGovernanceEffective;
const activeAdminOverrideNotice = effective?.active_admin_override_notice || null;
const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || "");
const layers = safeArray(effective?.layers);
const currentRun = getCurrentOneLinerRun();
const activeRuns = safeArray(appState.onelinerRuns).filter((item) => !["done", "failed", "cancelled"].includes(item.run_status));
if (fab) {
fab.hidden = !appState.session;
const mark = fab.querySelector(".oneliner-fab-mark");
const text = fab.querySelector(".oneliner-fab-text");
if (mark) mark.textContent = String(activeRuns.length || 1);
if (text) {
text.textContent = currentRun
? `OneLiner · ${currentRun.run_status === "needs_confirmation" ? "待确认" : currentRun.run_status === "running" ? "执行中" : currentRun.run_status === "queued" ? "排队中" : "工作中"}`
: "OneLiner";
}
}
if (meta) {
meta.innerHTML = `
<div class="task-meta">
<span class="tag blue">${escapeHtml(profile?.display_name || "OneLiner")}</span>
<span class="tag">${escapeHtml(getSelectedProject()?.name || "未选项目")}</span>
<span class="tag">${escapeHtml(profile?.default_platform ? platformLabel(profile.default_platform) : "未设默认平台")}</span>
<span class="tag green">${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent</span>
</div>
<div class="helper-text">${escapeHtml(profile?.long_term_goal || "当前没有设置长期目标。你可以先在这里说目标,后续再逐步产品化。")}</div>
<div class="task-meta" style="margin-top:10px;">
${layers.map((layer) => `<span class="tag ${layer.scope_kind === "admin_override" ? "orange" : "blue"}">${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}</span>`).join("") || `<span class="tag">还没有策略层</span>`}
${highlights.map((item) => `<span class="tag green">${escapeHtml(item)}</span>`).join("")}
<span class="tag clickable-tag" data-action="open-user-global-policy">我的策略</span>
</div>
${activeAdminOverrideNotice?.title ? `
<div class="task-item compact" style="margin-top:10px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>管理员覆盖生效中</h4>
<p>${escapeHtml(activeAdminOverrideNotice.summary || "当前主 Agent 会优先遵循管理员覆盖层。")}</p>
<div class="task-meta">
<span class="tag orange">${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}</span>
<span class="tag clickable-tag" data-action="open-user-global-policy">查看我的策略</span>
</div>
</div>
` : ""}
`;
}
if (runs) runs.innerHTML = renderOneLinerRunsHtml();
if (sessions) sessions.innerHTML = renderOneLinerSessionTabs();
if (messages) {
messages.innerHTML = renderOneLinerMessagesHtml();
messages.scrollTop = messages.scrollHeight;
}
if (status) {
status.textContent = appState.busy ? appState.message || "处理中..." : "";
}
if (input && !input.value && !safeArray(appState.onelinerMessages).length) {
input.value = "";
}
syncOneLinerRunPolling();
}
function clearOneLinerRunPollTimer() {
if (onelinerRunPollTimer) {
clearTimeout(onelinerRunPollTimer);
onelinerRunPollTimer = null;
}
}
function syncOneLinerRunPolling() {
clearOneLinerRunPollTimer();
const panel = document.querySelector(".oneliner-backdrop");
const isPanelVisible = Boolean(panel && !panel.classList.contains("hidden"));
const currentRun = getCurrentOneLinerRun();
const shouldPoll = Boolean(
isPanelVisible
&& currentRun?.id
&& ["queued", "running", "blocked"].includes(currentRun.run_status)
);
if (!shouldPoll) {
return;
}
onelinerRunPollTimer = setTimeout(async () => {
try {
await hydrateSelectedOneLinerRun();
renderAll();
} catch {
clearOneLinerRunPollTimer();
}
}, 1500);
}
function openOneLinerPanel() {
ensureOneLinerUi();
document.querySelector(".oneliner-backdrop")?.classList.remove("hidden");
syncOneLinerRunPolling();
}
function closeOneLinerPanel() {
document.querySelector(".oneliner-backdrop")?.classList.add("hidden");
clearOneLinerRunPollTimer();
}
function readActionForm() {
const values = {};
document.querySelectorAll("[data-action-field]").forEach((element) => {
const name = element.getAttribute("data-action-field");
if (!name) return;
if (element instanceof HTMLInputElement && element.type === "checkbox") {
values[name] = element.checked;
return;
}
if (element instanceof HTMLInputElement && element.type === "file") {
values[name] = element.files?.[0] || null;
return;
}
values[name] = element.value;
});
return values;
}
async function submitActionModal() {
if (!currentActionConfig?.onSubmit) return;
const message = document.querySelector('[data-role="action-message"]');
const submit = document.querySelector('[data-action="submit-sheet"]');
const values = readActionForm();
if (submit) submit.disabled = true;
if (message) message.textContent = "正在执行...";
try {
const result = await currentActionConfig.onSubmit(values);
if (result?.keepOpen) {
if (message) message.textContent = result.message || "已完成";
} else {
closeActionModal();
}
} catch (error) {
if (message) message.textContent = formatActionErrorMessage(error);
if (submit) submit.disabled = false;
return;
}
if (submit) submit.disabled = false;
}
async function storyforgeFetch(path, options = {}) {
return API_CLIENT.fetchJson(path, options);
}
async function storyforgeFetchBlob(path, options = {}) {
return API_CLIENT.fetchBlob(path, options);
}
async function loadBackendCapabilities(backendUrl) {
return API_CLIENT.loadBackendCapabilities(backendUrl);
}
function backendSupports(path) {
return API_CLIENT.backendSupports(path);
}
async function loginWithAutoSession(backendUrl = DEFAULT_BACKEND_URL) {
const payload = await storyforgeFetch("/v2/auth/auto-session", {
backendUrl,
auth: false,
method: "POST",
body: {}
});
persistSession({
backendUrl,
token: payload.token,
account: payload.account
});
appState.autoConnectError = "";
appState.autoConnectAttempted = true;
appState.autoConnectSuppressed = false;
}
async function ensureAutoSession(options = {}) {
const backendUrl = options.backendUrl || readAuthForm().backendUrl || DEFAULT_BACKEND_URL;
const force = Boolean(options.force);
const backendMismatch = hasSessionBackendMismatch(backendUrl);
if (backendMismatch) {
persistSession(null);
}
if (appState.session && !force) {
return true;
}
if (appState.autoConnectSuppressed && !force) {
return false;
}
if (appState.autoConnectAttempted && !force) {
return Boolean(appState.session);
}
appState.autoConnectAttempted = true;
renderAll();
try {
await loginWithAutoSession(backendUrl);
return true;
} catch (error) {
appState.autoConnectError = formatActionErrorMessage(error, "自动连接失败");
persistSession(null);
renderAll();
return false;
}
}
function isAutoConnectionPending() {
return !appState.session
&& appState.autoConnectAttempted
&& !appState.autoConnectSuppressed
&& !appState.autoConnectError;
}
function renderAutoConnectingScreen(screenTitle, nextStepText) {
return screenShell(
screenTitle,
"正在自动连接工作区,成功后会自动替换成真实内容。",
`${button("连接状态", "open-auth", "primary")}`,
renderEmptyState(
"正在自动连接工作区",
nextStepText || "如果超过几秒仍未进入真实页面,可以点右上角查看连接状态。"
)
);
}
async function refreshFromAuthModal() {
const modal = document.querySelector(".auth-modal-backdrop");
const visible = modal && !modal.classList.contains("hidden");
if (!visible) {
await bootstrap();
return;
}
appState.autoConnectSuppressed = false;
appState.autoConnectAttempted = false;
await ensureAutoSession({ force: true });
if (appState.session) closeAuthModal();
await bootstrap();
}
async function logoutSession() {
try {
if (appState.session) {
await storyforgeFetch("/v2/auth/logout", { method: "POST" });
}
} catch {}
persistSession(null);
appState.autoConnectAttempted = true;
appState.autoConnectSuppressed = true;
appState.autoConnectError = "当前会话已退出。需要时可以点右上角重新自动连接。";
appState.me = null;
appState.dashboard = null;
appState.contentSources = [];
appState.accounts = [];
appState.selectedAccountId = "";
appState.currentPlatform = "";
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;
appState.reviews = [];
appState.onelinerProfile = null;
appState.onelinerSessions = [];
appState.selectedOnelinerSessionId = "";
appState.onelinerRuns = [];
appState.selectedOnelinerRunId = "";
appState.lastCompletedOnelinerRunId = "";
appState.onelinerMessages = [];
appState.onelinerActionRegistry = [];
appState.platformAgents = [];
appState.onelinerGovernanceEffective = null;
appState.userGlobalPolicy = null;
appState.userCurrentPlatformPolicy = null;
appState.userPolicyAudits = [];
appState.adminSystemMainPolicy = null;
appState.adminSystemPlatformPolicies = [];
appState.adminGovernanceDirectory = null;
appState.adminOverrideTarget = null;
appState.adminOverridePolicy = null;
appState.adminPolicyAudits = [];
appState.tenantQuota = null;
appState.tenantUsage = null;
appState.adminOpsOverview = null;
appState.adminFixRuns = [];
clearRecoveryRecords();
appState.integrationHealth = null;
appState.storageStatus = null;
appState.liveRecorderHealth = null;
appState.backendCapabilities = null;
appState.lastAction = null;
appState.lastGeneratedCopy = null;
appState.lastSimilaritySearch = null;
appState.lastJobDetail = null;
localStorage.removeItem(STORAGE_KEY + ":currentPlatform");
renderAll();
}
async function loadKnowledgeDocuments(knowledgeBases) {
const targets = safeArray(knowledgeBases).slice(0, 3);
if (!targets.length) return [];
const groups = await Promise.all(
targets.map((kb) =>
storyforgeFetch(`/v2/knowledge-bases/${encodeURIComponent(kb.id)}/documents`).catch(() => [])
)
);
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 hydrateSelectedOneLinerRun() {
const runId = appState.selectedOnelinerRunId || "";
if (!runId || !backendSupports("/v2/oneliner/runs/{run_id}")) {
return null;
}
const detail = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}`).catch(() => null);
if (!detail?.id) return null;
const runs = safeArray(appState.onelinerRuns);
const nextRuns = runs.some((item) => item.id === detail.id)
? runs.map((item) => (item.id === detail.id ? detail : item))
: [detail, ...runs];
appState.onelinerRuns = nextRuns;
if (detail.run_status === "done" && detail.id && appState.lastCompletedOnelinerRunId !== detail.id) {
rememberAction("主 Agent 已完成本轮", detail.result?.execution_summary || detail.status_summary || detail.summary || "当前运行已经完成,可以继续执行下一步。", "green", detail);
appState.lastCompletedOnelinerRunId = detail.id;
}
return detail;
}
async function loadAgentControlSurfaces(projectId = "") {
const normalizedProjectId = projectId || getOneLinerProjectId();
const governancePlatform = normalizePlatformValue(getPreferredPlatform(), "douyin");
const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile");
const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions");
const supportsOneLinerRuns = backendSupports("/v2/oneliner/runs");
const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry");
const supportsPlatformAgents = backendSupports("/v2/platform-agents");
const supportsGovernanceEffective = backendSupports("/v2/oneliner/governance/effective");
const supportsUserGlobalPolicy = backendSupports("/v2/oneliner/governance/user/global");
const supportsUserPlatformPolicy = backendSupports("/v2/oneliner/governance/user/platforms/{platform}");
const supportsUserPolicyAudits = backendSupports("/v2/oneliner/governance/user/audits");
const supportsAdminSystemMainPolicy = backendSupports("/v2/admin/oneliner/governance/system/main-agent");
const supportsAdminSystemPlatformPolicy = backendSupports("/v2/admin/oneliner/governance/system/platforms/{platform}");
const supportsAdminGovernanceDirectory = backendSupports("/v2/admin/oneliner/governance/directory");
const supportsAdminOverridePolicy = backendSupports("/v2/admin/oneliner/governance/overrides");
const supportsAdminGovernanceAudits = backendSupports("/v2/admin/oneliner/governance/audits");
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, runsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, userPolicyAuditsPayload, adminSystemMainPolicy, adminSystemPlatformPolicies, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
supportsOneLinerProfile
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsOneLinerSessions
? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsOneLinerRuns
? storyforgeFetch(`/v2/oneliner/runs?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsActionRegistry
? storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsPlatformAgents
? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsGovernanceEffective
? storyforgeFetch(`/v2/oneliner/governance/effective?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => null)
: Promise.resolve(null),
supportsUserGlobalPolicy
? storyforgeFetch(`/v2/oneliner/governance/user/global?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsUserPlatformPolicy
? storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(governancePlatform)}?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsUserPolicyAudits
? storyforgeFetch(`/v2/oneliner/governance/user/audits?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsAdminSystemMainPolicy && isSuperAdmin()
? storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent").catch(() => null)
: Promise.resolve(null),
supportsAdminSystemPlatformPolicy && isSuperAdmin()
? Promise.all(ACTIVE_PLATFORMS.map((item) =>
storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(item.value)}`).catch(() => null)
))
: Promise.resolve([]),
supportsAdminGovernanceDirectory && isSuperAdmin()
? storyforgeFetch("/v2/admin/oneliner/governance/directory").catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsTenantQuota
? storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsTenantUsage
? storyforgeFetch(`/v2/tenant/usage?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsAdminOps && isSuperAdmin()
? storyforgeFetch("/v2/admin/ops/overview").catch(() => null)
: Promise.resolve(null),
supportsAdminFixRuns && isSuperAdmin()
? storyforgeFetch("/v2/admin/ops/fix-runs").catch(() => ({ items: [] }))
: Promise.resolve({ items: [] })
]);
appState.onelinerProfile = profile;
appState.onelinerSessions = safeArray(sessionsPayload?.items || sessionsPayload);
appState.onelinerRuns = safeArray(runsPayload?.items || runsPayload);
appState.onelinerActionRegistry = safeArray(actionRegistryPayload?.items || actionRegistryPayload);
if (!appState.selectedOnelinerSessionId || !safeArray(appState.onelinerSessions).some((item) => item.id === appState.selectedOnelinerSessionId)) {
appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || "";
}
appState.selectedOnelinerRunId = choosePreferredOneLinerRunId(appState.onelinerRuns, appState.selectedOnelinerRunId || "");
if (appState.selectedOnelinerRunId) {
await hydrateSelectedOneLinerRun();
}
appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload);
appState.onelinerGovernanceEffective = governanceEffective;
appState.userGlobalPolicy = userGlobalPolicy;
appState.userCurrentPlatformPolicy = userCurrentPlatformPolicy;
appState.userPolicyAudits = safeArray(userPolicyAuditsPayload?.items || userPolicyAuditsPayload);
appState.adminSystemMainPolicy = adminSystemMainPolicy;
appState.adminSystemPlatformPolicies = safeArray(adminSystemPlatformPolicies);
appState.adminGovernanceDirectory = safeArray(adminGovernanceDirectory?.items || adminGovernanceDirectory);
if (isSuperAdmin() && supportsAdminOverridePolicy && appState.adminGovernanceDirectory.length) {
const existingTarget = appState.adminOverrideTarget || {};
const hasExistingProjectTarget = Object.prototype.hasOwnProperty.call(existingTarget, "targetProjectId")
|| Object.prototype.hasOwnProperty.call(existingTarget, "target_project_id");
const targetUserId = String(existingTarget.targetUserId || existingTarget.target_user_id || appState.adminGovernanceDirectory[0]?.id || "");
const targetUser = appState.adminGovernanceDirectory.find((item) => item.id === targetUserId) || appState.adminGovernanceDirectory[0] || null;
const targetProjects = safeArray(targetUser?.projects);
const requestedProjectId = hasExistingProjectTarget
? String(existingTarget.targetProjectId ?? existingTarget.target_project_id ?? "")
: String(targetProjects[0]?.id || "");
const targetProjectId = requestedProjectId === ""
? ""
: (targetProjects.some((item) => String(item?.id || "") === requestedProjectId) ? requestedProjectId : String(targetProjects[0]?.id || ""));
const targetPlatform = normalizePlatformValue(existingTarget.platform || governancePlatform, "douyin");
appState.adminOverrideTarget = {
targetUserId,
targetProjectId,
platform: targetPlatform
};
appState.adminOverridePolicy = await storyforgeFetch(`/v2/admin/oneliner/governance/overrides?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}`).catch(() => null);
appState.adminPolicyAudits = supportsAdminGovernanceAudits
? safeArray((await storyforgeFetch(`/v2/admin/oneliner/governance/audits?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}&include_system=1`).catch(() => ({ items: [] })))?.items || [])
: [];
} else {
appState.adminOverrideTarget = null;
appState.adminOverridePolicy = null;
appState.adminPolicyAudits = [];
}
appState.tenantQuota = tenantQuota;
appState.tenantUsage = tenantUsage;
appState.adminOpsOverview = adminOpsOverview;
appState.adminFixRuns = safeArray(adminFixRunsPayload?.items || adminFixRunsPayload);
}
async function loadOneLinerMessages(sessionId) {
if (!sessionId || !backendSupports("/v2/oneliner/sessions/{session_id}/messages")) {
appState.onelinerMessages = [];
return [];
}
const payload = await storyforgeFetch(`/v2/oneliner/sessions/${encodeURIComponent(sessionId)}/messages`).catch(() => ({ items: [] }));
appState.onelinerMessages = safeArray(payload?.items || payload);
return appState.onelinerMessages;
}
async function ensureOneLinerSession() {
const projectId = getOneLinerProjectId();
if (!projectId) throw new Error("当前还没有项目OneLiner 需要先绑定项目上下文。");
if (!backendSupports("/v2/oneliner/sessions")) {
throw new Error("当前后端还没有接入 OneLiner 会话接口。");
}
let session = getCurrentOneLinerSession();
if (!session) {
session = await storyforgeFetch("/v2/oneliner/sessions", {
method: "POST",
body: {
project_id: projectId,
preferred_platform: getPreferredPlatform()
}
});
appState.onelinerSessions = [session, ...safeArray(appState.onelinerSessions)];
appState.selectedOnelinerSessionId = session.id;
}
await loadOneLinerMessages(session.id);
return session;
}
async function submitOneLinerMessage(content) {
const projectId = getOneLinerProjectId();
const session = await ensureOneLinerSession();
const payload = await storyforgeFetch(`/v2/oneliner/sessions/${encodeURIComponent(session.id)}/messages`, {
method: "POST",
body: {
content,
project_id: projectId,
platform: getPreferredPlatform()
}
});
appState.selectedOnelinerSessionId = payload.session?.id || session.id;
await loadAgentControlSurfaces(projectId);
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else {
appState.onelinerMessages = [
...safeArray(appState.onelinerMessages),
payload.user_message,
payload.assistant_message
].filter(Boolean);
}
appState.onelinerProfile = payload.result?.context?.oneliner_profile || appState.onelinerProfile || null;
rememberAction("OneLiner 已响应", payload.result?.summary_text || "已返回一版任务拆解。", "blue", payload);
return payload;
}
async function createOneLinerRun(runRequest) {
if (!backendSupports("/v2/oneliner/runs")) {
throw new Error("当前后端还没有接入主 Agent 运行层。");
}
const projectId = getOneLinerProjectId();
const payload = await storyforgeFetch("/v2/oneliner/runs", {
method: "POST",
body: {
project_id: projectId,
platform: getPreferredPlatform(),
platform_scope: "single_platform",
delivery_mode: "hybrid",
scheduling_mode: "queued",
...runRequest
}
});
await loadAgentControlSurfaces(projectId);
appState.selectedOnelinerRunId = payload?.id || choosePreferredOneLinerRunId(appState.onelinerRuns, "");
rememberAction("主 Agent 已接单", payload?.title || payload?.plan?.goal || "已创建待确认任务。", "blue", payload);
return payload;
}
async function confirmOneLinerRun(runId, reason = "") {
const payload = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}/confirm`, {
method: "POST",
body: { reason }
});
await loadAgentControlSurfaces(getOneLinerProjectId());
appState.selectedOnelinerRunId = payload?.id || runId;
rememberAction("主 Agent 已确认执行", payload?.status_summary || "当前任务已进入执行流。", "green", payload);
return payload;
}
async function cancelOneLinerRun(runId, reason = "") {
const payload = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}/cancel`, {
method: "POST",
body: { reason }
});
await loadAgentControlSurfaces(getOneLinerProjectId());
appState.selectedOnelinerRunId = choosePreferredOneLinerRunId(appState.onelinerRuns, "");
rememberAction("主 Agent 任务已取消", payload?.status_summary || "当前任务已取消。", "orange", payload);
return payload;
}
async function retryOneLinerRun(runId, reason = "") {
const payload = await storyforgeFetch(`/v2/oneliner/runs/${encodeURIComponent(runId)}/retry`, {
method: "POST",
body: { reason }
});
await loadAgentControlSurfaces(getOneLinerProjectId());
appState.selectedOnelinerRunId = payload?.id || choosePreferredOneLinerRunId(appState.onelinerRuns, "");
appState.onelinerRunFilter = "focus";
rememberAction("主 Agent 已重新生成待确认卡", payload?.title || payload?.plan?.goal || "你可以先确认新的执行计划。", "green", payload);
return payload;
}
function renderOneLinerExecutionPayloadHtml(payload) {
if (!payload || typeof payload !== "object") {
return `<div class="task-item compact"><h4>没有返回执行结果</h4><p>当前执行器没有附带额外数据。</p></div>`;
}
if (payload.result_kind === "main_agent_plan") {
const landingRunId = String(payload.run_id || "").trim();
const landingScreen = String(payload.recommended_action?.screen || "").trim();
const landingTitle = String(payload.goal || "主 Agent 执行建议").trim();
const landingSummary = String(
payload.recommended_action?.summary || payload.execution_summary || payload.summary_text || ""
).trim();
const resultSections = payload.result_sections && typeof payload.result_sections === "object"
? payload.result_sections
: {};
const resultCards = safeArray(resultSections.cards).slice(0, 4);
const landingAttrs = buildMainAgentLandingAttrs({
runId: landingRunId,
screen: landingScreen,
title: landingTitle,
summary: landingSummary
});
return `
<div class="task-item compact">
<h4>${escapeHtml(payload.goal || "主 Agent 执行建议")}</h4>
<p>${escapeHtml(payload.execution_summary || payload.summary_text || "已形成一版可继续执行的主 Agent 建议。")}</p>
<div class="task-meta">
${payload.platform ? `<span class="tag blue">${escapeHtml(platformLabel(payload.platform))}</span>` : ""}
<span class="tag">${escapeHtml(payload.platform_scope === "all_platforms" ? "全平台" : "单平台")}</span>
<span class="tag green">已收口</span>
${payload.recommended_action?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(payload.recommended_action.action)}" data-main-agent-run-id="${escapeHtml(landingRunId)}" data-main-agent-screen="${escapeHtml(landingScreen)}" data-main-agent-title="${escapeHtml(landingTitle)}" data-main-agent-summary="${escapeHtml(landingSummary)}" ${landingAttrs}>${escapeHtml(payload.recommended_action.label || "回到对应页面")}</span>` : ""}
</div>
</div>
${resultCards.length ? `
<div class="task-item compact" style="margin-top:12px;">
<h4>${escapeHtml(resultSections.workstream_label || "执行结果分组")}</h4>
<p>${escapeHtml(payload.summary_text || "主 Agent 已将当前执行结果整理为可继续推进的结构化建议。")}</p>
<div class="detail-grid" style="margin-top:12px;">
${resultCards.map((card, index) => {
const tags = safeArray(card?.tags).slice(0, 3);
const tone = String(card?.tone || "").trim();
const toneClass = tone === "orange" ? "orange" : tone === "green" ? "green" : "blue";
return `
<div class="task-item compact">
<h4>${escapeHtml(card?.title || (index === 0 ? "当前焦点" : "执行结果"))}</h4>
<p>${escapeHtml(card?.body || "主 Agent 已生成一条可继续执行的结果说明。")}</p>
<div class="task-meta">
<span class="tag ${toneClass}">${escapeHtml(card?.title || (index === 0 ? "当前焦点" : "结果卡片"))}</span>
${tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}
</div>
</div>
`;
}).join("")}
</div>
</div>
` : ""}
${payload.recommended_action?.summary ? `
<div class="task-item compact" style="margin-top:12px;">
<h4>建议回跳</h4>
<p>${escapeHtml(payload.recommended_action.summary)}</p>
</div>
` : ""}
${safeArray(payload.next_steps).length ? `
<div class="list" style="margin-top:12px;">
${safeArray(payload.next_steps).slice(0, 4).map((step, index) => `
<div class="task-item compact">
<h4>下一步 ${escapeHtml(formatNumber(index + 1))}</h4>
<p>${escapeHtml(step)}</p>
</div>
`).join("")}
</div>
` : ""}
`;
}
if (payload.job) {
const job = payload.job || {};
const sourceJob = payload.source_job || {};
return `
<div class="task-item compact">
<h4>${escapeHtml(job.title || "任务已创建")}</h4>
<p>${escapeHtml(payload.brief || sourceJob.title || `已创建 ${job.line_type || job.source_type || "任务"},你可以继续进入生产中心查看进度。`)}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(job.line_type || job.source_type || "analysis")}</span>
<span class="tag ${job.status === "completed" ? "green" : "orange"}">${escapeHtml(job.status || "queued")}</span>
${job.id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看任务详情</span>` : ""}
<span class="tag clickable-tag" data-action="goto-production">去生产中心</span>
</div>
</div>
`;
}
if (payload.saved || payload.started) {
const saved = payload.saved || {};
const item = saved.item || {};
const started = payload.started || {};
return `
<div class="task-item compact">
<h4>${escapeHtml(item.binding_title || payload.source_url || "录制源已保存")}</h4>
<p>${escapeHtml(item.source_url || payload.source_url || "直播源已经保存到当前租户的录制配置。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(platformLabel(payload.platform || item.platform || "kuaishou"))}</span>
<span class="tag">${escapeHtml(item.quality || "原画")}</span>
<span class="tag ${started && started.ok === false ? "orange" : "green"}">${escapeHtml(started && started.ok === false ? "启动待重试" : "已同步")}</span>
<span class="tag clickable-tag" data-action="open-live-recorder">打开录制控制</span>
</div>
</div>
`;
}
if (payload.analyzed_count !== undefined && safeArray(payload.items).length) {
return `
<div class="detail-grid">
<div class="mini-card"><small>平台</small><strong>${escapeHtml(platformLabel(payload.platform || payload.account?.platform || "douyin"))}</strong></div>
<div class="mini-card"><small>账号</small><strong>${escapeHtml(payload.account?.title || payload.account?.handle || payload.account?.source_url || "当前账号")}</strong></div>
<div class="mini-card"><small>拆解作品</small><strong>${escapeHtml(formatNumber(payload.analyzed_count || 0))}</strong></div>
<div class="mini-card"><small>已写记忆</small><strong>${escapeHtml(payload.memory?.title ? "是" : "否")}</strong></div>
</div>
<div class="list" style="margin-top:12px;">
${safeArray(payload.items).slice(0, 4).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.video_title || "高分作品")}</h4>
<p>${escapeHtml(item.summary_text || "已完成拆解。")}</p>
<div class="task-meta">
<span class="tag blue">得分 ${escapeHtml(formatNumber(item.performance_score || 0))}</span>
${item.latest_job_id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(item.latest_job_id)}">看原分析</span>` : ""}
</div>
</div>
`).join("")}
</div>
`;
}
if (payload.route_checks) {
return `
<div class="detail-grid">
<div class="mini-card"><small>平台</small><strong>${escapeHtml(payload.platform_label || payload.platform || "-")}</strong></div>
<div class="mini-card"><small>得分</small><strong>${escapeHtml(formatNumber(payload.score || 0))}</strong></div>
<div class="mini-card"><small>状态</small><strong>${escapeHtml(payload.readiness_label || payload.verdict || "-")}</strong></div>
<div class="mini-card"><small>账号源</small><strong>${escapeHtml(formatNumber(payload.source_count || 0))}</strong></div>
</div>
<div class="two-col" style="margin-top:12px;">
<div>
<div class="panel-subtitle">路由检查</div>
<div class="list" style="margin-top:8px;">
${safeArray(payload.route_checks).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.key || item.path || "route")}</h4>
<p>${escapeHtml(item.path || "")}</p>
<div class="task-meta"><span class="tag ${item.ok ? "green" : "orange"}">${escapeHtml(item.ok ? "可用" : "缺失")}</span></div>
</div>
`).join("")}
</div>
</div>
<div>
<div class="panel-subtitle">建议动作</div>
<div class="list" style="margin-top:8px;">
${(safeArray(payload.suggestions).length ? safeArray(payload.suggestions) : ["当前已经达到可运行状态。"]).map((item) => `
<div class="task-item compact">
<h4>下一步</h4>
<p>${escapeHtml(item)}</p>
</div>
`).join("")}
</div>
</div>
</div>
`;
}
if (payload.strategy && payload.tenant_usage) {
return `
<div class="detail-grid">
<div class="mini-card"><small>jobs</small><strong>${escapeHtml(payload.tenant_usage?.project_jobs?.human_size || "0B")}</strong></div>
<div class="mini-card"><small>downloads</small><strong>${escapeHtml(payload.tenant_usage?.project_downloads?.human_size || "0B")}</strong></div>
<div class="mini-card"><small>模型目录</small><strong>${escapeHtml(payload.strategy?.models?.mode || "-")}</strong></div>
<div class="mini-card"><small>录像</small><strong>${escapeHtml(payload.strategy?.live_recorder?.mode || "-")}</strong></div>
</div>
`;
}
if (payload.items || payload.files) {
return `
<div class="detail-grid">
<div class="mini-card"><small>录制源</small><strong>${escapeHtml(formatNumber(safeArray(payload.items).length))}</strong></div>
<div class="mini-card"><small>最近文件</small><strong>${escapeHtml(formatNumber(safeArray(payload.files).length))}</strong></div>
</div>
`;
}
if (payload.content) {
return `
<div class="task-item compact">
<h4>生成文案</h4>
<p>${escapeHtml(brief(payload.content, 1200))}</p>
<div class="task-meta">
${payload.assistant_id ? `<span class="tag blue">${escapeHtml(payload.assistant_id)}</span>` : ""}
<span class="tag">${escapeHtml(formatNumber(safeArray(payload.used_documents).length))} 个参考素材</span>
</div>
</div>
`;
}
if (payload.verdict !== undefined || payload.next_actions !== undefined || payload.highlights !== undefined) {
return `
<div class="task-item compact">
<h4>${escapeHtml(payload.title || "复盘草稿")}</h4>
<p>${escapeHtml(payload.highlights || payload.notes || "已生成一版待补充的复盘草稿。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(platformLabel(payload.platform || "douyin"))}</span>
<span class="tag">${escapeHtml(payload.verdict || "待补充")}</span>
${payload.source_job_id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(payload.source_job_id)}">看任务详情</span>` : ""}
${payload.id ? `<span class="tag clickable-tag" data-action="open-review-edit" data-review-id="${escapeHtml(payload.id)}">打开复盘</span>` : ""}
</div>
</div>
`;
}
return `
<div class="task-item compact">
<h4>原始结果</h4>
<p>${escapeHtml(brief(JSON.stringify(payload, null, 2), 1200))}</p>
</div>
`;
}
function parseOneLinerActionPayloadValue(value) {
const text = String(value ?? "").trim();
if (!text) return "";
if (text === "true") return true;
if (text === "false") return false;
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
if ((text.startsWith("{") && text.endsWith("}")) || (text.startsWith("[") && text.endsWith("]"))) {
try {
return JSON.parse(text);
} catch (error) {
return text;
}
}
return text;
}
function collectOneLinerActionPayload(action) {
const reserved = new Set(["action", "executorKey", "platform", "sessionId", "disabledReason"]);
const payload = {};
Object.entries(action?.dataset || {}).forEach(([key, value]) => {
if (reserved.has(key)) return;
if (value === undefined || value === null || value === "") return;
payload[key] = parseOneLinerActionPayloadValue(value);
});
return payload;
}
async function executeOneLinerAction(executorKey, options = {}) {
if (!backendSupports("/v2/oneliner/actions/execute")) {
throw new Error("当前后端还没有接入 OneLiner 动作执行器。");
}
const projectId = getOneLinerProjectId();
const session = getCurrentOneLinerSession() || await ensureOneLinerSession();
const payload = await storyforgeFetch("/v2/oneliner/actions/execute", {
method: "POST",
body: {
action_key: executorKey,
project_id: projectId,
platform: options.platform || getPreferredPlatform(),
session_id: options.sessionId || session?.id || "",
payload: options.payload || {}
}
});
await loadAgentControlSurfaces(projectId);
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
}
openActionModal({
title: payload.title || "OneLiner 执行结果",
description: payload.summary || "已完成一次对话内执行。",
hideSubmit: true,
fields: [
{
type: "html",
label: "执行结果",
html: `<div class="sheet-html">${renderOneLinerExecutionPayloadHtml(payload.payload || {})}</div>`
}
]
});
rememberAction("OneLiner 已执行", payload.summary || "当前动作已在对话内执行。", "green", payload);
renderAll();
return payload;
}
function openCurrentOneLinerRunResultAction(runId = "") {
const currentRun = safeArray(appState.onelinerRuns).find((item) => item.id === runId) || getCurrentOneLinerRun();
if (!currentRun?.id) {
rememberAction("还没有可查看的结果", "当前主 Agent 任务还没有返回可展示的执行结果。", "orange");
renderAll();
return;
}
if (!currentRun.result || !Object.keys(currentRun.result || {}).length) {
rememberAction("结果还在生成中", currentRun.status_summary || "当前主 Agent 任务还没有返回执行结果。", "orange", currentRun);
renderAll();
return;
}
appState.selectedOnelinerRunId = currentRun.id;
appState.onelinerRunFilter = currentRun.run_status === "done" ? "done" : appState.onelinerRunFilter;
openActionModal({
title: currentRun.title || currentRun.plan?.goal || "主 Agent 执行结果",
description: currentRun.result?.execution_summary || currentRun.status_summary || "这是当前主 Agent 任务的执行结果。",
hideSubmit: true,
fields: [
{
type: "html",
label: "执行结果",
html: `<div class="sheet-html">${renderOneLinerExecutionPayloadHtml(currentRun.result)}</div>`
}
]
});
}
function openConfirmOneLinerRunAction(runId = "") {
const run = safeArray(appState.onelinerRuns).find((item) => item.id === runId) || getCurrentOneLinerRun();
if (!run?.id) {
rememberAction("还没有可确认的任务", "当前没有主 Agent 待确认任务。", "orange");
renderAll();
return;
}
const planSteps = safeArray(run.plan?.steps).slice(0, 5);
const previewAction = run.recommended_preview_action || null;
const tags = [
run.platform_label ? `<span class="tag blue">${escapeHtml(run.platform_label)}</span>` : "",
`<span class="tag">${escapeHtml(run.platform_scope === "all_platforms" ? "全平台" : "单平台")}</span>`,
`<span class="tag">${escapeHtml(onelinerIntentLabel(run.intent_key))}</span>`,
run.source_screen ? `<span class="tag">${escapeHtml(run.source_screen)}</span>` : ""
].filter(Boolean).join("");
const planHtml = `
<div class="task-item compact">
<h4>当前计划</h4>
<p>${escapeHtml(run.plan?.summary || run.summary || "主 Agent 会先按这张确认卡理解目标,再继续执行。")}</p>
<div class="task-meta">${tags}</div>
${planSteps.length ? `
<div class="list" style="margin-top:12px;">
${planSteps.map((step, index) => `
<div class="task-item compact">
<h4>步骤 ${escapeHtml(formatNumber(index + 1))}</h4>
<p>${escapeHtml(step)}</p>
</div>
`).join("")}
</div>
` : ""}
${previewAction ? `
<div class="task-item compact" style="margin-top:12px;">
<h4>预计落点</h4>
<p>${escapeHtml(previewAction.summary || "执行后会回到更合适的业务页面继续推进。")}</p>
<div class="task-meta">
<span class="tag green">${escapeHtml(previewAction.label || "回到对应页面")}</span>
${previewAction.screen ? `<span class="tag">${escapeHtml(previewAction.screen)}</span>` : ""}
</div>
</div>
` : ""}
</div>
`;
openActionModal({
title: "确认主 Agent 执行计划",
description: "确认后,主 Agent 会按当前计划进入执行流;你也可以先补一句执行说明。",
submitLabel: "确认执行",
fields: [
{
name: "plan",
type: "html",
label: "执行计划",
html: `<div class="sheet-html">${planHtml}</div>`
},
{
name: "reason",
type: "textarea",
label: "补充说明",
rows: 3,
placeholder: "可选:比如优先做抖音,或者先给我一版更保守的执行建议。",
value: ""
}
],
onSubmit: async (values) => {
const payload = await confirmOneLinerRun(run.id, values.reason || "user confirmed");
return {
keepOpen: false,
payload
};
}
});
}
async function loadPlatformAccount(platform, accountId, requestToken = 0) {
if (!accountId) return;
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
const token = requestToken || ((appState.selectedAccountRequestToken || 0) + 1);
if (!requestToken) {
appState.selectedAccountRequestToken = token;
}
appState.selectedAccountId = accountId;
setCurrentPlatform(normalizedPlatform);
if (!isWorkbenchPlatform(normalizedPlatform)) {
if (token !== appState.selectedAccountRequestToken) {
return false;
}
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.similarSearchDetail = null;
return true;
}
const workspacePath = getWorkbenchRoute(normalizedPlatform, "workspace", accountId);
if (!workspacePath) {
if (token !== appState.selectedAccountRequestToken) {
return false;
}
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.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, snapshotsPayload, analysisReportsPayload] = await Promise.all([
storyforgeFetch(workspacePath),
supportsAccountVideos
? storyforgeFetch(videosPath).catch(() => ({
items: [],
meta: {},
top_scored_video_ids: [],
latest_video_ids: [],
high_score_threshold: 60
}))
: Promise.resolve({
items: [],
meta: {},
top_scored_video_ids: [],
latest_video_ids: [],
high_score_threshold: 60
}),
supportsAccountSnapshots
? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/snapshots`).catch(() => [])
: Promise.resolve([]),
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 = supportsCreatorFields && hasCreatorCenterSnapshot(appState.snapshots)
? await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/creator-fields`).catch(() => null)
: null;
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) {
return false;
}
throw error;
}
}
async function bootstrap() {
renderAll();
const backendMismatch = hasSessionBackendMismatch();
if (!appState.session || backendMismatch) {
setBusy(true, backendMismatch ? "正在切换到当前工作区后端..." : "正在自动连接后端...");
try {
await ensureAutoSession({ force: backendMismatch });
} finally {
setBusy(false, "");
}
}
if (!appState.session) {
renderAuthUi();
return;
}
setBusy(true, "正在同步工作区...");
try {
appState.me = await storyforgeFetch("/v2/me");
if (appState.me.approval_status !== "approved" && appState.me.role !== "super_admin") {
appState.dashboard = null;
appState.accounts = [];
appState.contentSources = [];
appState.trackingAccounts = [];
appState.trackingDigest = null;
appState.trackingCursorMap = {};
appState.documents = [];
renderAll();
return;
}
appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null);
const dashboard = await storyforgeFetch("/v2/me/dashboard");
appState.dashboard = dashboard;
const runtimePlatforms = getRuntimePlatformValues();
const preferredPlatform = getCurrentPlatformValue();
setCurrentPlatform(preferredPlatform);
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");
const supportsLiveRecorderHealth = backendSupports("/v2/live-recorder/health");
const [contentSources, platformPayloads, reviews, integrationHealth, localModelCatalog, liveRecorderSourcesPayload] = await Promise.all([
storyforgeFetch("/v2/content-sources").catch(() => []),
Promise.all(runtimePlatforms.map(async (platform) => {
const accountListPath = getWorkbenchRoute(platform, "accounts");
const trackingAccountsPath = getWorkbenchRoute(platform, "trackingAccounts");
const trackingDigestPath = getWorkbenchRoute(platform, "trackingDigest");
const supportsAccounts = accountListPath && backendSupports(accountListPath);
const supportsTrackingAccounts = trackingAccountsPath && backendSupports(trackingAccountsPath);
const supportsTrackingDigest = trackingDigestPath && backendSupports(trackingDigestPath);
const accounts = supportsAccounts
? await storyforgeFetch(accountListPath).catch(() => [])
: [];
const trackingAccountsPayload = supportsTrackingAccounts
? await storyforgeFetch(trackingAccountsPath).catch(() => ({ items: [], cursor_last_seen_at: "" }))
: { items: [], cursor_last_seen_at: "" };
const trackingCursorLastSeenAt = trackingAccountsPayload?.cursor_last_seen_at || "";
const trackingDigest = supportsTrackingDigest
? await storyforgeFetch(`${trackingDigestPath}?since=${encodeURIComponent(trackingCursorLastSeenAt || getTrackingSinceIso(platform))}&limit=24`).catch(() => ({
items: [],
tracked_accounts: [],
cursor_last_seen_at: trackingCursorLastSeenAt
}))
: {
items: [],
tracked_accounts: [],
cursor_last_seen_at: trackingCursorLastSeenAt
};
return {
platform,
accounts: safeArray(accounts).map((item) => ({
...item,
platform: normalizePlatformValue(item?.platform || platform, platform)
})),
trackingAccountsPayload,
trackingDigest
};
})),
supportsReviews ? storyforgeFetch("/v2/reviews").catch(() => []) : Promise.resolve([]),
supportsIntegrationHealth ? storyforgeFetch("/v2/integrations/health").catch(() => null) : Promise.resolve(null),
supportsLocalModels ? storyforgeFetch("/v2/integrations/local-models").catch(() => null) : Promise.resolve(null),
supportsLiveRecorderSources ? storyforgeFetch("/v2/live-recorder/sources").catch(() => ({ items: [] })) : Promise.resolve({ items: [] })
]);
const liveRecorderIntegration = integrationHealth?.live_recorder || null;
const canLoadLiveRecorderRuntime = Boolean(liveRecorderIntegration?.reachable);
const [liveRecorderStatus, liveRecorderFilesPayload, liveRecorderHealth] = await Promise.all([
supportsLiveRecorderStatus && canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/status").catch(() => null) : Promise.resolve(null),
supportsLiveRecorderFiles && canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }),
supportsLiveRecorderHealth && canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/health").catch(() => null) : Promise.resolve(null)
]);
const mergedAccounts = safeArray(platformPayloads)
.flatMap((entry) => safeArray(entry.accounts))
.sort((a, b) => {
const platformCompare = platformLabel(getAccountPlatform(a)).localeCompare(platformLabel(getAccountPlatform(b)), "zh-Hans-CN");
if (platformCompare !== 0) return platformCompare;
return getAccountName(a).localeCompare(getAccountName(b), "zh-Hans-CN");
});
const accountIndex = new Map(mergedAccounts.map((item) => [item.id, item]));
const assistantNameMap = buildAssistantNameMap(dashboard.assistants);
const trackingCursorMap = Object.fromEntries(
safeArray(platformPayloads)
.map((entry) => [entry.platform, entry.trackingAccountsPayload?.cursor_last_seen_at || ""])
.filter(([, value]) => value)
);
const currentCursor = trackingCursorMap[preferredPlatform] || pickLatestIso(Object.values(trackingCursorMap)) || "";
if (currentCursor) {
setLastSeenAt(currentCursor);
}
appState.contentSources = safeArray(contentSources);
appState.accounts = mergedAccounts;
appState.trackingCursorMap = trackingCursorMap;
appState.trackingAccounts = safeArray(platformPayloads).flatMap((entry) =>
enrichTrackingAccounts(
entry.trackingAccountsPayload?.items || entry.trackingAccountsPayload,
accountIndex,
assistantNameMap,
entry.platform
)
);
appState.trackingDigest = {
cursor_last_seen_at: currentCursor,
items: safeArray(platformPayloads).flatMap((entry) => enrichTrackingDigestItems(entry.trackingDigest?.items, accountIndex, entry.platform)),
tracked_accounts: safeArray(platformPayloads).flatMap((entry) =>
safeArray(entry.trackingDigest?.tracked_accounts).map((item) => ({
...item,
platform: normalizePlatformValue(item.platform || entry.platform, entry.platform)
}))
)
};
appState.reviews = safeArray(reviews);
appState.liveRecorderSources = safeArray(liveRecorderSourcesPayload?.items || liveRecorderSourcesPayload);
appState.liveRecorderStatus = liveRecorderStatus;
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) {
await loadStorageStatus(appState.selectedProjectId || "");
} else {
appState.storageStatus = null;
}
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else {
appState.onelinerMessages = [];
}
const selectedAssistantExists = safeArray(dashboard.assistants).some((item) => item.id === appState.selectedAssistantId);
appState.selectedAssistantId = selectedAssistantExists ? appState.selectedAssistantId : (dashboard.assistants?.[0]?.id || "");
const platformAccounts = getAccountsForPlatform(preferredPlatform);
const selectedAccountExists = platformAccounts.some((item) => item.id === appState.selectedAccountId);
const nextAccountId = selectedAccountExists ? appState.selectedAccountId : platformAccounts[0]?.id || appState.accounts[0]?.id || "";
if (nextAccountId) {
const nextAccount = appState.accounts.find((item) => item.id === nextAccountId) || null;
await loadPlatformAccount(getAccountPlatform(nextAccount), nextAccountId);
} else {
appState.selectedAccountId = "";
appState.selectedWorkspace = null;
appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 };
}
} catch (error) {
appState.message = error.message;
if (
String(error.message || "").includes("401")
|| String(error.message || "").includes("Not authenticated")
|| String(error.message || "").includes("Invalid token")
) {
persistSession(null);
appState.autoConnectAttempted = false;
}
} finally {
appState.recoveryRecords = getRecoveryRecords();
setBusy(false, "");
renderAll();
}
}
async function markTrackingDigestRead() {
const platform = getCurrentPlatformValue();
const trackingCursorPath = getWorkbenchRoute(platform, "trackingCursor");
if (!trackingCursorPath || !backendSupports(trackingCursorPath)) {
rememberAction("当前后端暂不支持", "这套 live collector 还没有接入跟踪已读游标。", "orange");
renderAll();
return;
}
const nextSeenAt = new Date().toISOString();
await storyforgeFetch(trackingCursorPath, {
method: "POST",
body: { last_seen_at: nextSeenAt }
});
appState.trackingCursorMap = {
...(appState.trackingCursorMap || {}),
[platform]: nextSeenAt
};
if (appState.trackingDigest) {
appState.trackingDigest = {
...appState.trackingDigest,
items: safeArray(appState.trackingDigest.items).filter((item) => item.platform !== platform),
cursor_last_seen_at: platform === getCurrentPlatformValue()
? nextSeenAt
: (appState.trackingDigest.cursor_last_seen_at || "")
};
}
setLastSeenAt(nextSeenAt);
}
async function refreshTrackingAccountsAction() {
const platform = getCurrentPlatformValue();
const trackingRefreshPath = getWorkbenchRoute(platform, "trackingRefresh");
if (!trackingRefreshPath || !backendSupports(trackingRefreshPath)) {
rememberAction("当前后端暂不支持", "这套 live collector 还没有接入批量跟踪同步。", "orange");
renderAll();
return;
}
setBusy(true, "正在同步跟踪账号...");
try {
const payload = await storyforgeFetch(trackingRefreshPath, {
method: "POST"
});
rememberAction(
"跟踪已同步",
`已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)}` : ""}`,
payload.failed ? "orange" : "green",
payload
);
await bootstrap();
} finally {
setBusy(false, "");
}
}
async function refreshTrackedAccountAction(trackedAccountId) {
if (!trackedAccountId) {
throw new Error("trackedAccountId is required");
}
const trackedItem = getTrackingAccounts().find((item) => item.tracked_account_id === trackedAccountId);
const platform = trackedItem?.platform || getCurrentPlatformValue();
const trackingRefreshPath = getWorkbenchRoute(platform, "trackingAccountRefresh", trackedAccountId);
if (!trackingRefreshPath || !backendSupports(`/v2/${platform}/tracking/accounts/{tracked_account_id}/refresh`)) {
rememberAction("当前后端暂不支持", "这套 live collector 还没有接入单账号跟踪同步。", "orange");
renderAll();
return;
}
setBusy(true, "正在同步该跟踪账号...");
try {
const payload = await storyforgeFetch(trackingRefreshPath, {
method: "POST"
});
const success = payload.success !== false;
rememberAction(
success ? "单账号已同步" : "单账号刷新失败",
success
? `已刷新「${payload.account?.nickname || trackedAccountId}」的最新作品。`
: `暂时无法刷新「${payload.account?.nickname || trackedAccountId}」:${payload.message || "请稍后重试"}`,
success ? (safeArray(payload.sync_errors).length ? "orange" : "green") : "orange",
payload
);
await bootstrap();
} finally {
setBusy(false, "");
}
}
function getSelectedProject() {
const projects = safeArray(appState.dashboard?.projects);
return projects.find((item) => item.id === appState.selectedProjectId) || projects[0] || null;
}
function isSuperAdmin() {
return appState.me?.role === "super_admin";
}
function getOneLinerProjectId() {
return getSelectedProject()?.id || appState.selectedProjectId || safeArray(appState.dashboard?.projects)[0]?.id || "";
}
function getCurrentOneLinerSession() {
const sessions = safeArray(appState.onelinerSessions);
return sessions.find((item) => item.id === appState.selectedOnelinerSessionId) || sessions[0] || null;
}
function choosePreferredOneLinerRunId(items, currentId = "") {
const runs = safeArray(items);
if (currentId && runs.some((item) => item.id === currentId)) {
return currentId;
}
return runs.find((item) => item.run_status === "needs_confirmation")?.id || runs[0]?.id || "";
}
function getCurrentOneLinerRun() {
const runs = safeArray(appState.onelinerRuns);
const preferredId = choosePreferredOneLinerRunId(runs, appState.selectedOnelinerRunId || "");
return runs.find((item) => item.id === preferredId) || null;
}
function onelinerIntentLabel(value) {
return ONELINER_INTENT_LABELS[value] || value || "自定义任务";
}
function getProjectKnowledgeBases(projectId) {
return safeArray(appState.dashboard?.knowledge_bases).filter((item) => item.project_id === projectId);
}
function getProjectAssistants(projectId) {
return safeArray(appState.dashboard?.assistants).filter((item) => item.project_id === projectId);
}
function getSelectedAssistant() {
const assistants = safeArray(appState.dashboard?.assistants);
return assistants.find((item) => item.id === appState.selectedAssistantId) || assistants[0] || null;
}
function getProjectOptions() {
return safeArray(appState.dashboard?.projects).map((project) => ({ value: project.id, label: project.name }));
}
function getAssistantOptions(projectId) {
return getProjectAssistants(projectId).map((assistant) => ({ value: assistant.id, label: assistant.name }));
}
function getKnowledgeBaseOptions(projectId) {
return getProjectKnowledgeBases(projectId).map((kb) => ({ value: kb.id, label: kb.name }));
}
function getModelOptions() {
return safeArray(appState.dashboard?.model_profiles).map((model) => ({ value: model.id, label: model.name }));
}
function getCurrentModelProfile() {
const models = safeArray(appState.dashboard?.model_profiles);
const currentId = appState.me?.preferred_analysis_model_id
|| models.find((item) => item.is_default)?.id
|| "";
return models.find((item) => item.id === currentId) || models.find((item) => item.is_default) || models[0] || null;
}
function getCompletedJobOptions() {
return safeArray(appState.dashboard?.recent_jobs)
.filter((item) => item.status === "completed")
.map((job) => ({
value: job.id,
label: `${job.title} · ${job.line_type || "analysis"}`
}));
}
function getProjectStats(projectId) {
const dashboard = appState.dashboard || {};
const knowledgeBases = safeArray(dashboard.knowledge_bases).filter((item) => item.project_id === projectId);
const assistants = safeArray(dashboard.assistants).filter((item) => item.project_id === projectId);
const jobs = safeArray(dashboard.recent_jobs).filter((item) => item.project_id === projectId);
const sources = safeArray(appState.contentSources).filter((item) => item.project_id === projectId);
return { knowledgeBases, assistants, jobs, sources };
}
function getProjectReviews(projectId) {
return safeArray(appState.reviews).filter((item) => item.project_id === projectId);
}
function getReviewById(reviewId) {
return safeArray(appState.reviews).find((item) => item.id === reviewId) || null;
}
function getContentSourcesForAccount(account) {
if (!account) return [];
const platform = getAccountPlatform(account);
const profileUrl = getAccountProfileUrl(account);
const handle = getAccountHandle(account);
const nickname = getAccountName(account);
return safeArray(appState.contentSources).filter((source) => {
const sourceUrl = String(source.source_url || "").trim();
const sourceHandle = String(source.handle || "").trim();
const title = String(source.title || "").trim();
const sourcePlatform = normalizePlatformValue(source.platform || "", platform);
return (
sourcePlatform === platform && (
(profileUrl && sourceUrl === profileUrl) ||
(handle && sourceHandle === handle) ||
(nickname && title.includes(nickname))
)
);
});
}
function getCurrentProjectSourcesForAccount(account, projectId) {
return getContentSourcesForAccount(account).filter((source) => source.project_id === projectId);
}
function getCurrentPlatformValue() {
const available = getRuntimePlatformValues();
const fallback = available[0] || "douyin";
const current = normalizePlatformValue(appState.currentPlatform, "");
if (current && available.includes(current)) return current;
return normalizePlatformValue(getPreferredPlatform(), fallback);
}
function getAccountsForPlatform(platform) {
const normalizedPlatform = normalizePlatformValue(platform, getCurrentPlatformValue());
return safeArray(appState.accounts).filter((item) => getAccountPlatform(item) === normalizedPlatform);
}
function parseIsoTime(value) {
const text = String(value || "").trim();
if (!text) return 0;
const timestamp = new Date(text).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function sortItemsByIsoDesc(items, fieldName) {
return items
.slice()
.sort((left, right) => parseIsoTime(right?.[fieldName]) - parseIsoTime(left?.[fieldName]));
}
function pickLatestIso(values) {
return values
.map((value) => String(value || "").trim())
.filter(Boolean)
.sort((left, right) => parseIsoTime(right) - parseIsoTime(left))[0] || "";
}
function createEmptyTrackingDigest(cursorLastSeenAt = "") {
return {
items: [],
tracked_accounts: [],
cursor_last_seen_at: cursorLastSeenAt
};
}
function getAssistantById(assistantId) {
if (!assistantId) return null;
return safeArray(appState.dashboard?.assistants).find((item) => item.id === assistantId) || null;
}
function getTrackedAccountDisplay(item) {
const account = item?.account
|| safeArray(appState.accounts).find((row) => row.id === item?.tracked_account_id)
|| null;
const assistant = getAssistantById(item?.assistant_id || "");
const platform = normalizePlatformValue(item?.platform || account?.platform || getCurrentPlatformValue(), getCurrentPlatformValue());
return {
...item,
account,
platform,
assistant_name: item?.assistant_name || assistant?.name || "",
note: item?.note || ""
};
}
function isTrackedAccount(accountId) {
return safeArray(appState.trackingAccounts).some((item) => item.tracked_account_id === accountId);
}
function getTrackingAccounts() {
return sortItemsByIsoDesc(
safeArray(appState.trackingAccounts).map((item) => getTrackedAccountDisplay(item)),
"updated_at"
);
}
function getTrackingAccountsForProject(projectId) {
if (!projectId) return getTrackingAccounts();
const assistantIds = new Set(getProjectAssistants(projectId).map((item) => item.id));
const scoped = getTrackingAccounts().filter((item) => item.assistant_id && assistantIds.has(item.assistant_id));
return scoped.length ? scoped : getTrackingAccounts();
}
function dashboardTabLabel(value) {
return ({
project_progress: "项目进度",
focus_accounts: "重点账号 / 对标",
production_jobs: "生产任务"
})[value] || "项目进度";
}
function getDashboardActionSourceLabel() {
return appState.onelinerProfile ? "主 Agent 优先推荐" : "规则推荐";
}
function hasCreatorCenterSnapshot(items) {
return safeArray(items).some((item) => String(item?.snapshot_type || "").toLowerCase() === "creator_center");
}
function getDashboardProjectProgressSummary(project, stats, trackedAccounts) {
const total = 5;
const completed = [
Boolean(project),
stats.assistants.length > 0,
stats.sources.length > 0 || appState.accounts.length > 0,
trackedAccounts.length > 0,
stats.jobs.length > 0
].filter(Boolean).length;
const failedJobs = stats.jobs.filter((item) => item.status === "failed").length;
let nextStep = "先创建当前项目";
if (project && !stats.assistants.length) {
nextStep = "补第一个 Agent";
} else if (project && !stats.sources.length && !appState.accounts.length) {
nextStep = "导入主页或作品";
} else if (project && !trackedAccounts.length) {
nextStep = "加一个重点跟踪";
} else if (project && !stats.jobs.length) {
nextStep = "发起第一条生产任务";
} else if (project) {
nextStep = "继续推进当前主流程";
}
const risk = failedJobs
? `${failedJobs} 条任务失败待处理`
: (!trackedAccounts.length && project)
? "还没有重点账号跟踪"
: "当前主流程没有明显阻塞";
return { total, completed, nextStep, risk };
}
function buildDashboardOverviewTabs(project, stats, trackedAccounts) {
const progress = getDashboardProjectProgressSummary(project, stats, trackedAccounts);
const pendingJobs = stats.jobs.filter((item) => item.status !== "completed").length;
return [
{
key: "project_progress",
label: "项目进度",
value: `${progress.completed} / ${progress.total}`,
hint: progress.nextStep,
active: appState.dashboardOverviewTab === "project_progress"
},
{
key: "focus_accounts",
label: "重点账号 / 对标",
value: formatNumber(trackedAccounts.length || appState.accounts.length),
hint: trackedAccounts.length ? "优先看变化最多的对象" : "等待接入重点对象",
active: appState.dashboardOverviewTab === "focus_accounts"
},
{
key: "production_jobs",
label: "生产任务",
value: formatNumber(stats.jobs.length),
hint: pendingJobs ? `${formatNumber(pendingJobs)} 条待推进` : "可发起下一条任务",
active: appState.dashboardOverviewTab === "production_jobs"
}
];
}
function renderDashboardProjectProgressBody(project, stats, trackedAccounts) {
const progress = getDashboardProjectProgressSummary(project, stats, trackedAccounts);
return `
<div class="dashboard-overview-detail">
<div class="mini-grid">
<div class="mini-card">
<small>当前项目</small>
<strong>${escapeHtml(project?.name || "还没有项目")}</strong>
<span>${escapeHtml(project?.description || "先选定项目,首页动作才会真正收敛。")}</span>
</div>
<div class="mini-card">
<small>主流程进度</small>
<strong>${escapeHtml(`${progress.completed} / ${progress.total}`)}</strong>
<span>${escapeHtml(progress.nextStep)}</span>
</div>
<div class="mini-card">
<small>阶段风险</small>
<strong>${escapeHtml(progress.risk)}</strong>
<span>${escapeHtml(stats.jobs.length ? `当前项目任务 ${formatNumber(stats.jobs.length)}` : "还没有生产任务")}</span>
</div>
</div>
<div class="task-item compact dashboard-overview-note">
<h4>下一步建议</h4>
<p>${escapeHtml(project ? `当前项目建议先「${progress.nextStep}」,做完后再回到首页看下一条动作。` : "先创建项目或切换到已有项目,然后首页动作区会自动切到该项目。")}</p>
<div class="task-meta">
${actionTag(project ? "切换项目" : "去建项目", project ? "open-dashboard-project-switcher" : "goto-intake")}
${actionTag("去生产中心", "goto-production")}
</div>
</div>
</div>
`;
}
function renderDashboardFocusAccountsBody(project, trackedAccounts) {
const fallbackAccounts = safeArray(appState.accounts).slice(0, 3).map((item) => ({
platform: getAccountPlatform(item),
assistant_name: "",
account: item
}));
const items = (trackedAccounts.length ? trackedAccounts : fallbackAccounts).slice(0, 3);
if (!items.length) {
return `
<div class="task-item compact dashboard-overview-empty">
<h4>还没有重点账号 / 对标</h4>
<p>${escapeHtml(project ? "先导入主页或把一个重点账号加入跟踪,首页才会持续给出更聪明的动作推荐。" : "先创建项目,再导入第一个重点账号。")}</p>
<div class="task-meta">
${actionTag("去找对标", "goto-discovery")}
${actionTag("去跟踪账号", "goto-tracking")}
</div>
</div>
`;
}
return `
<div class="three-col dashboard-overview-grid">
${items.map((item) => {
const account = item.account || item;
const accountId = account?.id || item.tracked_account_id || "";
return `
<div class="entity-card pad dashboard-overview-card">
<div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(getAccountName(account)))}</div>
<div>
<div class="cell-title">${escapeHtml(getAccountName(account))}</div>
<div class="cell-desc">${escapeHtml(platformLabel(item.platform || getAccountPlatform(account)))} · ${escapeHtml(item.assistant_name || account.signature || "重点关注对象")}</div>
</div>
</div>
<div class="entity-meta">
<span class="tag blue">${escapeHtml(account.video_summary?.count ? `作品 ${formatNumber(account.video_summary.count)}` : "重点对象")}</span>
${account.sync_status ? `<span class="tag">${escapeHtml(account.sync_status)}</span>` : ""}
${accountId ? actionTag("查看详情", "select-account", `data-account-id="${escapeHtml(accountId)}"`) : ""}
</div>
</div>
`;
}).join("")}
</div>
`;
}
function renderDashboardProductionJobsBody(stats) {
const jobs = sortItemsByIsoDesc(stats.jobs, "updated_at").slice(0, 4);
if (!jobs.length) {
return `
<div class="task-item compact dashboard-overview-empty">
<h4>当前项目还没有生产任务</h4>
<p>先补完项目和对标,再从首页动作区或生产中心发起第一条任务。</p>
<div class="task-meta">
${actionTag("去生产中心", "goto-production")}
${actionTag("去找对标", "goto-discovery")}
</div>
</div>
`;
}
return `
<div class="list dashboard-overview-list">
${jobs.map((job) => `
<div class="task-item compact">
<h4>${escapeHtml(job.title || job.id)}</h4>
<p>${escapeHtml(brief(job.error || job.summary || `最近更新于 ${formatDateTime(job.updated_at || job.created_at)}`, 120))}</p>
<div class="task-meta">
<span class="tag ${statusTone(job.status)}">${escapeHtml(job.status || "-")}</span>
${job.line_type ? `<span class="tag">${escapeHtml(job.line_type)}</span>` : ""}
${actionTag("查看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
</div>
</div>
`).join("")}
</div>
`;
}
function renderDashboardOverviewBody(tab, context) {
if (tab === "focus_accounts") {
return renderDashboardFocusAccountsBody(context.project, context.trackedAccounts);
}
if (tab === "production_jobs") {
return renderDashboardProductionJobsBody(context.stats);
}
return renderDashboardProjectProgressBody(context.project, context.stats, context.trackedAccounts);
}
function buildDashboardHomeModel() {
const project = getSelectedProject();
const emptyStats = { knowledgeBases: [], assistants: [], jobs: [], sources: [] };
const stats = project ? getProjectStats(project.id) : emptyStats;
const trackedAccounts = project ? getTrackingAccountsForProject(project.id) : getTrackingAccounts();
const dashboardModule = window.StoryForgeDashboardHome;
const summaryTabs = buildDashboardOverviewTabs(project, stats, trackedAccounts);
const activeTabLabel = dashboardTabLabel(appState.dashboardOverviewTab);
const baseModel = dashboardModule?.createDashboardHomeModel
? dashboardModule.createDashboardHomeModel({
workspaceLabel: appState.me?.display_name || appState.me?.username || "当前工作区",
currentProjectName: project?.name || "还没有项目",
trackedAccountsCount: trackedAccounts.length || appState.accounts.length,
assistantCount: stats.assistants.length,
jobCount: stats.jobs.length,
hasProject: Boolean(project),
actionSourceLabel: getDashboardActionSourceLabel(),
dashboardOverviewTab: appState.dashboardOverviewTab,
summaryTabs,
activeTabLabel,
contextLinks: [
{ label: "账号", value: formatNumber(trackedAccounts.length || appState.accounts.length), action: "goto-owned" },
{ label: "任务", value: formatNumber(stats.jobs.length), action: "goto-production" },
{ label: "Agent", value: formatNumber(stats.assistants.length), action: "goto-playbook" }
]
})
: {
workspaceLabel: appState.me?.display_name || appState.me?.username || "当前工作区",
currentProjectName: project?.name || "还没有项目",
actionSourceLabel: getDashboardActionSourceLabel(),
contextLinks: [
{ label: "账号", value: formatNumber(trackedAccounts.length || appState.accounts.length), action: "goto-owned" },
{ label: "任务", value: formatNumber(stats.jobs.length), action: "goto-production" },
{ label: "Agent", value: formatNumber(stats.assistants.length), action: "goto-playbook" }
],
primaryAction: {
title: project ? "继续推进当前项目主流程" : "先创建或切换到一个项目",
reason: project ? "从首页动作区进入当前最该做的事。" : "首页动作和概览都跟随当前项目。",
badges: ["默认动作"],
goAction: project ? "goto-production" : "goto-intake",
goLabel: project ? "去处理" : "去项目",
agentLabel: "交给主 Agent"
},
secondaryActions: [],
summaryTabs,
activeTabLabel
};
return {
...baseModel,
summaryTabs,
activeTabLabel,
overviewBodyHtml: renderDashboardOverviewBody(appState.dashboardOverviewTab, { project, stats, trackedAccounts })
};
}
function openDashboardProjectSwitcher() {
const options = getProjectOptions();
if (!options.length) {
rememberAction("还没有项目", "先创建一个项目,再让首页跟着当前项目切换。", "orange");
setScreen("intake");
return;
}
openActionModal({
title: "切换当前项目",
description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。",
submitLabel: "切换项目",
fields: [
{ name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options }
],
onSubmit: async (payload) => {
appState.selectedProjectId = payload.projectId || "";
setBusy(true, "正在切换项目视图...");
try {
if (backendSupports("/v2/storage/status")) {
await loadStorageStatus(appState.selectedProjectId || "");
} else {
appState.storageStatus = null;
}
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
}
} finally {
setBusy(false, "");
}
renderAll();
}
});
}
function openDashboardActionReasonAction(index) {
const model = buildDashboardHomeModel();
const actions = [model.primaryAction, ...safeArray(model.secondaryActions)];
const action = actions[Number(index)] || actions[0];
if (!action) return;
appState.dashboardActionReason = {
title: action.title,
reason: action.reason,
sourceLabel: model.actionSourceLabel,
badges: safeArray(action.badges),
goLabel: action.goLabel || "去处理"
};
openActionModal({
title: "动作原因",
description: "首页只放最短判断,这里再把原因展开一层。",
hideSubmit: true,
fields: [
{
type: "html",
label: "动作详情",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(appState.dashboardActionReason.title)}</h4>
<p>${escapeHtml(appState.dashboardActionReason.reason)}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(appState.dashboardActionReason.sourceLabel)}</span>
${appState.dashboardActionReason.badges.map((item) => `<span class="tag">${escapeHtml(item)}</span>`).join("")}
</div>
</div>
</div>
`
}
]
});
}
function getTrackingDigestItems(limit = 6, options = {}) {
const targetPlatform = normalizePlatformValue(options.platform || "", "");
const fallbackPlatform = targetPlatform || getCurrentPlatformValue();
return safeArray(appState.trackingDigest?.items)
.map((item) => {
const tracked = getTrackedAccountDisplay(item);
const summary = item?.summary || item?.summary_text || item?.video?.description || item?.video?.title || item?.description || "";
const video = item?.video || {};
const isHighValue = item?.is_high_value != null
? Boolean(item.is_high_value)
: Number(video?.score?.performance_score || 0) >= 60;
return {
...item,
account: item?.account || tracked.account,
platform: tracked.platform || fallbackPlatform,
assistant_name: item?.assistant_name || tracked.assistant_name || "",
summary,
borrowing_points: safeArray(item?.borrowing_points),
is_high_value: isHighValue
};
})
.filter((item) => !targetPlatform || item.platform === targetPlatform)
.sort((left, right) => parseIsoTime(right?.created_at || right?.video?.published_at) - parseIsoTime(left?.created_at || left?.video?.published_at))
.slice(0, limit);
}
function getSelectedAccount() {
return appState.selectedWorkspace?.account
|| appState.accounts.find((item) => item.id === appState.selectedAccountId)
|| null;
}
function getHighScoreVideos(limit = 3) {
const items = safeArray(appState.selectedVideos?.items);
const fallback = safeArray(getSelectedAccount()?.video_summary?.videos);
const pool = items.length ? items : fallback;
return pool
.slice()
.sort((a, b) => Number(b.score?.performance_score || 0) - Number(a.score?.performance_score || 0))
.slice(0, limit);
}
function getLatestVideos(limit = 3) {
const items = safeArray(appState.selectedVideos?.items);
const fallback = safeArray(getSelectedAccount()?.video_summary?.videos);
const pool = items.length ? items : fallback;
return pool
.slice()
.sort((a, b) => new Date(b.published_at || 0).getTime() - new Date(a.published_at || 0).getTime())
.slice(0, limit);
}
function getProductionWorks(limit = 6) {
const preferred = safeArray(appState.selectedVideos?.items);
const fallback = safeArray(appState.accounts)
.flatMap((account) => safeArray(account.video_summary?.videos))
.filter(Boolean);
const pool = preferred.length ? preferred : fallback;
const scored = pool
.slice()
.sort((a, b) => Number(b.score?.performance_score || 0) - Number(a.score?.performance_score || 0))
.slice(0, Math.ceil(limit / 2));
const latest = pool
.slice()
.sort((a, b) => new Date(b.published_at || 0).getTime() - new Date(a.published_at || 0).getTime())
.slice(0, Math.ceil(limit / 2) + 1);
const deduped = [];
const seen = new Set();
[...scored, ...latest].forEach((item) => {
const key = item.aweme_id || item.share_url || item.title || item.description;
if (!key || seen.has(key)) return;
seen.add(key);
deduped.push(item);
});
return deduped.slice(0, limit);
}
function describeVideo(video) {
return video.title || video.description || video.aweme_id || "未命名作品";
}
function getVideoLink(video) {
return video.share_url || video.play_url || "";
}
async function loadJobDetail(jobId) {
const [job, events, childJobs] = await Promise.all([
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}`),
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}/events`).catch(() => []),
storyforgeFetch(`/v2/explore/jobs?parent_job_id=${encodeURIComponent(jobId)}`).catch(() => [])
]);
appState.lastJobDetail = { job, events: safeArray(events), childJobs: safeArray(childJobs) };
return appState.lastJobDetail;
}
function isJobCompleted(job) {
return String(job?.status || "").toLowerCase() === "completed";
}
function canDeriveAiVideo(job) {
if (!job || !isJobCompleted(job)) return false;
return String(job.line_type || "").toLowerCase() !== "ai_video";
}
function canDeriveRealCut(job) {
if (!job || !isJobCompleted(job)) return false;
const sourceType = String(job.source_type || "").toLowerCase();
return ["video_link", "upload_video"].includes(sourceType);
}
function hasIntegrationHealthData() {
return Boolean(appState.integrationHealth && typeof appState.integrationHealth === "object");
}
function getIntegrationDetail(key) {
const raw = hasIntegrationHealthData() ? appState.integrationHealth?.[key] : null;
return {
key,
available: Boolean(raw && typeof raw === "object"),
configured: Boolean(raw?.configured),
reachable: Boolean(raw?.reachable),
statusCode: Number(raw?.status_code || 0),
error: String(raw?.error || ""),
url: String(raw?.url || raw?.base_url || ""),
baseUrl: String(raw?.base_url || ""),
routeMode: String(raw?.route_mode || ""),
supportsUploads: raw?.supports_uploads !== undefined ? Boolean(raw?.supports_uploads) : true,
uploadStatusCode: Number(raw?.upload_status_code || 0),
uploadError: String(raw?.upload_error || ""),
uploadUrl: String(raw?.upload_url || "")
};
}
function isFnosTunnelCutvideo(detail) {
if (!detail || detail.key !== "cutvideo") return false;
if (detail.routeMode) return detail.routeMode === "fnos_tunnel";
const baseUrl = String(detail.baseUrl || detail.url || "");
return /:19186(?:\/|$)/.test(baseUrl);
}
function getCutvideoIntegrationHint(detail) {
if (isFnosTunnelCutvideo(detail)) {
return "fnOS NAS 隧道入口";
}
return "Windows 直连";
}
function getCutvideoIntegrationUrlLabel(detail) {
return isFnosTunnelCutvideo(detail) ? "fnOS NAS 隧道入口" : "Windows 直连";
}
function getIntegrationStatus(detail) {
if (!detail.available) {
return { tone: "blue", summary: "未拉取" };
}
if (detail.key === "cutvideo" && detail.reachable && !detail.supportsUploads) {
return { tone: "orange", summary: "缺上传能力" };
}
if (detail.reachable) {
return { tone: "green", summary: "在线" };
}
if (detail.configured) {
return { tone: "red", summary: "不可达" };
}
return { tone: "orange", summary: "未配置" };
}
function describeIntegrationFailure(key) {
const detail = getIntegrationDetail(key);
const meta = INTEGRATION_META[key] || { label: key };
if (!detail.available) return `${meta.label}健康状态未拉取`;
if (key === "cutvideo" && detail.reachable && !detail.supportsUploads) {
return `${meta.label}缺少 /api/uploads`;
}
if (!detail.configured) return `${meta.label}未配置`;
if (detail.statusCode) return `${meta.label}返回 HTTP ${detail.statusCode}`;
if (detail.error) return `${meta.label}${brief(detail.error, 42)}`;
return `${meta.label}不可达`;
}
function getPipelineGuard(kind) {
const config = PIPELINE_GUARDS[kind];
if (!config) {
return { enabled: true, reason: "", blocked: [] };
}
const blocked = config.dependencies
.map((key) => ({ key, detail: getIntegrationDetail(key), meta: INTEGRATION_META[key] || { label: key } }))
.filter((item) => {
if (!item.detail.available) return false;
if (!item.detail.reachable) return true;
if (item.key === "cutvideo" && !item.detail.supportsUploads) return true;
return false;
});
if (!blocked.length) {
return { enabled: true, reason: "", blocked: [] };
}
return {
enabled: false,
blocked,
reason: `${config.label}暂不可用:${blocked.map((item) => describeIntegrationFailure(item.key)).join("")}`
};
}
function getIntegrationCards() {
const currentModel = getCurrentModelProfile();
const localCatalog = appState.localModelCatalog || {};
return INTEGRATION_ORDER.map((key) => {
const detail = getIntegrationDetail(key);
const status = getIntegrationStatus(detail);
const meta = INTEGRATION_META[key] || { label: key, hint: key, impacts: [] };
const metaHint = key === "cutvideo" ? getCutvideoIntegrationHint(detail) : meta.hint;
let note = "尚未获取健康检查数据";
if (detail.available) {
if (detail.reachable) {
if (key === "cutvideo" && !detail.supportsUploads) {
note = detail.uploadStatusCode
? `主服务在线,但 /api/uploads 返回 HTTP ${detail.uploadStatusCode}`
: (detail.uploadError ? brief(detail.uploadError, 72) : "主服务在线,但缺少上传接口");
} else if (key === "cutvideo" && isFnosTunnelCutvideo(detail)) {
note = "当前走 fnOS NAS 隧道,不是 Windows 直连 cutvideo";
} else {
note = detail.statusCode
? `健康探测返回 HTTP ${detail.statusCode}`
: "TCP 探测已通过";
}
} else if (!detail.configured) {
note = "后端还没有配置该依赖地址";
} else if (detail.statusCode) {
note = `探测返回 HTTP ${detail.statusCode}`;
} else if (detail.error) {
note = brief(detail.error, 72);
} else {
note = "探测失败,请检查服务进程和网络";
}
}
let extra = "";
let actions = "";
if (key === "local_model") {
const availableModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean);
extra = currentModel
? `当前主模型:${currentModel.name} · ${currentModel.model_name || "-"}`
: `默认模型:${localCatalog.default_model || "GLM-5"}`;
if (availableModels.length) {
extra += ` · 可用:${availableModels.slice(0, 4).join(" / ")}${availableModels.length > 4 ? "…" : ""}`;
}
actions = [
localCatalog.management_url
? `<a class="tag blue" href="${escapeHtml(localCatalog.management_url)}" target="_blank" rel="noreferrer">打开管理页</a>`
: "",
`<span class="tag clickable-tag" data-action="open-preferred-model">设主模型</span>`
].filter(Boolean).join("");
}
if (key === "live_recorder") {
const ownedSources = safeArray(appState.liveRecorderSources);
const ownedFiles = safeArray(appState.liveRecorderFiles);
const activeCount = Number(appState.liveRecorderStatus?.recording_count || 0);
extra = ownedSources.length
? `我的录制源 ${ownedSources.length} · 录像 ${ownedFiles.length} · 正在录制 ${activeCount}`
: "当前还没有你的录制源";
actions = `<span class="tag clickable-tag" data-action="open-live-recorder">录制控制</span>`;
}
if (key === "cutvideo") {
extra = isFnosTunnelCutvideo(detail)
? `当前通过 fnOS NAS 隧道访问 ${detail.baseUrl || detail.url || "cutvideo"}`
: `当前直连 ${detail.baseUrl || detail.url || "cutvideo"}`;
}
return {
key,
meta: {
...meta,
hint: metaHint
},
detail,
status,
note,
extra,
actions
};
});
}
function renderLiveRecorderSummaryHtml() {
const sources = safeArray(appState.liveRecorderSources);
const files = safeArray(appState.liveRecorderFiles);
const status = appState.liveRecorderStatus || {};
const activeItems = safeArray(status.active_recordings);
const sourceHtml = sources.slice(0, 4).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.remote_name || item.source_url || "录制源")}</h4>
<p>${escapeHtml(platformLabel(item.platform))} · ${escapeHtml(item.quality || "原画")} · ${escapeHtml(item.enabled ? "启用中" : "已停用")}</p>
</div>
`).join("");
const fileHtml = files.slice(0, 4).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.name || "录像文件")}</h4>
<p>${escapeHtml(item.mtime || "-")} · ${escapeHtml(item.name || item.relative_path || "-")}</p>
<div class="task-meta">
<span class="tag blue clickable-tag" data-action="open-live-recorder-file" data-file-id="${escapeHtml(item.id || "")}">打开录像</span>
</div>
</div>
`).join("");
return `
<div class="task-item compact">
<h4>租户隔离状态</h4>
<p>当前只展示你自己名下的录制源、活动录制和录像文件。全局 NAS 配置不会直接暴露给前端。</p>
<div class="task-meta">
<span class="tag">${escapeHtml(`录制源 ${sources.length}`)}</span>
<span class="tag">${escapeHtml(`活动 ${activeItems.length}`)}</span>
<span class="tag">${escapeHtml(`文件 ${files.length}`)}</span>
</div>
</div>
${sourceHtml || `<div class="task-item compact"><h4>还没有录制源</h4><p>新增直播源后会自动挂到你的租户空间下。</p></div>`}
${fileHtml || `<div class="task-item compact"><h4>还没有录像文件</h4><p>录制完成后的文件会只出现在你的当前租户视图里。</p></div>`}
`;
}
function getStorageItemPath(item) {
return (
item?.artifacts?.source_path ||
item?.artifacts?.uploaded_path ||
item?.artifacts?.output_path ||
item?.artifacts?.file_path ||
item?.result?.source_path ||
item?.result?.output_path ||
item?.result?.file_path ||
item?.result?.path ||
item?.save_path ||
item?.path ||
item?.relative_path ||
item?.content_url ||
item?.job_id ||
item?.id ||
"-"
);
}
function renderStorageJobCards(items, emptyTitle, emptyText) {
return safeArray(items).slice(0, 4).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.name || item.job_id || "任务")}</h4>
<p>${escapeHtml(brief(getStorageItemPath(item), 140))}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(item.status || "-")}</span>
${item.project_name ? `<span class="tag">${escapeHtml(item.project_name)}</span>` : ""}
${item.line_type || item.source_type ? `<span class="tag">${escapeHtml(item.line_type || item.source_type)}</span>` : ""}
${item.id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(item.id)}">看详情</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>${escapeHtml(emptyTitle)}</h4><p>${escapeHtml(emptyText)}</p></div>`;
}
function renderStorageFileCards(items, emptyTitle, emptyText) {
return safeArray(items).slice(0, 4).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.name || item.relative_path || "文件")}</h4>
<p>${escapeHtml(brief(item.relative_path || item.name || item.content_url || "-", 140))}</p>
<div class="task-meta">
${(item.updated_at || item.mtime) ? `<span class="tag">${escapeHtml(formatDateTime(item.updated_at || item.mtime))}</span>` : ""}
${(item.size_bytes || item.size) ? `<span class="tag">${escapeHtml(formatBytes(item.size_bytes || item.size))}</span>` : ""}
${item.id ? `<span class="tag clickable-tag" data-action="${escapeHtml(item.kind === "downloads" || item.kind === "jobs" ? "open-storage-artifact" : "open-live-recorder-file")}" data-file-id="${escapeHtml(item.id)}">打开文件</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>${escapeHtml(emptyTitle)}</h4><p>${escapeHtml(emptyText)}</p></div>`;
}
function renderStorageStatusPanel() {
const storage = appState.storageStatus;
const dashboardJobs = safeArray(appState.dashboard?.recent_jobs);
const currentProject = getSelectedProject();
const currentAccount = getSelectedAccount();
const liveRecorderSources = safeArray(appState.liveRecorderSources);
const liveRecorderFiles = safeArray(appState.liveRecorderFiles);
const fallbackRecentJobs = dashboardJobs.slice(0, 4);
const recentJobs = safeArray(storage?.tenant_usage?.recent_jobs).length ? safeArray(storage?.tenant_usage?.recent_jobs) : fallbackRecentJobs;
const recentFiles = [
...safeArray(storage?.tenant_usage?.recent_download_artifacts),
...safeArray(storage?.tenant_usage?.recent_job_artifacts),
...safeArray(storage?.recent_files || storage?.recent_artifacts || storage?.tenant_usage?.recent_files || storage?.tenant_usage?.recent_artifacts)
];
if (!storage) {
const projectJobCount = appState.selectedProjectId ? dashboardJobs.filter((item) => item.project_id === appState.selectedProjectId).length : dashboardJobs.length;
const accountJobCount = appState.selectedAccountId ? dashboardJobs.filter((item) => item.account_id === appState.selectedAccountId).length : dashboardJobs.length;
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>存储状态</h3>
<div class="panel-subtitle">后端暂未提供 /v2/storage/status先用任务和录像文件做本地观察</div>
</div>
<span class="tag blue">降级视图</span>
</div>
<div class="task-item">
<h4>未拉取到 NAS 策略</h4>
<p>后端补上 storage/status 后,这里会自动显示账号 / 项目 / 任务分层、容量和最近写入路径。</p>
<div class="task-meta">
<span class="tag">最近任务 ${escapeHtml(formatNumber(projectJobCount))}</span>
<span class="tag">录制源 ${escapeHtml(formatNumber(liveRecorderSources.length))}</span>
<span class="tag">录像文件 ${escapeHtml(formatNumber(liveRecorderFiles.length))}</span>
</div>
</div>
<div class="mini-grid" style="margin-top:14px;">
<div class="mini-card">
<small>当前项目</small>
<strong>${escapeHtml(currentProject?.name || appState.selectedProjectId || "未选择")}</strong>
<span>任务 ${escapeHtml(formatNumber(projectJobCount))}</span>
</div>
<div class="mini-card">
<small>当前账号</small>
<strong>${escapeHtml(currentAccount ? getAccountName(currentAccount) : "未选择")}</strong>
<span>任务 ${escapeHtml(formatNumber(accountJobCount))}</span>
</div>
<div class="mini-card">
<small>录像源</small>
<strong>${escapeHtml(formatNumber(liveRecorderSources.length))}</strong>
<span>仅当前租户可见</span>
</div>
<div class="mini-card">
<small>最近文件</small>
<strong>${escapeHtml(formatNumber(liveRecorderFiles.length))}</strong>
<span>可直接打开</span>
</div>
</div>
<div class="list" style="margin-top:14px;">
<div class="task-item compact">
<h4>最近任务</h4>
<p>优先展示 dashboard.recent_jobs方便在没有 storage/status 时也能继续追踪产物。</p>
</div>
${renderStorageJobCards(
fallbackRecentJobs,
"还没有任务样本",
"等你完成一次分析、下载或剪辑后,这里就会出现最近的任务路径和详情入口。"
)}
<div class="task-item compact">
<h4>最近录像文件</h4>
<p>录像文件沿用 live-recorder 的当前租户视图,支持直接打开查看。</p>
</div>
${renderStorageFileCards(
liveRecorderFiles,
"还没有录像文件",
"录制完成后,这里会直接暴露当前租户的最近文件入口。"
)}
</div>
</div>
`;
}
const strategy = storage.strategy || {};
const disk = storage.disk || {};
const usage = storage.tenant_usage || {};
const strategyMode = (strategy.jobs?.mode || "local").toUpperCase();
const projectName = currentProject?.name || appState.selectedProjectId || "未选择";
const accountName = currentAccount ? getAccountName(currentAccount) : "未选择";
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: "当前账号下载", value: formatBytes(usage.account_downloads?.bytes), sub: `文件 ${formatNumber(usage.account_downloads?.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(strategyMode)}</span>
</div>
<div class="task-item">
<h4>当前观察范围</h4>
<p>${escapeHtml(`项目 ${projectName} · 账号 ${accountName} · 最近任务 ${recentJobs.length} 条 · 最近录像 ${liveRecorderFiles.length}`)}</p>
</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="two-col" style="margin-top:14px;">
<div class="task-item compact">
<h4>目录策略</h4>
<p>${escapeHtml([
`数据库 ${strategy.database?.path || "本机"}`,
`任务缓存 ${usage.project_jobs?.path || strategy.jobs?.path || "-"}`,
`下载缓存 ${usage.project_downloads?.path || strategy.downloads?.path || "-"}`,
`录制缓存 ${strategy.live_recorder?.path || strategy.live_recorder?.base_path || "-"}`
].join(" · "))}</p>
<div class="task-meta">
<span class="tag">项目层 ${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")}</span>
<span class="tag">账号层 ${escapeHtml(usage.account_jobs?.path || "-")}</span>
</div>
</div>
<div class="task-item compact">
<h4>产物入口</h4>
<p>最近任务、分析产物和录像文件都能直接点开,便于从 NAS 面板跳回详情或原文件。</p>
<div class="task-meta">
<span class="tag">任务 ${escapeHtml(formatNumber(recentJobs.length))}</span>
<span class="tag">产物 ${escapeHtml(formatNumber(recentFiles.length))}</span>
<span class="tag">录像 ${escapeHtml(formatNumber(liveRecorderFiles.length))}</span>
</div>
</div>
</div>
<div class="list" style="margin-top:14px;">
<div class="task-item compact">
<h4>最近任务样本</h4>
<p>默认取 storage.status 里的 recent_jobs如果后端没给会退回到 dashboard.recent_jobs。</p>
</div>
${renderStorageJobCards(
recentJobs,
"还没有缓存样本",
"上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。"
)}
${recentFiles.length ? `
<div class="task-item compact">
<h4>最近产物文件</h4>
<p>后端如果提供产物文件索引,这里会优先直接露出最近写入的文件入口。</p>
</div>
${renderStorageFileCards(
recentFiles,
"还没有产物文件",
"当前 storage/status 没有返回可直接打开的产物文件。"
)}
` : ""}
<div class="task-item compact">
<h4>最近录像文件</h4>
<p>如果 live-recorder 已接入,这里会继续显示当前租户的录像文件入口。</p>
</div>
${renderStorageFileCards(
liveRecorderFiles,
"还没有录像文件",
"录制完成后的文件会出现在当前租户的录像列表里。"
)}
</div>
</div>
`;
}
function renderOneLinerActionRegistryPanel() {
const items = safeArray(appState.onelinerActionRegistry);
if (!items.length) {
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>OneLiner 动作注册表</h3>
<div class="panel-subtitle">当前后端还没返回动作注册表,先沿用默认动作。</div>
</div>
</div>
<div class="task-item"><h4>暂未接入</h4><p>等 <code>/v2/oneliner/action-registry</code> 可用后,这里会显示动作开关、描述和租户级配置。</p></div>
</div>
`;
}
const grouped = items.reduce((acc, item) => {
const category = item.category || "custom";
acc[category] = acc[category] || [];
acc[category].push(item);
return acc;
}, {});
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>OneLiner 动作注册表</h3>
<div class="panel-subtitle">把 OneLiner 可执行动作做成租户级注册中心,便于商业化灰度和定制。</div>
</div>
<span class="tag blue">${escapeHtml(formatNumber(items.length))} 条</span>
</div>
<div class="list">
${Object.entries(grouped).map(([category, list]) => `
<div class="task-item compact">
<h4>${escapeHtml(category)}</h4>
<p>${escapeHtml(`当前分类下 ${list.length} 条动作。`)}</p>
<div class="task-meta">
${list.map((item) => `
<span class="tag ${item.status === "enabled" ? "green" : "orange"} clickable-tag" data-action="open-action-registry-edit" data-action-key="${escapeHtml(item.action_key || "")}">
${escapeHtml(item.label || item.action_key || "action")}
</span>
`).join("")}
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function renderTenantQuotaPanel() {
const quota = appState.tenantQuota;
const usage = appState.tenantUsage || quota?.usage || {};
const quotaNotice = renderQuotaBlockingNotice();
if (!quota && !usage) {
return `
<div class="panel pad">
<div class="panel-head"><div><h3>租户额度与审计</h3><div class="panel-subtitle">当前后端还没接入 quota / usage。</div></div></div>
<div class="task-item"><h4>暂未接入</h4><p>等 live collector 同步 `/v2/tenant/quota``/v2/tenant/usage` 后,这里会展示本周期预算、动作配额和最近计量记录。</p></div>
</div>
`;
}
const categories = usage?.categories || {};
const recentItems = safeArray(usage?.recent_items);
const cards = [
{ label: "预算", value: `${formatNumber((quota?.monthly_budget_cents || 0) / 100)}`, sub: `已用 ${formatNumber((usage?.total_cost_cents || 0) / 100)}` },
{ label: "分析配额", value: formatNumber(quota?.analysis_quota || 0), sub: `已用 ${formatNumber(categories.analysis?.quantity || 0)}` },
{ label: "文案配额", value: formatNumber(quota?.copy_quota || 0), sub: `已用 ${formatNumber(categories.copy?.quantity || 0)}` },
{ label: "AI 视频配额", value: formatNumber(quota?.ai_video_quota || 0), sub: `已用 ${formatNumber(categories.ai_video?.quantity || 0)}` },
{ label: "实拍剪辑配额", value: formatNumber(quota?.real_cut_quota || 0), sub: `已用 ${formatNumber(categories.real_cut?.quantity || 0)}` },
{ label: "存储上限", value: formatBytes(quota?.storage_limit_bytes || 0), sub: `当前 ${formatBytes(usage?.storage_bytes || 0)}` }
];
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>租户额度与审计</h3>
<div class="panel-subtitle">预算、动作配额和最近计量都按租户 + 项目隔离。</div>
</div>
<div class="task-meta">
<span class="tag ${quota?.enabled === false ? "orange" : "green"}">${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")}</span>
${quota?.storage_over_limit ? `<span class="tag red">存储超限</span>` : ""}
<span class="tag clickable-tag" data-action="open-tenant-quota">编辑额度</span>
</div>
</div>
${quotaNotice}
<div class="mini-grid" style="margin-top:14px;">
${cards.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>动作执行后会写入租户级 ledger便于后面做商业化配额、成本和审计。</p>
</div>
${recentItems.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.category || "usage")}</h4>
<p>${escapeHtml(formatDateTime(item.created_at))}</p>
<div class="task-meta">
<span class="tag blue">次数 ${escapeHtml(formatNumber(item.quantity || 0))}</span>
<span class="tag">成本 ${(item.cost_cents || 0) / 100} 元</span>
${item.reference_type ? `<span class="tag">${escapeHtml(item.reference_type)}</span>` : ""}
${item.reference_id ? `<span class="tag">${escapeHtml(brief(item.reference_id, 14))}</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有计量记录</h4><p>等 OneLiner 或生产动作实际执行后,这里会累积本周期的 usage ledger。</p></div>`}
</div>
</div>
`;
}
function policyScopeTagLabel(scopeKind, platform = "") {
if (scopeKind === "system_main") return "系统默认";
if (scopeKind === "system_platform") return `${platformLabel(platform || "douyin")} 默认`;
if (scopeKind === "user_global") return "我的全局";
if (scopeKind === "user_platform") return `${platformLabel(platform || "douyin")} 我的策略`;
if (scopeKind === "admin_override") return "管理员覆盖";
return "策略层";
}
function summarizePolicyHighlights(policy = {}, platform = "") {
const items = [];
if (policy?.tone?.style) items.push(`语气 ${policy.tone.style}`);
if (policy?.actions?.max_cards != null) items.push(`首页动作 ${formatNumber(policy.actions.max_cards)}`);
if (policy?.memory?.default_window) items.push(`记忆窗口 ${policy.memory.default_window}`);
if (platform && policy?.[platform]?.benchmark_mode) items.push(`${platformLabel(platform)} 对标 ${policy[platform].benchmark_mode}`);
if (policy?.guardrails?.require_admin_review) items.push("需管理员复核");
return items.slice(0, 4);
}
function getGovernanceDirectoryItems() {
return safeArray(appState.adminGovernanceDirectory?.items || appState.adminGovernanceDirectory);
}
function parseAdminOverrideScopeValue(value) {
const normalized = String(value || "").trim();
if (!normalized) return { targetUserId: "", targetProjectId: "" };
if (normalized.startsWith("project:")) {
const parts = normalized.slice("project:".length).split("|");
return { targetUserId: parts[0] || "", targetProjectId: parts[1] || "" };
}
if (normalized.startsWith("user:")) {
return { targetUserId: normalized.slice("user:".length), targetProjectId: "" };
}
return { targetUserId: normalized, targetProjectId: "" };
}
function getAdminOverrideTargetOptions(directoryItems = getGovernanceDirectoryItems()) {
return safeArray(directoryItems).flatMap((account) => {
const accountLabel = account.display_name || account.username || account.id;
const projects = safeArray(account.projects);
return [
{ value: `user:${account.id}`, label: `${accountLabel} · 全部项目` },
...projects.map((project) => ({
value: `project:${account.id}|${project.id}`,
label: `${accountLabel} / ${project.name || project.id}`
}))
];
});
}
function normalizeAdminOverrideTarget(target, directoryItems = getGovernanceDirectoryItems(), fallbackPlatform = "") {
const items = safeArray(directoryItems);
if (!items.length) {
return { targetUserId: "", targetProjectId: "", platform: normalizePlatformValue(fallbackPlatform, "douyin") };
}
const preferred = target && target.targetUserId
? items.find((item) => item.id === target.targetUserId)
: items.find((item) => item.role !== "super_admin") || items[0];
const preferredProjects = safeArray(preferred?.projects);
const project =
target?.targetProjectId
? preferredProjects.find((item) => item.id === target.targetProjectId)
: preferredProjects[0] || null;
return {
targetUserId: preferred?.id || "",
targetProjectId: project?.id || "",
platform: target?.platform === "" ? "" : normalizePlatformValue(target?.platform || fallbackPlatform, "douyin")
};
}
function findGovernanceDirectoryAccount(userId) {
return getGovernanceDirectoryItems().find((item) => item.id === userId) || null;
}
function findGovernanceDirectoryProject(userId, projectId) {
const account = findGovernanceDirectoryAccount(userId);
return safeArray(account?.projects).find((item) => item.id === projectId) || null;
}
function getAdminOverrideTargetSummary(target = appState.adminOverrideTarget) {
const normalized = target || {};
const account = findGovernanceDirectoryAccount(normalized.targetUserId || "");
const project = normalized.targetProjectId ? findGovernanceDirectoryProject(normalized.targetUserId || "", normalized.targetProjectId) : null;
return {
account,
project,
accountLabel: account?.display_name || account?.username || normalized.targetUserId || "未选择用户",
projectLabel: project?.name || (normalized.targetProjectId ? normalized.targetProjectId : "全部项目"),
platformLabel: normalized.platform ? platformLabel(normalized.platform) : "全部平台"
};
}
function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction = "", primaryLabel = "编辑策略", secondaryAction = "", secondaryLabel = "", secondaryPlatform = "", actions = null }) {
const layers = safeArray(effective?.layers);
const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || secondaryPlatform || "");
const activeAdminOverrideNotice = effective?.active_admin_override_notice || null;
const resolvedActions = safeArray(actions?.length ? actions : [
primaryAction ? { action: primaryAction, label: primaryLabel } : null,
secondaryAction ? { action: secondaryAction, label: secondaryLabel, platform: secondaryPlatform } : null
].filter(Boolean));
return `
<div class="task-item compact">
<h4>${escapeHtml(title)}</h4>
<p>${escapeHtml(subtitle || "当前还没有策略摘要。")}</p>
<div class="task-meta">
${layers.map((layer) => `<span class="tag ${layer.scope_kind === "admin_override" ? "orange" : "blue"}">${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}</span>`).join("") || `<span class="tag">尚未发布</span>`}
${highlights.map((item) => `<span class="tag green">${escapeHtml(item)}</span>`).join("")}
</div>
${activeAdminOverrideNotice?.title ? `
<div class="task-item compact" style="margin-top:10px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>管理员覆盖生效中</h4>
<p>${escapeHtml(activeAdminOverrideNotice.summary || activeAdminOverrideNotice.title || "当前这层管理员覆盖会优先于你的个人策略生效。")}</p>
<div class="task-meta">
<span class="tag orange">${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}</span>
${activeAdminOverrideNotice.platform_label ? `<span class="tag">${escapeHtml(activeAdminOverrideNotice.platform_label)}</span>` : ""}
</div>
</div>
` : ""}
${resolvedActions.length ? `
<div class="task-meta" style="margin-top:10px;">
${resolvedActions.map((item) => `
<span class="tag clickable-tag" data-action="${escapeHtml(item.action || "")}"
${item.platform ? `data-platform="${escapeHtml(item.platform)}"` : ""}
${item.sourceActionKey ? `data-source-action-key="${escapeHtml(item.sourceActionKey)}"` : ""}
${item.sourceScreen ? `data-source-screen="${escapeHtml(item.sourceScreen)}"` : ""}
${item.intentKey ? `data-intent-key="${escapeHtml(item.intentKey)}"` : ""}
${item.title ? `data-title="${escapeHtml(item.title)}"` : ""}
${item.goal ? `data-goal="${escapeHtml(item.goal)}"` : ""}
${item.summary ? `data-summary="${escapeHtml(item.summary)}"` : ""}
${item.platformScope ? `data-platform-scope="${escapeHtml(item.platformScope)}"` : ""}
${item.planSteps ? `data-plan-steps="${escapeHtml(JSON.stringify(item.planSteps))}"` : ""}>
${escapeHtml(item.label || "查看")}
</span>
`).join("")}
</div>
` : ""}
</div>
`;
}
function renderAdminGovernanceSummaryPanel() {
const systemMain = appState.adminSystemMainPolicy;
const systemPlatforms = safeArray(appState.adminSystemPlatformPolicies);
const configuredPlatforms = systemPlatforms.filter((item) => item?.current_version);
const targetSummary = getAdminOverrideTargetSummary();
const overrideBundle = appState.adminOverridePolicy;
return `
<div class="panel pad" style="box-shadow:none; margin-bottom:18px;">
<div class="panel-head">
<div>
<h3>系统级主 Agent 治理</h3>
<div class="panel-subtitle">先管系统默认主 Agent再按平台补默认策略普通用户的个性化覆盖会叠加在这些底座之上。</div>
</div>
<div class="task-meta">
<span class="tag blue">系统主 Agent ${escapeHtml(systemMain?.current_version ? "已发布" : "未发布")}</span>
<span class="tag">${escapeHtml(formatNumber(configuredPlatforms.length))} 个平台默认策略</span>
<span class="tag clickable-tag" data-action="open-system-main-policy">编辑系统主 Agent</span>
</div>
</div>
<div class="three-col">
<div class="entity-card pad">
<div class="cell-title">系统主 Agent</div>
<div class="cell-desc">${escapeHtml(systemMain?.current_version?.summary || "还没有发布系统默认主 Agent 策略。")}</div>
<div class="entity-meta">
<span class="tag ${systemMain?.current_version ? "green" : "orange"}">${escapeHtml(systemMain?.current_version ? `版本 ${formatNumber(systemMain.current_version.version_no)}` : "未发布")}</span>
<span class="tag">历史 ${escapeHtml(formatNumber(systemMain?.versions?.count || 0))}</span>
<span class="tag clickable-tag" data-action="open-system-main-policy-history">历史与回滚</span>
</div>
</div>
<div class="entity-card pad">
<div class="cell-title">管理员覆盖</div>
<div class="cell-desc">${escapeHtml(overrideBundle?.current_version?.summary || `${targetSummary.accountLabel} / ${targetSummary.projectLabel} / ${targetSummary.platformLabel}`)}</div>
<div class="entity-meta">
<span class="tag blue">${escapeHtml(targetSummary.accountLabel)}</span>
<span class="tag">${escapeHtml(targetSummary.projectLabel)}</span>
<span class="tag">${escapeHtml(targetSummary.platformLabel)}</span>
<span class="tag ${overrideBundle?.current_version ? "orange" : "blue"}">${escapeHtml(overrideBundle?.current_version ? `版本 ${formatNumber(overrideBundle.current_version.version_no)}` : "未发布")}</span>
<span class="tag clickable-tag" data-action="open-admin-override-target">切换目标</span>
<span class="tag clickable-tag" data-action="open-admin-override-policy">编辑覆盖</span>
<span class="tag clickable-tag" data-action="open-admin-override-history">历史与回滚</span>
</div>
</div>
${ACTIVE_PLATFORMS.map((platformItem) => {
const item = systemPlatforms.find((entry) => entry?.scope?.platform === platformItem.value) || null;
return `
<div class="entity-card pad">
<div class="cell-title">${escapeHtml(platformItem.label)} 默认策略</div>
<div class="cell-desc">${escapeHtml(item?.current_version?.summary || "还没有平台默认策略,当前会沿用系统主 Agent 默认。")}</div>
<div class="entity-meta">
<span class="tag ${item?.current_version ? "green" : "blue"}">${escapeHtml(item?.current_version ? `版本 ${formatNumber(item.current_version.version_no)}` : "沿用系统默认")}</span>
<span class="tag">历史 ${escapeHtml(formatNumber(item?.versions?.count || 0))}</span>
<span class="tag clickable-tag" data-action="open-system-platform-policy" data-platform="${escapeHtml(platformItem.value)}">编辑</span>
<span class="tag clickable-tag" data-action="open-system-platform-policy-history" data-platform="${escapeHtml(platformItem.value)}">历史与回滚</span>
</div>
</div>
`;
}).join("")}
</div>
</div>
`;
}
function renderAdminGovernanceAuditPanel() {
const targetSummary = getAdminOverrideTargetSummary();
const audits = safeArray(appState.adminPolicyAudits);
return `
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
<div class="panel-head">
<div>
<h3>覆盖与审计</h3>
<div class="panel-subtitle">按当前目标查看管理员覆盖、系统默认和相关策略变更,避免治理动作只留在弹窗里。</div>
</div>
<div class="task-meta">
<span class="tag blue">${escapeHtml(targetSummary.accountLabel)}</span>
<span class="tag">${escapeHtml(targetSummary.projectLabel)}</span>
<span class="tag">${escapeHtml(targetSummary.platformLabel)}</span>
<span class="tag clickable-tag" data-action="open-admin-override-target">切换目标</span>
</div>
</div>
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="task-item compact">
<h4>当前管理员覆盖</h4>
<p>${escapeHtml(appState.adminOverridePolicy?.current_version?.summary || "当前目标还没有管理员覆盖版本。")}</p>
<div class="task-meta">
<span class="tag ${appState.adminOverridePolicy?.current_version ? "orange" : "blue"}">${escapeHtml(appState.adminOverridePolicy?.current_version ? `版本 ${formatNumber(appState.adminOverridePolicy.current_version.version_no || 0)}` : "未发布")}</span>
<span class="tag clickable-tag" data-action="open-admin-override-policy">编辑覆盖</span>
<span class="tag clickable-tag" data-action="open-admin-override-history">历史与回滚</span>
</div>
</div>
<div class="task-item compact" style="margin-top:14px;">
<h4>治理说明</h4>
<p>管理员对用户策略的代改不会抹掉用户自己的历史,只会形成更高优先级的覆盖层,并在这里留下审计记录。</p>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近策略审计</h3><div class="panel-subtitle">系统默认、用户策略和管理员覆盖都会在这里形成时间线。</div></div></div>
<div class="list">
${renderPolicyAuditFeed(audits, "当前目标还没有策略审计记录。")}
</div>
</div>
</div>
</div>
</div>
`;
}
function renderPlatformAgentPanel() {
const items = safeArray(appState.platformAgents);
if (!items.length) {
return `
<div class="panel pad">
<div class="panel-head"><div><h3>平台 Agent</h3><div class="panel-subtitle">当前后端还没接入平台 Agent 控制面。</div></div></div>
<div class="task-item"><h4>暂未接入</h4><p>等 live collector 同步 `/v2/platform-agents` 后,这里会切成真实视图。</p></div>
</div>
`;
}
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>平台 Agent</h3>
<div class="panel-subtitle">按用户 + 平台隔离,沉淀该平台的方法论、记忆和技能。</div>
</div>
<span class="tag blue">${escapeHtml(formatNumber(items.length))} 个</span>
</div>
<div class="three-col">
${items.map((item) => `
<div class="entity-card pad">
<div class="cell-title">${escapeHtml(item.name || item.platform_label)}</div>
<div class="cell-desc">${escapeHtml(item.mission || item.notes || "先绑定执行 Agent再补任务目标和方法论。")}</div>
<div class="entity-meta">
<span class="tag ${item.status === "active" ? "green" : "blue"}">${escapeHtml(item.status || "draft")}</span>
${item.readiness_label ? `<span class="tag ${item.readiness_score >= 75 ? "green" : item.readiness_score >= 50 ? "blue" : "orange"}">${escapeHtml(item.readiness_label)} ${escapeHtml(formatNumber(item.readiness_score || 0))}</span>` : ""}
<span class="tag">记忆 ${escapeHtml(formatNumber(item.memory_count))}</span>
<span class="tag">技能 ${escapeHtml(formatNumber(item.skill_count))}</span>
<span class="tag">${escapeHtml(item.assistant?.name || "未绑 Agent")}</span>
</div>
${item.recent_memory || item.recent_skill ? `
<div class="list" style="margin-top:10px;">
${item.recent_memory ? `
<div class="task-item compact">
<h4>最近记忆 · ${escapeHtml(item.recent_memory.title || item.recent_memory.memory_key || "未命名")}</h4>
<p>${escapeHtml(brief(item.recent_memory.summary || "暂无摘要", 68))}</p>
</div>
` : ""}
${item.recent_skill ? `
<div class="task-item compact">
<h4>最近技能 · ${escapeHtml(item.recent_skill.name || item.recent_skill.skill_key || "未命名")}</h4>
<p>${escapeHtml(brief(item.recent_skill.test_spec?.summary || item.recent_skill.method?.summary || "暂无方法摘要", 68))}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(item.recent_skill.status || "draft")}</span>
<span class="tag blue">得分 ${escapeHtml(formatNumber(item.recent_skill.last_score || 0))}</span>
</div>
</div>
` : ""}
</div>
` : ""}
<div class="task-meta" style="margin-top:10px;">
<span class="tag clickable-tag" data-action="open-platform-agent-detail" data-platform="${escapeHtml(item.platform)}">查看详情</span>
<span class="tag clickable-tag" data-action="open-platform-agent-profile" data-platform="${escapeHtml(item.platform)}">配置</span>
<span class="tag clickable-tag" data-action="open-platform-agent-memory" data-platform="${escapeHtml(item.platform)}">补记忆</span>
<span class="tag clickable-tag" data-action="open-platform-agent-skill" data-platform="${escapeHtml(item.platform)}">补技能</span>
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function renderAdminOpsPanel() {
if (!isSuperAdmin()) return "";
const overview = appState.adminOpsOverview;
if (!overview) {
return `
<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()}
${renderRecoveryHistoryPanel()}
</div>
`;
}
const incidents = safeArray(overview.incidents).slice(0, 6);
const audits = safeArray(overview.recent_audits).slice(0, 5);
return `
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>运维与审计 Agent</h3>
<div class="panel-subtitle">只给管理员开放,主要盯日志、失败任务和集成异常。</div>
</div>
<div class="task-meta">
<span class="tag blue">${escapeHtml(formatNumber(overview.incident_count))} 条事件</span>
<span class="tag orange">待处理 ${escapeHtml(formatNumber(overview.open_incident_count || 0))}</span>
<span class="tag red">错误 ${escapeHtml(formatNumber(overview.severity_counts?.error || 0))}</span>
<span class="tag">${escapeHtml(formatNumber(overview.failed_job_count))} 个失败任务</span>
<span class="tag">修复计划 ${escapeHtml(formatNumber(overview.fix_run_count || 0))}</span>
<span class="tag clickable-tag" data-action="scan-admin-ops">重新扫描</span>
</div>
</div>
<div class="list">
${incidents.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title)}</h4>
<p>${escapeHtml(item.summary || "待补详情")}</p>
<div class="task-meta">
<span class="tag ${item.severity === "error" ? "red" : "orange"}">${escapeHtml(item.severity || "warn")}</span>
<span class="tag">${escapeHtml(item.status || "open")}</span>
${item.source_type ? `<span class="tag">${escapeHtml(item.source_type)}</span>` : ""}
${item.tenant_user_id ? `<span class="tag">租户 ${escapeHtml(brief(item.tenant_user_id, 12))}</span>` : ""}
${item.source_type === "job" ? actionTag("看任务详情", "open-job-detail", `data-job-id="${escapeHtml(item.source_id || "")}"`) : ""}
${item.source_type === "integration" ? actionTag("去自动流程", "goto-automation") : ""}
${item.tenant_project_id ? actionTag("去生产中心", "goto-production") : ""}
<span class="tag clickable-tag" data-action="open-admin-repair-plan" data-incident-id="${escapeHtml(item.id)}">生成修复计划</span>
<span class="tag clickable-tag" data-action="review-admin-incident" data-incident-id="${escapeHtml(item.id)}">审计处理</span>
</div>
</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>保留管理员扫描、放行、驳回等动作,方便商业化量产时追责和复盘。</p>
</div>
${audits.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.summary || item.action_key || "审计记录")}</h4>
<p>${escapeHtml(formatDateTime(item.created_at))}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(item.action_key || "audit")}</span>
<span class="tag">${escapeHtml(item.status || "recorded")}</span>
${item.incident_id ? `<span class="tag">事件 ${escapeHtml(brief(item.incident_id, 10))}</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有审计记录</h4><p>等管理员做一次扫描或审计处理后,这里会自动出现。</p></div>`}
</div>
${renderAdminFixRunsPanel()}
${renderRecoveryHistoryPanel()}
</div>
`;
}
function getIntegrationOverview() {
const cards = getIntegrationCards();
const reachableCount = cards.filter((item) => item.detail.available && item.detail.reachable).length;
const availableCount = cards.filter((item) => item.detail.available).length;
const aiVideoGuard = getPipelineGuard("aiVideo");
const realCutGuard = getPipelineGuard("realCut");
const blockedActions = [
!aiVideoGuard.enabled ? aiVideoGuard.reason : "",
!realCutGuard.enabled ? realCutGuard.reason : ""
].filter(Boolean);
const tone = !availableCount
? "blue"
: blockedActions.length
? "red"
: cards.some((item) => item.detail.available && !item.detail.reachable)
? "orange"
: "green";
const headline = !availableCount
? "依赖健康尚未拉取"
: blockedActions.length
? `自动链路受阻:${blockedActions.length}`
: `${reachableCount}/${cards.length} 项依赖在线`;
const subtitle = !availableCount
? "刷新后会显示直播录制 / cutvideo / huobao / n8n / ASR 的真实状态。"
: blockedActions.length
? blockedActions.join("")
: "AI 视频与实拍剪辑链路当前可直接发起。";
return { cards, tone, headline, subtitle };
}
function getJobSeedBrief(job) {
return [
job?.style_summary,
job?.transcript_text,
job?.result?.summary,
job?.artifacts?.brief,
job?.title
].find((value) => String(value || "").trim()) || "";
}
function collectHttpLinks(input, path = "result", bucket = []) {
if (!input) return bucket;
if (typeof input === "string") {
const value = input.trim();
if (/^https?:\/\//i.test(value)) {
bucket.push({ label: path, url: value });
}
return bucket;
}
if (Array.isArray(input)) {
input.forEach((item, index) => collectHttpLinks(item, `${path}[${index}]`, bucket));
return bucket;
}
if (typeof input === "object") {
Object.entries(input).forEach(([key, value]) => collectHttpLinks(value, `${path}.${key}`, bucket));
}
return bucket;
}
function getJobPreviewLinks(job) {
const deduped = [];
const seen = new Set();
collectHttpLinks(job?.result, "result", deduped);
collectHttpLinks(job?.artifacts, "artifacts", deduped);
return deduped.filter((item) => {
if (!item.url || seen.has(item.url)) return false;
seen.add(item.url);
return true;
}).slice(0, 8);
}
function isCandidateLinked(candidate, links) {
const accountId = String(candidate?.candidate_account_id || "");
const profileUrl = String(candidate?.candidate_profile_url || "");
return safeArray(links).some((link) => (
(accountId && String(link.target_account_id || "") === accountId) ||
(profileUrl && String(link.target_profile_url || "") === profileUrl)
));
}
function markSavedCandidate(candidate, links) {
const nextCandidates = safeArray(appState.lastSimilaritySearch?.candidates).map((item) => {
const sameAccount = String(item.candidate_account_id || "") && String(item.candidate_account_id || "") === String(candidate.candidate_account_id || "");
const sameUrl = String(item.candidate_profile_url || "") && String(item.candidate_profile_url || "") === String(candidate.candidate_profile_url || "");
if (!sameAccount && !sameUrl) return item;
return { ...item, saved: true };
});
if (appState.lastSimilaritySearch) {
appState.lastSimilaritySearch = {
...appState.lastSimilaritySearch,
candidates: nextCandidates
};
}
if (appState.selectedWorkspace) {
appState.selectedWorkspace = {
...appState.selectedWorkspace,
linked_accounts: safeArray(links)
};
}
}
async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmark") {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) throw new Error(gate.reason);
const platform = gate.platform;
const benchmarkPath = getWorkbenchRoute(platform, "benchmarkLinks", account.id);
if (!benchmarkPath) throw new Error(getPendingWorkbenchReason(platform));
const candidate = safeArray(appState.lastSimilaritySearch?.candidates)[Number(candidateIndex)];
if (!candidate) throw new Error("当前候选不存在,请先重新查相似");
const payload = {
target_account_ids: candidate.candidate_account_id ? [candidate.candidate_account_id] : [],
target_profile_urls: candidate.candidate_account_id ? [] : [candidate.candidate_profile_url].filter(Boolean),
relation_type: relationType,
note: brief(candidate.rationale_text || "由相似搜索自动加入对标库", 120),
search_id: appState.lastSimilaritySearch?.id || ""
};
if (!payload.target_account_ids.length && !payload.target_profile_urls.length) {
throw new Error("当前候选没有可保存的账号或主页链接");
}
const result = await storyforgeFetch(benchmarkPath, {
method: "POST",
body: payload
});
markSavedCandidate(candidate, result.links);
rememberAction("候选已存对标", `已把「${candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号"}」加入对标关系。`, "green", result);
renderAll();
}
function screenShell(title, subtitle, actionsHtml, bodyHtml) {
return `
<div class="screen-head">
<div>
<h2>${escapeHtml(title)}</h2>
<p>${escapeHtml(subtitle)}</p>
</div>
<div class="action-row">${actionsHtml || ""}</div>
</div>
${bodyHtml}
`;
}
function button(label, action, tone = "secondary", options = {}) {
const classes = ["btn", `btn-${tone}`];
if (options.className) classes.push(options.className);
if (options.disabledReason) classes.push("is-disabled");
const targetAction = options.disabledReason ? "show-disabled-reason" : action;
const title = options.disabledReason || options.title || "";
const attrs = options.attrs || "";
return `
<button
class="${classes.join(" ")}"
type="button"
data-action="${escapeHtml(targetAction)}"
${options.disabledReason ? `data-disabled-reason="${escapeHtml(options.disabledReason)}" aria-disabled="true"` : ""}
${title ? `title="${escapeHtml(title)}"` : ""}
${attrs}
>${escapeHtml(label)}</button>
`.replace(/\s+/g, " ").trim();
}
function actionTag(label, action, attrs = "", options = {}) {
const classes = ["tag"];
const targetAction = options.disabledReason ? "show-disabled-reason" : action;
if (options.disabledReason) {
classes.push("tag-disabled");
} else if (targetAction) {
classes.push("clickable-tag");
}
const title = options.disabledReason || options.title || "";
return `
<span
class="${classes.join(" ")}"
${targetAction ? `data-action="${escapeHtml(targetAction)}"` : ""}
${options.disabledReason ? `data-disabled-reason="${escapeHtml(options.disabledReason)}" aria-disabled="true"` : ""}
${title ? `title="${escapeHtml(title)}"` : ""}
${attrs}
>${escapeHtml(label)}</span>
`.replace(/\s+/g, " ").trim();
}
function renderPipelineButton(kind, tone = "secondary") {
const config = PIPELINE_GUARDS[kind];
if (!config) return "";
const guard = getPipelineGuard(kind);
const quotaGuard = kind === "aiVideo"
? getQuotaSummaryForCategory("ai_video")
: kind === "realCut"
? getQuotaSummaryForCategory("real_cut")
: { blocked: false, reason: "" };
return button(config.label, config.openAction, tone, {
disabledReason: guard.enabled ? (quotaGuard.blocked ? quotaGuard.reason : "") : guard.reason
});
}
function renderPipelineJobTag(kind, job, label) {
const config = PIPELINE_GUARDS[kind];
if (!config || !job?.id) return "";
const guard = getPipelineGuard(kind);
const quotaGuard = kind === "aiVideo"
? getQuotaSummaryForCategory("ai_video")
: kind === "realCut"
? getQuotaSummaryForCategory("real_cut")
: { blocked: false, reason: "" };
return actionTag(label, config.jobAction, `data-job-id="${escapeHtml(job.id)}"`, {
disabledReason: guard.enabled ? (quotaGuard.blocked ? quotaGuard.reason : "") : guard.reason
});
}
function renderIntegrationOverviewPanel(options = {}) {
const overview = getIntegrationOverview();
const cards = overview.cards;
const showActions = options.showActions !== false;
return `
<div class="panel pad integration-panel ${options.compact ? "integration-panel-compact" : ""}">
<div class="integration-summary ${overview.tone}">
<div>
<strong>${escapeHtml(overview.headline)}</strong>
<p>${escapeHtml(overview.subtitle)}</p>
</div>
${showActions ? `
<div class="integration-actions">
${renderPipelineButton("aiVideo", "primary")}
${renderPipelineButton("realCut")}
</div>
` : ""}
</div>
<div class="task-meta integration-highlights">
${cards.map((item) => `<span class="tag ${item.status.tone}">${escapeHtml(item.meta.label)} ${escapeHtml(item.status.summary)}</span>`).join("")}
</div>
<div class="layout-grid grid-4 integration-grid">
${cards.map((item) => `
<div class="integration-card ${item.status.tone}">
<div class="integration-card-head">
<div>
<h4>${escapeHtml(item.meta.label)}</h4>
<p>${escapeHtml(item.meta.hint)}</p>
</div>
<span class="tag ${item.status.tone}">${escapeHtml(item.status.summary)}</span>
</div>
<div class="task-meta">
${safeArray(item.meta.impacts).map((impact) => `<span class="tag">${escapeHtml(impact)}</span>`).join("")}
${item.detail.statusCode ? `<span class="tag">HTTP ${escapeHtml(item.detail.statusCode)}</span>` : ""}
</div>
<div class="integration-note">${escapeHtml(item.note)}</div>
${item.extra ? `<div class="integration-note">${escapeHtml(item.extra)}</div>` : ""}
<div class="integration-url">${escapeHtml(item.key === "live_recorder" ? "仅通过当前租户的后端代理访问" : (item.key === "cutvideo" ? `${getCutvideoIntegrationUrlLabel(item.detail)}${item.detail.url || item.detail.baseUrl || "未提供探测地址"}` : (item.detail.url || item.detail.baseUrl || "未提供探测地址")))}</div>
${item.actions ? `<div class="task-meta integration-highlights" style="margin-top:12px;">${item.actions}</div>` : ""}
</div>
`).join("")}
</div>
</div>
`;
}
function buildMainAgentHandoffAttrs({
sourceScreen = "",
sourceActionKey = "",
intentKey = "custom",
title = "",
goal = "",
summary = "",
platform = "",
platformScope = "single_platform",
planSteps = []
} = {}) {
const attrs = [
`data-source-screen="${escapeHtml(sourceScreen || appState.screen || "dashboard")}"`,
`data-source-action-key="${escapeHtml(sourceActionKey || "main-agent-handoff")}"`,
`data-intent-key="${escapeHtml(intentKey || "custom")}"`,
`data-title="${escapeHtml(title || goal || "交给主 Agent 处理")}"`,
`data-goal="${escapeHtml(goal || title || "交给主 Agent 处理")}"`,
`data-summary="${escapeHtml(summary || "")}"`,
`data-platform-scope="${escapeHtml(platformScope || "single_platform")}"`
];
if (platform) {
attrs.push(`data-platform="${escapeHtml(platform)}"`);
}
if (safeArray(planSteps).length) {
attrs.push(`data-plan-steps="${escapeHtml(JSON.stringify(safeArray(planSteps)))}"`);
}
return attrs.join(" ");
}
function buildMainAgentLandingAttrs({
runId = "",
screen = "",
title = "",
summary = ""
} = {}) {
const attrs = [];
if (runId) attrs.push(`data-main-agent-run-id="${escapeHtml(runId)}"`);
if (screen) attrs.push(`data-main-agent-screen="${escapeHtml(screen)}"`);
if (title) attrs.push(`data-main-agent-title="${escapeHtml(title)}"`);
if (summary) attrs.push(`data-main-agent-summary="${escapeHtml(summary)}"`);
return attrs.join(" ");
}
function resolveMainAgentLandingScreen(target) {
const value = String(target || "").trim();
if (!value) return "";
if (!value.startsWith("goto-")) return value;
const routeMap = {
"goto-discovery": "discovery",
"goto-intake": "intake",
"goto-automation": "automation",
"goto-playbook": "playbook",
"goto-tracking": "tracking",
"goto-production": "production",
"goto-strategy": "strategy",
"goto-review": "review"
};
return routeMap[value] || value.replace(/^goto-/, "");
}
function captureMainAgentLandingContext(action, targetScreen) {
const runId = String(action?.dataset?.mainAgentRunId || action?.dataset?.runId || "").trim();
const screen = resolveMainAgentLandingScreen(action?.dataset?.mainAgentScreen || targetScreen || "");
const title = String(action?.dataset?.mainAgentTitle || "").trim();
const summary = String(action?.dataset?.mainAgentSummary || "").trim();
if (!screen || (!runId && !title && !summary)) {
return;
}
appState.mainAgentLanding = {
screen,
runId,
title,
summary,
createdAt: new Date().toISOString()
};
}
function renderMainAgentLandingQuickActions(screenKey) {
const actionMap = {
intake: [
{ action: "create-project", label: "新建项目" },
{ action: "open-import-video-link", label: "导入作品" },
{ action: "open-upload-video", label: "上传视频" }
],
discovery: [
{ action: "open-import-homepage", label: "导入主页" },
{ action: "analyze-top-videos", label: "高分分析" },
{ action: "goto-tracking", label: "去跟踪账号" }
],
tracking: [
{ action: "refresh-tracking", label: "同步全部" },
{ action: "mark-tracking-read", label: "标记已读" },
{ action: "goto-discovery", label: "跳到找对标" }
],
automation: [
{ action: "refresh-data", label: "刷新状态" },
{ action: "goto-production", label: "去生产中心" }
],
playbook: [
{ action: "open-oneliner-profile", label: "配置 OneLiner" },
{ action: "open-create-assistant", label: "新建 Agent" },
{ action: "goto-production", label: "去生产中心" }
],
production: [
{ action: "goto-review", label: "去复盘" },
{ action: "batch-recover-jobs", label: "批量恢复" },
{ action: "refresh-data", label: "刷新队列" }
],
review: [
{ action: "open-create-review", label: "写复盘" },
{ action: "goto-production", label: "去生产中心" },
{ action: "refresh-data", label: "刷新结果" }
],
strategy: [
{ action: "open-user-global-policy", label: "编辑全局策略" },
{ action: "open-user-platform-policy", label: "编辑当前平台策略" },
{ action: "open-oneliner-profile", label: "调整 OneLiner" }
]
};
const actions = safeArray(actionMap[screenKey]).slice(0, 3);
if (!actions.length) return "";
return `
<div class="task-meta" style="margin-top:10px;">
${actions.map((item) => `<span class="tag clickable-tag" data-action="${escapeHtml(item.action)}">${escapeHtml(item.label)}</span>`).join("")}
</div>
`;
}
function renderMainAgentLandingNotice(screenKey) {
const landing = appState.mainAgentLanding;
if (!landing || landing.screen !== screenKey) return "";
const landingRun = safeArray(appState.onelinerRuns).find((item) => item.id === landing.runId);
const resultSections = landingRun?.result?.result_sections && typeof landingRun.result.result_sections === "object"
? landingRun.result.result_sections
: {};
const cards = safeArray(resultSections.cards).slice(0, 2);
const recommendedAction = landingRun?.result?.recommended_action || null;
return `
<div class="task-item compact" style="margin-bottom:18px; border-color:rgba(59, 130, 246, 0.24); background:linear-gradient(180deg, rgba(239, 246, 255, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>你正在处理主 Agent 的结果</h4>
<p>${escapeHtml(landing.summary || landing.title || "这是主 Agent 刚刚给出的下一步落点。")}</p>
${cards.length ? `
<div class="detail-grid" style="margin-top:12px;">
${cards.map((card, index) => `
<div class="task-item compact">
<h4>${escapeHtml(card?.title || (index === 0 ? "当前焦点" : "结果摘要"))}</h4>
<p>${escapeHtml(card?.body || "主 Agent 已整理出一条可继续推进的建议。")}</p>
</div>
`).join("")}
</div>
` : ""}
${renderMainAgentLandingQuickActions(screenKey)}
<div class="task-meta">
${landing.title ? `<span class="tag blue">${escapeHtml(landing.title)}</span>` : ""}
${resultSections.workstream_label ? `<span class="tag">${escapeHtml(resultSections.workstream_label)}</span>` : ""}
${recommendedAction?.label ? `<span class="tag blue">${escapeHtml(recommendedAction.label)}</span>` : ""}
${landing.runId ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(landing.runId)}">查看结果</span>` : ""}
<span class="tag clickable-tag" data-action="dismiss-main-agent-landing">收起提示</span>
</div>
</div>
`;
}
function renderEmptyState(title, description) {
return `<div class="panel pad"><div class="empty-state"><strong>${escapeHtml(title)}</strong><p>${escapeHtml(description)}</p></div></div>`;
}
function renderPlatformSwitchChips(currentPlatform) {
return getPlatformOptions().map((item) => `
<span
class="chip clickable-tag ${item.value === currentPlatform ? "active" : ""}"
data-action="select-platform"
data-platform="${escapeHtml(item.value)}"
>
${escapeHtml(getPlatformShortLabel(item.value))}
</span>
`).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>分析报告来自 <code>/analysis-reports</code>,可直接对照结论和建议。</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 renderAdminWorkbenchScreen() {
if (!isSuperAdmin()) {
return screenShell(
"管理员配置台",
"仅超级管理员可见。",
"",
renderEmptyState("无权限", "请使用超级管理员账号访问管理员配置台。")
);
}
const tabs = [
{ value: "integrations", label: "依赖健康" },
{ value: "storage", label: "存储状态" },
{ value: "agents", label: "Agent 治理" },
{ value: "governance_audit", label: "覆盖与审计" },
{ value: "ops", label: "运维审计" }
];
const activeTab = getActiveDetailTab("adminWorkbenchTab", tabs);
return screenShell(
"管理员配置台",
"系统级依赖、存储、平台 Agent 与运维治理。",
"",
`
<div class="panel pad">
<div class="panel-head">
<div>
<h3>系统治理工作区</h3>
<div class="panel-subtitle">按系统依赖存储Agent 治理和运维审计分区查看不再整页堆叠</div>
</div>
</div>
${renderDetailTabs("adminWorkbenchTab", tabs)}
${activeTab === "integrations"
? renderIntegrationOverviewPanel({ showActions: false })
: activeTab === "storage"
? renderStorageStatusPanel()
: activeTab === "agents"
? `${renderAdminGovernanceSummaryPanel()}${renderPlatformAgentPanel()}<div style="margin-top:18px;">${renderOneLinerActionRegistryPanel()}</div>`
: activeTab === "governance_audit"
? renderAdminGovernanceAuditPanel()
: renderAdminOpsPanel()}
</div>
`
);
}
function renderDashboardScreen() {
if (!appState.session) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("项目总台", "会话签发成功后,这里会自动切成真实项目总台。");
}
return screenShell(
"项目总台",
"先自动连接工作区再加载项目、对标、Agent 和生产状态。",
`${button("自动连接", "open-auth", "primary")}`,
renderEmptyState("还没有连接 StoryForge", "自动连接成功后,这里会替换成真实项目总台。")
);
}
if (!appState.dashboard) {
return screenShell(
"项目总台",
appState.me?.approval_status === "pending" ? "当前账号还在等待审批。" : "正在加载项目数据。",
`${button("刷新", "refresh-data")} ${button("切换连接", "open-auth")}`,
renderEmptyState("工作区暂未就绪", appState.message || "如果账号未审批通过,当前页会先停在待审批状态。")
);
}
const dashboardHomeRenderer = window.StoryForgeDashboardHome;
const homeModel = buildDashboardHomeModel();
return screenShell(
"项目总台",
"先做最能推进当前项目的一步,再按需看概览。",
`${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
dashboardHomeRenderer?.renderDashboardHome
? dashboardHomeRenderer.renderDashboardHome(homeModel, { escapeHtml })
: renderEmptyState("首页模块未加载", "请刷新页面后重试。")
);
}
function renderProjectsScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("我的项目", "工作区就绪后,这里会自动加载真实项目和导入队列。");
}
return screenShell("我的项目", "先完成工作区自动连接,再加载项目。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("项目未加载", "自动连接成功后,这里会显示真实项目和导入队列。"));
}
const projects = safeArray(appState.dashboard.projects);
const selectedProject = getSelectedProject();
const intakeHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "intake",
sourceActionKey: "project-intake-handoff",
intentKey: "project_intake",
title: selectedProject ? `继续推进项目 ${selectedProject.name}` : "继续梳理项目工作区",
goal: selectedProject
? `围绕项目 ${selectedProject.name} 生成下一步推进计划`
: "根据当前项目列表和导入情况,生成下一步项目推进计划",
summary: selectedProject
? "主 Agent 会结合当前项目状态、账号和任务规模,整理下一步动作。"
: "主 Agent 会先看项目列表和内容导入情况,再整理下一步动作。",
platform: getPreferredPlatform(),
platformScope: "single_platform",
planSteps: [
"读取当前项目、账号和任务状态",
"检查是否需要补导入或切换项目",
"生成下一步项目推进计划"
]
});
return screenShell(
"我的项目",
"先建项目,再决定是否绑定自己的账号。",
`${button("新建项目", "create-project", "primary")} ${button("导入作品", "open-import-video-link")} ${button("导入文本", "open-import-text")} ${button("上传视频", "open-upload-video")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: intakeHandoffAttrs })}`,
`
${renderMainAgentLandingNotice("intake")}
<div class="hero-card">
<h3>当前项目</h3>
<p>${escapeHtml(selectedProject?.name || "还没有项目")} · ${escapeHtml(selectedProject?.description || "创建后即可承接对标、Agent 和生产任务。")}</p>
<div class="chip-row" style="margin-top:14px;">
${projects.map((project) => `<span class="chip ${project.id === appState.selectedProjectId ? "active" : ""}" data-action="select-project" data-project-id="${escapeHtml(project.id)}">${escapeHtml(project.name)}</span>`).join("") || `<span class="chip active">暂无项目</span>`}
</div>
</div>
<div class="layout-grid grid-main" style="margin-top:18px;">
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>项目状态</h3><div class="panel-subtitle"></div></div></div>
<div class="project-status-grid">
${projects.map((project) => {
const stats = getProjectStats(project.id);
return `
<div class="entity-card pad">
<div class="cell-title">${escapeHtml(project.name)}</div>
<div class="cell-desc">${escapeHtml(project.description || "未填写说明")}</div>
<div class="entity-meta">
<span class="tag blue">知识库 ${escapeHtml(formatNumber(stats.knowledgeBases.length))}</span>
<span class="tag">Agent ${escapeHtml(formatNumber(stats.assistants.length))}</span>
<span class="tag green">任务 ${escapeHtml(formatNumber(stats.jobs.length))}</span>
</div>
</div>
`;
}).join("") || `<div class="empty-state">当前还没有项目。</div>`}
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>最近导入队列</h3><div class="panel-subtitle"></div></div></div>
<div class="list">
${safeArray(appState.contentSources).slice(0, 6).map((source) => `
<div class="review-card">
<h4>${escapeHtml(source.title || source.handle || source.source_url || source.id)}</h4>
<p>${escapeHtml(source.platform || source.source_kind || "内容源")} · ${escapeHtml(source.source_url || source.local_path || "暂无链接")}</p>
<div class="task-meta">
<span class="tag ${source.project_id === appState.selectedProjectId ? "green" : "blue"}">${source.project_id === appState.selectedProjectId ? "当前项目" : "其他项目"}</span>
<span class="tag">${escapeHtml(source.source_kind || "-")}</span>
</div>
</div>
`).join("") || `<div class="review-card"><h4>还没有导入内容</h4><p>先去“找对标”导入主页、作品或本地视频。</p></div>`}
</div>
</div>
</div>
</div>
`
);
}
function getActiveDetailTab(stateKey, tabs) {
const fallback = tabs[0]?.value || "";
const values = tabs.map((item) => item.value);
const active = values.includes(appState[stateKey]) ? appState[stateKey] : fallback;
appState[stateKey] = active;
return active;
}
function renderDetailTabs(stateKey, tabs) {
const active = getActiveDetailTab(stateKey, tabs);
return `
<div class="tab-row page-detail-tabs">
${tabs.map((tab) => `
<button
class="tab ${tab.value === active ? "active" : ""}"
type="button"
aria-pressed="${tab.value === active ? "true" : "false"}"
data-action="select-page-tab"
data-page-tab-key="${escapeHtml(stateKey)}"
data-page-tab-value="${escapeHtml(tab.value)}"
>
${escapeHtml(tab.label)}
</button>
`).join("")}
</div>
`;
}
function renderDiscoveryOverviewSection({ selected, selectedProject, importedSources, tracked, workbenchReason, topVideos, reports, latestVideos, currentPlatformLabel }) {
return `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>接入当前项目</h3><div class="panel-subtitle"> Agent </div></div><span class="tag ${importedSources.length ? "green" : "blue"}">${escapeHtml(importedSources.length ? "" : "")}</span></div>
${selected ? `
<div class="task-item">
<h4>${escapeHtml(selectedProject?.name || "未选项目")}</h4>
<p>${escapeHtml(importedSources.length ? `当前项目已接入 ${formatNumber(importedSources.length)} 个内容源,可继续同步或换 Agent。` : "当前项目还没有接入这个对标账号,可直接导入主页并绑定 Agent。")}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(selectedProject?.name || "未选项目")}</span>
<span class="tag">${escapeHtml(getSelectedAssistant()?.name || "未选 Agent")}</span>
${actionTag(importedSources.length ? "继续同步" : "导入当前对标", "open-import-selected-account", "", { disabledReason: workbenchReason || "" })}
${tracked ? `<span class="tag green">已在跟踪</span>` : actionTag("加入跟踪", "open-track-selected-account", "", { disabledReason: workbenchReason || "" })}
</div>
</div>
` : `<div class="task-item"><h4>还没有选中账号</h4><p>先从上方列表选一个对标账号,再决定是否导入到当前项目。</p></div>`}
</div>
<div class="three-col">
<div class="insight-card">
<h4>账号画像</h4>
<ul>
<li>${escapeHtml(selected?.signature || "暂无签名")}</li>
<li>${escapeHtml("平台:" + currentPlatformLabel)}</li>
<li>${escapeHtml("标签:" + (safeArray(selected?.tags).slice(0, 4).join(" / ") || "暂无标签"))}</li>
<li>${escapeHtml("同步状态:" + (selected?.sync_status || "-"))}</li>
</ul>
</div>
<div class="insight-card">
<h4>高分作品</h4>
<ul>
${topVideos.map((video) => `<li>${escapeHtml(describeVideo(video))}</li>`).join("") || "<li>暂无高分作品</li>"}
</ul>
</div>
<div class="insight-card">
<h4>最近报告</h4>
<ul>
${reports.slice(0, 3).map((report) => {
const suggestion = safeArray(report.suggestions)[0];
const summary = suggestion?.parsed_json?.executive_summary || suggestion?.suggestion_text || report.focus_text || "暂无结论";
return `<li>${escapeHtml(brief(summary, 48))}</li>`;
}).join("") || "<li>暂无分析报告</li>"}
</ul>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最新作品</h3><div class="panel-subtitle"></div></div><span class="tag">${escapeHtml(formatNumber(latestVideos.length))} </span></div>
<div class="list">
${latestVideos.map((video) => `
<div class="task-item">
<h4>${escapeHtml(describeVideo(video))}</h4>
<p>发布时间 ${escapeHtml(formatDateTime(video.published_at))} · 播放 ${escapeHtml(formatNumber(video.stats?.play))} · 点赞 ${escapeHtml(formatNumber(video.stats?.like))}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(video.content_type || "video")}</span>
<span class="tag green">得分 ${escapeHtml(formatNumber(video.score?.performance_score || 0))}</span>
${getVideoLink(video) ? `<a class="tag" href="${escapeHtml(getVideoLink(video))}" target="_blank" rel="noreferrer">打开原作品</a>` : ""}
</div>
</div>
`).join("") || `<div class="task-item"><h4>还没有最近作品</h4><p>当前账号只同步了基础信息,还没拉到完整作品列表。</p></div>`}
</div>
</div>
</div>
</div>
`;
}
function renderDiscoveryRelationsSection(linkedAccounts, similarCandidates) {
return `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>已绑关系</h3><div class="panel-subtitle"></div></div><span class="tag">${escapeHtml(formatNumber(linkedAccounts.length))} </span></div>
<div class="list">
${linkedAccounts.map((link) => `
<div class="task-item">
<h4>${escapeHtml(link.target_nickname || link.target_profile_url || "未命名对标")}</h4>
<p>${escapeHtml(link.note || link.target_profile_url || "已保存对标关系")}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(link.relation_type || "benchmark")}</span>
${link.target_account_id ? `<span class="tag clickable-tag" data-action="select-account" data-account-id="${escapeHtml(link.target_account_id)}">看详情</span>` : ""}
${link.target_profile_url ? `<a class="tag" href="${escapeHtml(link.target_profile_url)}" target="_blank" rel="noreferrer">打开主页</a>` : ""}
</div>
</div>
`).join("") || `<div class="task-item"><h4>暂无已保存对标</h4><p>当前账号还没有保存过对标关系。</p></div>`}
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近相似候选</h3><div class="panel-subtitle"> Agent </div></div><span class="tag">${escapeHtml(formatNumber(similarCandidates.length))} </span></div>
<div class="list">
${similarCandidates.map((candidate, index) => `
<div class="task-item">
<h4>${escapeHtml(candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号")}</h4>
<p>${escapeHtml(brief(candidate.rationale_text || "暂无理由", 96))}</p>
<div class="task-meta">
<span class="tag blue">启发分 ${escapeHtml(formatNumber(candidate.agent_score || candidate.heuristic_score || 0))}</span>
${candidate.candidate_account_id ? `<span class="tag clickable-tag" data-action="select-account" data-account-id="${escapeHtml(candidate.candidate_account_id)}">看详情</span>` : ""}
${isCandidateLinked(candidate, linkedAccounts) || candidate.saved ? `<span class="tag green">已保存</span>` : `<span class="tag clickable-tag" data-action="save-candidate-benchmark" data-candidate-index="${escapeHtml(index)}">存对标</span>`}
${candidate.candidate_profile_url ? `<a class="tag" href="${escapeHtml(candidate.candidate_profile_url)}" target="_blank" rel="noreferrer">打开主页</a>` : ""}
</div>
</div>
`).join("") || `<div class="task-item"><h4>还没有相似候选</h4><p>先点“查相似”,这里会展示最近一轮结果。</p></div>`}
</div>
</div>
</div>
</div>
`;
}
function renderDiscoveryScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("找对标", "工作区就绪后,这里会自动显示当前平台的账号列表和详情。");
}
return screenShell("找对标", "完成工作区自动连接后才能加载真实对标账号。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "自动连接成功后,这里会显示当前平台的账号列表和详情。"));
}
const query = appState.discoveryQuery.toLowerCase();
const currentPlatform = getCurrentPlatformValue();
const currentPlatformLabel = getPlatformShortLabel(currentPlatform);
const accounts = getAccountsForPlatform(currentPlatform).filter((account) => {
if (!query) return true;
return [getAccountName(account), account.signature, getAccountProfileUrl(account), getAccountHandle(account), ...safeArray(account.tags), ...safeArray(account.keywords)]
.join(" ")
.toLowerCase()
.includes(query);
});
const selected = getSelectedAccount();
const selectedPlatform = getAccountPlatform(selected);
const effectivePlatform = selectedPlatform || currentPlatform;
const workbenchReason = !isWorkbenchPlatform(effectivePlatform) ? getPendingWorkbenchReason(effectivePlatform) : "";
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);
const effectiveVideos = videos.length ? videos : fallbackVideos;
const topVideos = getHighScoreVideos(3);
const latestVideos = getLatestVideos(2);
const similarCandidates = safeArray(appState.lastSimilaritySearch?.candidates).slice(0, 5);
const selectedProject = getSelectedProject();
const importedSources = getCurrentProjectSourcesForAccount(selected, selectedProject?.id || "");
const tracked = selected?.id ? isTrackedAccount(selected.id) : false;
const discoveryHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "discovery",
sourceActionKey: "discovery-handoff",
intentKey: "benchmark_discovery",
title: selected ? `继续处理对标 ${getAccountName(selected)}` : "继续推进对标工作",
goal: selected
? `围绕 ${getAccountName(selected)} 输出一版下一步对标计划`
: `根据当前${currentPlatformLabel}账号列表生成下一步对标推进计划`,
summary: selected
? "结合当前选中的对标账号、分析报告和相似关系,生成下一步动作。"
: "结合当前账号池和已导入内容,整理一版可执行的对标计划。",
platform: effectivePlatform || currentPlatform,
platformScope: "single_platform",
planSteps: [
"读取当前对标账号与已导入内容",
"检查分析报告与相似关系",
"生成下一步对标推进计划"
]
});
const detailTabs = [
{ value: "overview", label: "账号概览" },
{ value: "snapshots", label: "快照 / 字段 / 报告" },
{ value: "relations", label: "相似对标 / 已绑关系" }
];
const activeTab = getActiveDetailTab("discoveryDetailTab", detailTabs);
const selectedSummaryHtml = `
<div class="hero-card" style="padding:18px;">
<div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(getAccountName(selected) || "SF"))}</div>
<div>
<h3>${escapeHtml(getAccountName(selected) || "还没有选中账号")}</h3>
<p>${escapeHtml(getAccountProfileUrl(selected) || selected?.signature || "先从上方列表选一个账号,这里会展示当前对象。")}</p>
</div>
</div>
<div class="mini-grid">
<div class="mini-card"><small>作品数</small><strong>${escapeHtml(formatNumber(selected?.video_summary?.count))}</strong></div>
<div class="mini-card"><small>高分作品</small><strong>${escapeHtml(formatNumber(topVideos.length))}</strong></div>
<div class="mini-card"><small>报告数</small><strong>${escapeHtml(formatNumber(reports.length))}</strong></div>
<div class="mini-card"><small>已绑对标</small><strong>${escapeHtml(formatNumber(linkedAccounts.length))}</strong></div>
</div>
</div>
`;
let detailBodyHtml = "";
if (activeTab === "overview") {
detailBodyHtml = renderDiscoveryOverviewSection({
selected,
selectedProject,
importedSources,
tracked,
workbenchReason,
topVideos,
reports,
latestVideos,
currentPlatformLabel
});
} else if (activeTab === "snapshots") {
detailBodyHtml = renderDouyinInsightPanel();
} else {
detailBodyHtml = renderDiscoveryRelationsSection(linkedAccounts, similarCandidates);
}
return screenShell(
"找对标",
isWorkbenchPlatform(currentPlatform)
? `这里已经接入真实${currentPlatformLabel}账号列表和单账号详情`
: `${workbenchReason}当前仍可导入内容源绑定 Agent 和沉淀复盘`,
`${button("导入主页", "open-import-homepage")} ${button("导入当前对标", "open-import-selected-account", "secondary", { disabledReason: workbenchReason || "" })} ${button(tracked ? "已在跟踪" : "加入跟踪", "open-track-selected-account", "secondary", { disabledReason: workbenchReason || "" })} ${button("账号分析", "analyze-selected-account", "secondary", { disabledReason: workbenchReason || "" })} ${button("高分分析", "analyze-top-videos", "secondary", { disabledReason: workbenchReason || "" })} ${button("查相似", "open-similar-search", "secondary", { disabledReason: workbenchReason || "" })} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: discoveryHandoffAttrs })} ${button("存对标", "open-benchmark-link", "primary", { disabledReason: workbenchReason || "" })}`,
`
${renderMainAgentLandingNotice("discovery")}
<div class="panel">
<div class="toolbar">
<div class="toolbar-stack">
<label class="search search-inline">
<span></span>
<input data-action="discovery-query" value="${escapeHtml(appState.discoveryQuery)}" placeholder="搜账号名、账号标识、主页链接、关键词" />
</label>
<div class="filters">
<div class="filter">平台${escapeHtml(currentPlatformLabel)}</div>
<div class="filter">账号数${escapeHtml(formatNumber(accounts.length))}</div>
<div class="filter">报告${escapeHtml(formatNumber(reports.length))}</div>
<div class="filter">作品${escapeHtml(formatNumber(effectiveVideos.length))}</div>
</div>
</div>
<div class="side-stack">
<div class="chip-row">
${renderPlatformSwitchChips(currentPlatform)}
</div>
</div>
</div>
<div class="mobile-only mobile-account-list">
${accounts.map((account) => {
const active = account.id === appState.selectedAccountId;
return `
<button class="account-select-card ${active ? "is-active" : ""}" type="button" data-action="select-account" data-account-id="${escapeHtml(account.id)}">
<div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(getAccountName(account)))}</div>
<div>
<div class="cell-title">${escapeHtml(getAccountName(account))}</div>
<div class="cell-desc">${escapeHtml(getAccountSubtitle(account) || "未填账号标识")}</div>
</div>
</div>
<div class="kpi-inline">
<span>作品 ${escapeHtml(formatNumber(account.video_summary?.count))}</span>
<span>均播 ${escapeHtml(formatNumber(account.video_summary?.avg_play))}</span>
<span>均赞 ${escapeHtml(formatNumber(account.video_summary?.avg_like))}</span>
</div>
<div class="task-meta">
<span class="tag ${active ? "green" : "blue"}">${active ? "当前选中" : "点击查看"}</span>
<span class="tag">${escapeHtml(account.sync_status || "ready")}</span>
</div>
</button>
`;
}).join("") || `<div class="empty-state">当前平台没有账号数据。</div>`}
</div>
<div class="table-wrap">
<table class="account-table">
<thead>
<tr>
<th>账号</th>
<th>签名</th>
<th>作品数</th>
<th>平均播放</th>
<th>平均点赞</th>
<th>动作</th>
</tr>
</thead>
<tbody>
${accounts.map((account) => `
<tr>
<td>
<div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(getAccountName(account)))}</div>
<div>
<div class="cell-title">${escapeHtml(getAccountName(account))}</div>
<div class="cell-desc">${escapeHtml(getAccountSubtitle(account) || "-")}</div>
</div>
</div>
</td>
<td>${escapeHtml(brief(account.signature || "暂无签名", 36))}</td>
<td><span class="metric">${escapeHtml(formatNumber(account.video_summary?.count))}</span></td>
<td>${escapeHtml(formatNumber(account.video_summary?.avg_play))}</td>
<td>${escapeHtml(formatNumber(account.video_summary?.avg_like))}</td>
<td><div class="row-meta"><span class="tag" data-action="select-account" data-account-id="${escapeHtml(account.id)}">查看</span></div></td>
</tr>
`).join("") || `<tr><td colspan="6">当前平台没有账号数据。</td></tr>`}
</tbody>
</table>
</div>
<div class="panel pad page-section-panel" id="selected-account-anchor" style="border-top:1px solid var(--line); border-radius:0; box-shadow:none;">
<div class="panel-head">
<div>
<h3>当前选中对标</h3>
<div class="panel-subtitle">先看核心信息再按分类切换深层内容</div>
</div>
<span class="tag blue">${escapeHtml(getAccountName(selected) || "未选中")}</span>
</div>
${selectedSummaryHtml}
${renderDetailTabs("discoveryDetailTab", detailTabs)}
${detailBodyHtml}
</div>
</div>
`
);
}
function renderTrackingScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("跟踪账号", "工作区就绪后,这里会自动加载真实跟踪对象和日报。");
}
return screenShell("跟踪账号", "完成工作区自动连接后才能生成真实日报。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。"));
}
const currentPlatform = getCurrentPlatformValue();
const trackingAccountsPath = getWorkbenchRoute(currentPlatform, "trackingAccounts");
if (!trackingAccountsPath || !backendSupports(trackingAccountsPath)) {
return screenShell(
"跟踪账号",
`${getPendingWorkbenchReason(currentPlatform)}`,
`${button("跳到找对标", "goto-discovery", "primary")}`,
renderEmptyState("跟踪能力暂未接入", `这套后端还没有接入 ${platformLabel(currentPlatform)} 跟踪接口 live collector 同步后这里会自动切成真实日报`)
);
}
const trackedAccounts = getTrackingAccounts().filter((item) => item.platform === currentPlatform);
const digestItems = getTrackingDigestItems(12, { platform: currentPlatform });
const platformCursor = getTrackingCursorForPlatform(currentPlatform) || appState.lastSeenAt;
const cursorLabel = platformCursor ? formatDateTime(platformCursor) : "尚未记录";
const trackingHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "tracking",
sourceActionKey: "tracking-handoff",
intentKey: "tracking_digest",
title: `继续处理${getPlatformShortLabel(currentPlatform)}跟踪日报`,
goal: `基于当前${getPlatformShortLabel(currentPlatform)}跟踪账号和日报生成下一步跟进计划`,
summary: "主 Agent 会结合已跟踪账号、日报窗口和更新摘要,给出下一步动作。",
platform: currentPlatform,
platformScope: "single_platform",
planSteps: [
"读取当前平台跟踪账号和最新日报",
"识别值得继续跟进的账号或内容",
"生成一版跟踪推进计划"
]
});
return screenShell(
"跟踪账号",
`这里已经接上真实${getPlatformShortLabel(currentPlatform)}跟踪对象和按上次打开后的更新日报`,
`${button("同步全部", "refresh-tracking")} ${button("标记已读", "mark-tracking-read")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: trackingHandoffAttrs })} ${button("跳到找对标", "goto-discovery", "primary")}`,
`
${renderMainAgentLandingNotice("tracking")}
<div class="hero-card">
<h3>日报逻辑</h3>
<p>按上次打开后汇总上次打开距今 ${escapeHtml(daysSince(platformCursor))} 本次优先展示有更新且值得借鉴的内容</p>
<div class="chip-row" style="margin-top:14px;">
${renderPlatformSwitchChips(currentPlatform)}
<span class="chip">上次已读 ${escapeHtml(cursorLabel)}</span>
</div>
</div>
<div class="layout-grid grid-main" style="margin-top:18px;">
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>跟踪列表</h3><div class="panel-subtitle"> Agent</div></div><span class="tag">${escapeHtml(formatNumber(trackedAccounts.length))} </span></div>
<div class="mobile-only compact-summary-row">
<span class="tag blue">跟踪 ${escapeHtml(formatNumber(trackedAccounts.length))}</span>
<span class="tag green">日报 ${escapeHtml(formatNumber(digestItems.length))}</span>
<span class="tag">${escapeHtml(daysSince(platformCursor))} 天窗口</span>
</div>
<div class="list">
${trackedAccounts.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.account?.nickname || "未命名账号")}</h4>
<p>最近作品 ${escapeHtml(formatNumber(item.account?.video_summary?.count))} 条 · 平均播放 ${escapeHtml(formatNumber(item.account?.video_summary?.avg_play))}</p>
<div class="task-meta">
<span class="tag green">已跟踪</span>
<span class="tag">${escapeHtml(item.assistant_name || "未绑 Agent")}</span>
${actionTag("立即同步", "refresh-tracked-account", `data-tracked-account-id="${escapeHtml(item.tracked_account_id)}"`)}
${actionTag("看详情", "select-account", `data-account-id="${escapeHtml(item.tracked_account_id)}"`)}
</div>
</div>
`).join("") || `<div class="task-item"><h4>暂无跟踪账号</h4><p>先去找对标把重点账号加入跟踪。</p></div>`}
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>更新日报</h3><div class="panel-subtitle"></div></div><span class="tag blue">${escapeHtml(formatNumber(digestItems.length))} </span></div>
<div class="list">
${digestItems.map((item) => `
<div class="review-card compact">
<h4>${escapeHtml(item.account?.nickname || "账号")} · ${escapeHtml(item.video?.title || item.video?.description || "最新作品")}</h4>
<p>${escapeHtml(item.summary || `发布时间 ${formatDateTime(item.video?.published_at)},建议继续判断借鉴点。`)}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(getPlatformShortLabel(item.platform || currentPlatform))}</span>
<span class="tag green">${escapeHtml(item.is_high_value ? "高价值" : "可学习")}</span>
${item.assistant_name ? `<span class="tag">${escapeHtml(item.assistant_name)}</span>` : ""}
${item.video?.share_url ? `<a class="tag" href="${escapeHtml(item.video.share_url)}" target="_blank" rel="noreferrer">打开作品</a>` : ""}
</div>
${safeArray(item.borrowing_points).length ? `
<div class="task-meta">
${safeArray(item.borrowing_points).slice(0, 3).map((point) => `<span class="tag blue">${escapeHtml(point)}</span>`).join("")}
</div>
` : ""}
</div>
`).join("") || `<div class="review-card"><h4>暂无日报</h4><p>先把账号加入跟踪,并等待新作品更新。</p></div>`}
</div>
</div>
</div>
</div>
`
);
}
function renderAutomationScreen() {
const jobs = safeArray(appState.dashboard?.recent_jobs);
const analysisJobs = jobs.filter((item) => item.line_type === "analysis").length;
const aiVideoJobs = jobs.filter((item) => item.line_type === "ai_video").length;
const realCutJobs = jobs.filter((item) => item.line_type === "real_cut").length;
const overview = getIntegrationOverview();
const tabs = [
{ value: "health", label: "依赖健康" },
{ value: "guards", label: "动作防呆" }
];
const activeTab = getActiveDetailTab("automationDetailTab", tabs);
const automationHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "automation",
sourceActionKey: "automation-main-agent-handoff",
intentKey: "ops_admin",
title: "继续检查自动流程",
goal: "继续检查自动流程",
summary: "让主 Agent 结合依赖健康和动作防呆状态,给出下一步处理建议。",
platform: getPreferredPlatform(),
planSteps: ["读取当前依赖健康", "检查动作防呆和拦截状态", "生成下一步处理建议"]
});
return screenShell(
"自动流程",
"自动同步、日报生成和失败补跑先统一看这里。",
`${button("刷新", "refresh-data")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: automationHandoffAttrs })} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`,
`
${renderMainAgentLandingNotice("automation")}
<div class="hero-card">
<h3>自动流程</h3>
<p>当前按真实任务量和依赖健康状态给出看板自动流程受阻时会直接在这里拦住动作</p>
<div class="mini-grid">
<div class="mini-card"><small>分析任务</small><strong>${escapeHtml(formatNumber(analysisJobs))}</strong></div>
<div class="mini-card"><small>AI 视频</small><strong>${escapeHtml(formatNumber(aiVideoJobs))}</strong></div>
<div class="mini-card"><small>实拍剪辑</small><strong>${escapeHtml(formatNumber(realCutJobs))}</strong></div>
<div class="mini-card"><small>内容源</small><strong>${escapeHtml(formatNumber(appState.contentSources.length))}</strong></div>
</div>
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>自动链路工作区</h3>
<div class="panel-subtitle">普通用户只看健康状态和动作是否可执行系统治理移到管理员配置台</div>
</div>
<span class="tag ${escapeHtml(overview.tone)}">${escapeHtml(overview.headline)}</span>
</div>
${renderDetailTabs("automationDetailTab", tabs)}
${activeTab === "health" ? renderIntegrationOverviewPanel({ showActions: false }) : `
<div class="panel pad automation-guard-panel" style="box-shadow:none;">
<div class="panel-head">
<div>
<h3>动作防呆</h3>
<div class="panel-subtitle">依赖不可用时,相关动作会在这里和生产页一起被拦住。</div>
</div>
<span class="tag ${escapeHtml(overview.tone)}">${escapeHtml(overview.headline)}</span>
</div>
<div class="task-meta integration-highlights">
<span class="tag ${getPipelineGuard("aiVideo").enabled ? "green" : "red"}">AI 视频 ${escapeHtml(getPipelineGuard("aiVideo").enabled ? "可执行" : "已拦截")}</span>
<span class="tag ${getPipelineGuard("realCut").enabled ? "green" : "red"}">实拍剪辑 ${escapeHtml(getPipelineGuard("realCut").enabled ? "可执行" : "已拦截")}</span>
<span class="tag blue">ASR ${escapeHtml(getIntegrationStatus(getIntegrationDetail("asr")).summary)}</span>
</div>
<div class="integration-actions" style="margin-top:14px;">
${renderPipelineButton("aiVideo", "primary")}
${renderPipelineButton("realCut")}
</div>
<div class="integration-note" style="margin-top:12px;">${escapeHtml(overview.subtitle)}</div>
</div>
`}
</div>
`
);
}
function renderOwnedScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("我的账号", "工作区就绪后,这里会自动显示当前账号和建议动作。");
}
return screenShell("我的账号", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("我的账号未加载", "自动连接成功后,这里会展示当前账号和建议动作。"));
}
const me = appState.me || appState.session?.account || {};
const firstAssistant = safeArray(appState.dashboard.assistants)[0];
const selectedProject = getSelectedProject();
const jobs = safeArray(appState.dashboard.recent_jobs);
const completedJobs = jobs.filter((item) => item.status === "completed").length;
const activeJobs = jobs.filter((item) => item.status !== "completed").length;
return screenShell(
"我的账号",
"这里先用当前登录账号和最近产出组合成第一版总览。",
`${button("刷新", "refresh-data")} ${button("去 Agent", "goto-playbook", "primary")}`,
`
<div class="hero-card">
<div class="entity-cell">
<div class="avatar-lg">${escapeHtml(initials(me.display_name || me.username))}</div>
<div>
<h3>${escapeHtml(me.display_name || me.username || "当前账号")}</h3>
<p>${escapeHtml(firstAssistant?.generation_goal || "先创建 Agent再把平台目标和变现方式补齐。")}</p>
</div>
</div>
<div class="mini-grid">
<div class="mini-card"><small>项目</small><strong>${escapeHtml(formatNumber(appState.dashboard.projects?.length))}</strong></div>
<div class="mini-card"><small>Agent</small><strong>${escapeHtml(formatNumber(appState.dashboard.assistants?.length))}</strong></div>
<div class="mini-card"><small>任务</small><strong>${escapeHtml(formatNumber(appState.dashboard.recent_jobs?.length))}</strong></div>
<div class="mini-card"><small>素材</small><strong>${escapeHtml(formatNumber(appState.documents.length))}</strong></div>
</div>
</div>
<div class="layout-grid grid-main" style="margin-top:18px;">
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>当前负责范围</h3><div class="panel-subtitle"></div></div></div>
<div class="list">
<div class="task-item">
<h4>当前项目 · ${escapeHtml(selectedProject?.name || "未选项目")}</h4>
<p>${escapeHtml(selectedProject?.description || "当前还没有清晰项目说明,可以去“我的项目”补齐。")}</p>
<div class="task-meta">
<span class="tag blue">项目 ${escapeHtml(formatNumber(appState.dashboard.projects?.length))}</span>
<span class="tag">${escapeHtml(getSelectedAssistant()?.name || "未选 Agent")}</span>
${actionTag("去我的项目", "goto-intake")}
</div>
</div>
<div class="task-item">
<h4>当前主 Agent</h4>
<p>${escapeHtml(firstAssistant?.generation_goal || firstAssistant?.description || "先在 Agent 页面补齐你的默认协作方式。")}</p>
<div class="task-meta">
<span class="tag green">${escapeHtml(firstAssistant?.name || "默认文案助手")}</span>
${actionTag("去 Agent", "goto-playbook")}
</div>
</div>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>最近工作摘要</h3><div class="panel-subtitle"></div></div></div>
<div class="mini-grid" style="margin-top:0;">
<div class="mini-card"><small>待推进任务</small><strong>${escapeHtml(formatNumber(activeJobs))}</strong></div>
<div class="mini-card"><small>已完成任务</small><strong>${escapeHtml(formatNumber(completedJobs))}</strong></div>
<div class="mini-card"><small>平台数</small><strong>${escapeHtml(formatNumber(getPlatformOptions().length))}</strong></div>
<div class="mini-card"><small>对标数</small><strong>${escapeHtml(formatNumber(appState.accounts.length))}</strong></div>
</div>
<div class="task-item" style="margin-top:14px;">
<h4>推荐下一步</h4>
<p>${escapeHtml(activeJobs ? "先去生产中心处理待推进任务,再回到首页看下一条动作。" : "当前任务不重,适合补 Agent 策略或整理项目说明。")}</p>
<div class="task-meta">
${actionTag(activeJobs ? "去生产中心" : "去 Agent", activeJobs ? "goto-production" : "goto-playbook")}
${actionTag("看跟踪账号", "goto-tracking")}
</div>
</div>
</div>
</div>
</div>
`
);
}
function renderPlaybookScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("Agent", "工作区就绪后,这里会自动加载真实 Agent 列表和模型。");
}
return screenShell("Agent", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("Agent 未加载", "自动连接成功后,这里会展示真实 Agent 列表和模型。"));
}
const assistants = safeArray(appState.dashboard.assistants);
const models = safeArray(appState.dashboard.model_profiles);
const currentModel = getCurrentModelProfile();
const currentAssistant = getSelectedAssistant();
const localCatalog = appState.localModelCatalog || {};
const activeAdminOverrideNotice = appState.onelinerGovernanceEffective?.active_admin_override_notice || null;
const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean);
const playbookHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "playbook",
sourceActionKey: "playbook-main-agent-handoff",
intentKey: "custom",
title: "继续梳理当前 Agent 工作区",
goal: "继续梳理当前 Agent 工作区",
summary: "让主 Agent 结合当前 Agent、模型和策略状态给出下一步执行建议。",
platform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform(),
planSteps: ["读取当前 Agent 与模型配置", "检查当前策略与平台 Agent 缺口", "生成下一步执行建议"]
});
const tabs = [
{ value: "workspace", label: "当前 Agent 工作台" },
{ value: "platform_agents", label: "平台 Agent" },
{ value: "models", label: "模型与学习" }
];
const activeTab = getActiveDetailTab("playbookDetailTab", tabs);
return screenShell(
"Agent",
"这里接真实 Agent 列表,当前已经支持切换和编辑 Agent。",
`${button("配置 OneLiner", "open-oneliner-profile")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: playbookHandoffAttrs })} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("去生产", "goto-production", "primary")}`,
`
${renderMainAgentLandingNotice("playbook")}
<div class="hero-card">
<h3>Agent 概览</h3>
<p>先定项目平台和主模型再导入内容让 Agent 学习</p>
<div class="chip-row" style="margin-top:14px;">
${models.slice(0, 6).map((model) => `<span class="chip ${model.is_default ? "active" : ""}">${escapeHtml(model.name)}</span>`).join("") || `<span class="chip active">暂无模型</span>`}
</div>
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>Agent 工作区</h3>
<div class="panel-subtitle">先处理你当前真的会用到的 Agent 信息系统治理内容已移到管理员配置台</div>
</div>
</div>
${renderDetailTabs("playbookDetailTab", tabs)}
${activeTab === "workspace" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head">
<div>
<h3>OneLiner 主 Agent</h3>
<div class="panel-subtitle">前端还没上的功能由它兜底承接,并调度平台 Agent。</div>
</div>
<div class="task-meta">
<span class="tag blue">${escapeHtml(appState.onelinerProfile?.display_name || "OneLiner")}</span>
<span class="tag">${escapeHtml(appState.onelinerProfile?.default_platform ? platformLabel(appState.onelinerProfile.default_platform) : "未设默认平台")}</span>
${actionTag("交给主 Agent", "handoff-to-main-agent", playbookHandoffAttrs)}
</div>
</div>
<div class="task-item compact">
<h4>${escapeHtml(appState.onelinerProfile?.long_term_goal || "还没有设置长期目标")}</h4>
<p>${escapeHtml(appState.onelinerProfile?.notes || "你可以把用户长期目标、账号目标、默认平台都绑给 OneLiner再让它去调度平台 Agent。")}</p>
<div class="task-meta">
<span class="tag">会话 ${escapeHtml(formatNumber(safeArray(appState.onelinerSessions).length))}</span>
<span class="tag">平台 Agent ${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))}</span>
<span class="tag clickable-tag" data-action="open-oneliner-profile">编辑配置</span>
</div>
</div>
${activeAdminOverrideNotice?.title ? `
<div class="task-item compact" style="margin-top:12px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>管理员覆盖生效中</h4>
<p>${escapeHtml(activeAdminOverrideNotice.summary || "当前 OneLiner 和平台 Agent 都会先遵循管理员覆盖层。")}</p>
<div class="task-meta">
<span class="tag orange">${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}</span>
<span class="tag clickable-tag" data-action="open-user-global-policy">看我的策略</span>
</div>
</div>
` : ""}
${renderGovernanceSummaryCard({
title: "我的策略与历史",
subtitle: appState.userGlobalPolicy?.current_version?.summary || "你和主 Agent 的策略对话,会先沉淀成用户全局策略,再按需要下放到单平台。",
effective: appState.onelinerGovernanceEffective,
actions: [
{
action: "open-user-global-policy",
label: `编辑全局策略 · 历史 ${formatNumber(appState.userGlobalPolicy?.versions?.count || 0)}`
},
{
action: "open-user-global-policy-history",
label: "查看全局历史"
},
{
action: "open-user-platform-policy",
label: `编辑当前平台策略 · 历史 ${formatNumber(appState.userCurrentPlatformPolicy?.versions?.count || 0)}`,
platform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform()
},
{
action: "open-user-platform-policy-history",
label: "查看当前平台历史",
platform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform()
}
]
})}
</div>
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
<div class="panel-head">
<div>
<h3>当前 Agent</h3>
<div class="panel-subtitle">后续文案生成、对标绑定和复盘默认都会优先使用这里选中的 Agent。</div>
</div>
<div class="task-meta">
${currentAssistant ? `<span class="tag blue">已选</span>` : `<span class="tag red">未选</span>`}
${currentAssistant ? `<span class="tag clickable-tag" data-action="open-edit-assistant" data-assistant-id="${escapeHtml(currentAssistant.id)}">编辑</span>` : ""}
</div>
</div>
${currentAssistant ? `
<div class="task-item compact">
<h4>${escapeHtml(currentAssistant.name)}</h4>
<p>${escapeHtml(currentAssistant.generation_goal || currentAssistant.description || "先补齐这个 Agent 的目标和说明。")}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(models.find((item) => item.id === currentAssistant.model_profile_id)?.name || "默认模型")}</span>
<span class="tag blue">${escapeHtml(formatNumber(safeArray(currentAssistant.knowledge_base_ids).length))} 条知识库</span>
<span class="tag">${escapeHtml(brief(currentAssistant.description || "暂无说明", 22))}</span>
</div>
</div>
` : `<div class="task-item"><h4>还没有可用 Agent</h4><p>先创建一个 Agent再把当前项目的内容都交给它学习。</p></div>`}
</div>
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
<div class="panel-head"><div><h3>Agent 列表</h3><div class="panel-subtitle">当前接的是后端 assistants</div></div></div>
<div class="list">
${assistants.map((assistant) => `
<div class="task-item ${assistant.id === currentAssistant?.id ? "active" : ""}">
<h4>${escapeHtml(assistant.name)}</h4>
<p>${escapeHtml(assistant.description || assistant.generation_goal || "暂无说明")}</p>
<div class="task-meta">
<span class="tag blue">知识库 ${escapeHtml(formatNumber(safeArray(assistant.knowledge_base_ids).length))}</span>
<span class="tag">${escapeHtml(models.find((item) => item.id === assistant.model_profile_id)?.name || "默认模型")}</span>
<span class="tag clickable-tag" data-action="select-assistant" data-assistant-id="${escapeHtml(assistant.id)}">${assistant.id === currentAssistant?.id ? "当前 Agent" : "设为当前"}</span>
<span class="tag clickable-tag" data-action="open-edit-assistant" data-assistant-id="${escapeHtml(assistant.id)}">编辑</span>
</div>
</div>
`).join("") || `<div class="task-item"><h4>还没有 Agent</h4><p>下一步可以直接把创建动作接进来。</p></div>`}
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近生成</h3><div class="panel-subtitle">当前先承接文案生成结果</div></div></div>
${appState.lastGeneratedCopy ? `
<div class="task-item">
<h4>${escapeHtml(appState.lastGeneratedCopy.assistantName)}</h4>
<p>${escapeHtml(appState.lastGeneratedCopy.content)}</p>
<div class="task-meta">
<span class="tag blue">需求:${escapeHtml(brief(appState.lastGeneratedCopy.prompt, 24))}</span>
<span class="tag">${escapeHtml(formatNumber(appState.lastGeneratedCopy.usedDocuments.length))} 条参考</span>
</div>
</div>
` : `<div class="task-item"><h4>还没有生成结果</h4><p>先点“生成文案”,这里会保留最近一次结果。</p></div>`}
</div>
</div>
</div>
` : activeTab === "platform_agents" ? `
<div style="margin-top:6px;">${renderPlatformAgentPanel()}</div>
` : `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head">
<div>
<h3>本机模型网关</h3>
<div class="panel-subtitle">当前默认分析会优先走本机 cli-proxy-api。</div>
</div>
<div class="task-meta">
<span class="tag ${escapeHtml(localCatalog.reachable ? "green" : "red")}">${escapeHtml(localCatalog.reachable ? "在线" : "离线")}</span>
${localCatalog.management_url ? `<a class="tag blue" href="${escapeHtml(localCatalog.management_url)}" target="_blank" rel="noreferrer">打开管理页</a>` : ""}
</div>
</div>
<div class="task-item compact">
<h4>${escapeHtml(currentModel?.name || localCatalog.default_model || "GLM-5")}</h4>
<p>${escapeHtml(currentModel ? `${currentModel.model_name || "-"} · ${currentModel.base_url || "-"}` : (localCatalog.public_base_url || localCatalog.base_url || "尚未读取到网关地址"))}</p>
<div class="task-meta">
${gatewayModels.slice(0, 6).map((model) => `<span class="tag">${escapeHtml(model)}</span>`).join("") || `<span class="tag red">暂无可见模型</span>`}
</div>
</div>
</div>
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
<div class="panel-head"><div><h3>模型列表</h3><div class="panel-subtitle">来自真实 model_profiles</div></div></div>
<div class="playbook-list">
${models.map((model) => `
<div class="playbook-item ${model.is_default ? "active" : ""}">
<h4>${escapeHtml(model.name)}</h4>
<p>${escapeHtml(model.model_name || "-")} · ${escapeHtml(model.base_url || "-")}</p>
</div>
`).join("") || `<div class="playbook-item active"><h4>暂无模型</h4><p>先在“我的”里配置模型。</p></div>`}
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近学习素材</h3><div class="panel-subtitle">从知识库文档取最近内容</div></div></div>
<div class="list">
${appState.documents.slice(0, 4).map((doc) => `
<div class="task-item">
<h4>${escapeHtml(doc.title)}</h4>
<p>${escapeHtml(brief(doc.style_summary || doc.transcript_text || doc.combined_text, 72))}</p>
</div>
`).join("") || `<div class="task-item"><h4>还没有学习素材</h4><p>先去找对标导入一条主页或作品。</p></div>`}
</div>
</div>
</div>
</div>
`}
</div>
`
);
}
function renderProductionScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("生产中心", "工作区就绪后,这里会自动加载真实任务和作品。");
}
return screenShell("生产中心", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("生产中心未加载", "自动连接成功后,这里会展示真实任务和作品。"));
}
const jobs = safeArray(appState.dashboard.recent_jobs);
const activeJobs = jobs.filter((item) => item.status !== "completed").slice(0, 4);
const failedJobs = getRecoverableFailedJobs();
const recoverableCount = failedJobs.filter((item) => item.recovery.recoverable).length;
const recentDocs = appState.documents.slice(0, 3);
const works = getProductionWorks(6);
const productionHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "production",
sourceActionKey: "production-handoff",
intentKey: "production_coordination",
title: "继续推进生产中心",
goal: "基于当前生产队列、失败任务和产物,生成下一步生产推进计划",
summary: "主 Agent 会结合生产队列、失败恢复和产物状态,给出下一步动作。",
platform: getPreferredPlatform(),
platformScope: "single_platform",
planSteps: [
"读取当前生产队列和失败任务",
"识别最该优先推进或恢复的项",
"生成一版生产推进计划"
]
});
const tabs = [
{ value: "queue", label: "生产队列" },
{ value: "recovery", label: "失败恢复" },
{ value: "recorder", label: "录制维护" },
{ value: "outputs", label: "作品与产物" }
];
const activeTab = getActiveDetailTab("productionDetailTab", tabs);
return screenShell(
"生产中心",
"这里已经接上真实任务和知识库文档,后续再继续补任务创建动作。",
`${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: productionHandoffAttrs })} ${button("去复盘", "goto-review", "primary")} ${button("批量恢复", "batch-recover-jobs", "secondary", { disabledReason: recoverableCount ? "" : "当前没有可恢复的失败任务" })}`,
`
${renderMainAgentLandingNotice("production")}
<div class="panel pad">
<div class="panel-head"><div><h3>生产队列</h3><div class="panel-subtitle"></div></div></div>
<div class="layout-grid grid-4" style="margin-top:16px;">
<div class="queue-card"><h4>分析任务</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))} </p></div>
<div class="queue-card"><h4>实拍剪辑</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))} </p></div>
<div class="queue-card"><h4>AI 视频</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))} </p></div>
<div class="queue-card"><h4>内容源同步</h4><p> ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "content_source_sync").length))} </p></div>
</div>
<div class="mobile-only compact-summary-row" style="margin-top:14px;">
<span class="tag blue">分析 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "analysis").length))}</span>
<span class="tag">实拍 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "real_cut").length))}</span>
<span class="tag green">AI 视频 ${escapeHtml(formatNumber(jobs.filter((item) => item.line_type === "ai_video").length))}</span>
</div>
</div>
${renderQuotaBlockingNotice()}
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>生产工作区</h3>
<div class="panel-subtitle">把队列恢复录制和产物拆开看减少一次性信息量</div>
</div>
</div>
${renderDetailTabs("productionDetailTab", tabs)}
${activeTab === "queue" ? `
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>当前任务</h3><div class="panel-subtitle">来自 recent_jobs</div></div></div>
<div class="list">
${(activeJobs.length ? activeJobs : jobs.slice(0, 4)).map((job) => `
<div class="task-item compact">
<h4>${escapeHtml(job.title)}</h4>
<p>${escapeHtml(brief(job.style_summary || job.transcript_text || job.error || "暂无摘要", 80))}</p>
<div class="task-meta">
<span class="tag ${statusTone(job.status)}">${escapeHtml(job.status)}</span>
<span class="tag">${escapeHtml(job.line_type || "analysis")}</span>
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""}
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""}
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
</div>
</div>
`).join("") || `<div class="task-item"><h4>还没有任务</h4><p>先去找对标导入内容。</p></div>`}
</div>
</div>
` : activeTab === "recovery" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head">
<div>
<h3>失败任务恢复</h3>
<div class="panel-subtitle">把最近失败任务按恢复可行性分组,批量恢复入口在这里。</div>
</div>
<div class="task-meta">
<span class="tag red">${escapeHtml(formatNumber(failedJobs.filter((item) => item.recovery.recoverable).length))} 可恢复</span>
<span class="tag orange">${escapeHtml(formatNumber(failedJobs.filter((item) => !item.recovery.recoverable).length))} 需人工</span>
<span class="tag clickable-tag" data-action="batch-recover-jobs">批量恢复</span>
</div>
</div>
<div class="list">
${failedJobs.map(({ job, recovery }) => `
<div class="task-item compact">
<h4>${escapeHtml(job.title || job.id)}</h4>
<p>${escapeHtml(brief(job.error || recovery.reason || "任务失败,请查看恢复说明。", 120))}</p>
<div class="task-meta">
<span class="tag ${recovery.recoverable ? "green" : recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.label)}</span>
<span class="tag">${escapeHtml(job.line_type || job.source_type || "analysis")}</span>
${job.updated_at ? `<span class="tag">${escapeHtml(formatDateTime(job.updated_at))}</span>` : ""}
${recovery.recoverable ? `<span class="tag clickable-tag" data-action="recover-job" data-job-id="${escapeHtml(job.id)}">${escapeHtml(recovery.actionLabel)}</span>` : `<span class="tag ${recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.reason)}</span>`}
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span>
</div>
</div>
`).join("") || `<div class="task-item"><h4>当前没有失败任务</h4><p></p></div>`}
</div>
</div>
</div>
<div class="side-stack">
${renderRecoveryHistoryPanel()}
</div>
</div>
` : activeTab === "recorder" ? `
<div style="margin-top:6px;">
${renderLiveRecorderManagementPanel()}
</div>
` : `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>作品与成片</h3><div class="panel-subtitle"></div></div></div>
<div class="list">
${works.map((video) => `
<div class="review-card compact">
<h4>${escapeHtml(describeVideo(video))}</h4>
<p>${escapeHtml(`发布时间 ${formatDateTime(video.published_at)} · 播放 ${formatNumber(video.stats?.play)} · 点赞 ${formatNumber(video.stats?.like)}`)}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(video.content_type || "video")}</span>
<span class="tag green">得分 ${escapeHtml(formatNumber(video.score?.performance_score || 0))}</span>
${getVideoLink(video) ? `<a class="tag" href="${escapeHtml(getVideoLink(video))}" target="_blank" rel="noreferrer">打开原作品</a>` : ""}
</div>
</div>
`).join("")}
${recentDocs.map((doc) => `
<div class="review-card compact">
<h4>${escapeHtml(doc.title)}</h4>
<p>${escapeHtml(brief(doc.style_summary || doc.combined_text || doc.transcript_text, 92))}</p>
<div class="task-meta"><span class="tag">${escapeHtml(doc.source_type || "document")}</span><span class="tag blue">学习素材</span></div>
</div>
`).join("") || (works.length ? "" : `<div class="review-card"><h4>还没有作品</h4><p>先导入内容或跑一次分析任务。</p></div>`)}
</div>
</div>
</div>
<div class="side-stack">
${renderLastJobDetailCard()}
</div>
</div>
`}
</div>
`
);
}
function renderReviewScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("发布与复盘", "工作区就绪后,这里会自动生成复盘入口和最近完成任务。");
}
return screenShell("发布与复盘", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("复盘未加载", "自动连接成功后,这里会先用最近任务生成一版复盘入口。"));
}
if (!backendSupports("/v2/reviews")) {
return screenShell(
"发布与复盘",
"当前 live collector 还没有接入复盘读写接口。",
`${button("去生产", "goto-production", "primary")}`,
renderEmptyState("复盘能力暂未接入", "这套后端还缺 /v2/reviews当前可以继续跑生产任务等 live collector 同步后这里会自动切成真实复盘工作台。")
);
}
const project = getSelectedProject();
const completed = safeArray(appState.dashboard.recent_jobs).filter((item) => item.status === "completed").slice(0, 4);
const reviews = getProjectReviews(project?.id || "").slice(0, 8);
const reviewHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "review",
sourceActionKey: "review-handoff",
intentKey: "review_followup",
title: project ? `继续沉淀项目复盘 · ${project.name}` : "继续沉淀复盘",
goal: "基于最近完成任务和已保存复盘,生成下一步复盘与发布计划",
summary: "主 Agent 会结合最近完成任务和现有复盘,整理下一步复盘动作。",
platform: getPreferredPlatform(),
platformScope: "single_platform",
planSteps: [
"读取最近完成任务和现有复盘",
"识别还缺的复盘或发布动作",
"生成下一步复盘推进计划"
]
});
return screenShell(
"发布与复盘",
"先看已保存复盘,再把完成任务转成结构化复盘。",
`${button("写复盘", "open-create-review")} ${button("刷新", "refresh-data")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: reviewHandoffAttrs })} ${button("去生产", "goto-production", "primary")}`,
`
${renderMainAgentLandingNotice("review")}
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>已保存复盘</h3><div class="panel-subtitle">当前项目的真实复盘记录</div></div><span class="tag blue">${escapeHtml(formatNumber(reviews.length))} 条</span></div>
<div class="list">
${reviews.map((review) => `
<div class="review-card compact">
<h4>${escapeHtml(review.title)}</h4>
<p>${escapeHtml(brief(review.highlights || review.next_actions || review.notes || "已保存复盘,待继续补充表现数据。", 92))}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(platformLabel(review.platform || "douyin"))}</span>
<span class="tag ${statusTone(review.verdict || "blue")}">${escapeHtml(review.verdict || "已记录")}</span>
${review.publish_url ? `<a class="tag" href="${escapeHtml(review.publish_url)}" target="_blank" rel="noreferrer">打开链接</a>` : ""}
<span class="tag clickable-tag" data-action="open-review-edit" data-review-id="${escapeHtml(review.id)}">编辑</span>
</div>
</div>
`).join("") || `<div class="review-card"><h4>还没有复盘</h4><p>可以把最近完成任务直接写成一条复盘。</p></div>`}
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>最近完成</h3><div class="panel-subtitle">从完成任务继续写复盘或进入下一步生产</div></div></div>
<div class="list">
${completed.map((job) => `
<div class="review-card compact">
<h4>${escapeHtml(job.title)}</h4>
<p>${escapeHtml(brief(job.style_summary || job.transcript_text || "已完成,待补复盘。", 84))}</p>
<div class="task-meta">
<span class="tag green">已完成</span>
<span class="tag">${escapeHtml(job.line_type || "analysis")}</span>
${actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(job.id)}"`)}
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""}
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""}
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
</div>
</div>
`).join("") || `<div class="review-card"><h4>还没有完成任务</h4><p>先去生产中心跑一条链路。</p></div>`}
</div>
</div>
</div>
</div>
${renderLastJobDetailCard()}
`
);
}
function renderStrategyScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("我的策略", "工作区就绪后,这里会自动展示你自己的策略层和治理记录。");
}
return screenShell("我的策略", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("策略未加载", "自动连接成功后,这里会展示你的全局策略、平台策略和治理记录。"));
}
const tabs = [
{ value: "effective", label: "当前生效" },
{ value: "global", label: "全局策略" },
{ value: "platform", label: "当前平台策略" },
{ value: "activity", label: "变更记录" }
];
const activeTab = getActiveDetailTab("strategyDetailTab", tabs);
const project = getSelectedProject();
const platform = appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform();
const activeAdminOverrideNotice = appState.onelinerGovernanceEffective?.active_admin_override_notice || null;
const strategyHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "strategy",
sourceActionKey: "strategy-main-agent-handoff",
intentKey: "custom",
title: "继续调整我的策略",
goal: "继续调整我的策略",
summary: "让主 Agent 结合当前生效层、个人策略和管理员覆盖,给出下一步治理建议。",
platform,
planSteps: ["读取当前生效策略", "检查用户层与管理员覆盖差异", "生成下一步治理建议"]
});
return screenShell(
"我的策略",
"把你和主 Agent 的对话沉淀成可查看、可回滚、可追溯的个人策略层。",
`${button("编辑全局策略", "open-user-global-policy")} ${button("编辑当前平台策略", "open-user-platform-policy", "primary")} ${button("交给主 Agent", "handoff-to-main-agent", "secondary", { attrs: strategyHandoffAttrs })}`,
`
${renderMainAgentLandingNotice("strategy")}
<div class="hero-card">
<h3>当前策略工作区</h3>
<p>${escapeHtml(project?.name || "当前项目")} · ${escapeHtml(platformLabel(platform))}。这里展示系统默认、你的个性化策略和管理员覆盖是如何叠加生效的。</p>
${activeAdminOverrideNotice?.title ? `
<div class="task-item compact" style="margin-top:14px; border-color:rgba(245, 158, 11, 0.28); background:linear-gradient(180deg, rgba(255, 250, 240, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);">
<h4>管理员覆盖生效中</h4>
<p>${escapeHtml(activeAdminOverrideNotice.summary || "当前项目下的部分策略被管理员覆盖层托底。")}</p>
<div class="task-meta">
<span class="tag orange">${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}</span>
${activeAdminOverrideNotice.platform_label ? `<span class="tag">${escapeHtml(activeAdminOverrideNotice.platform_label)}</span>` : ""}
</div>
</div>
` : ""}
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>策略治理</h3>
<div class="panel-subtitle">先看当前生效,再回看你自己的历史和管理员覆盖,不必再通过多个弹窗来回切。</div>
</div>
</div>
${renderDetailTabs("strategyDetailTab", tabs)}
${activeTab === "effective" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
${renderGovernanceSummaryCard({
title: "当前生效策略",
subtitle: appState.onelinerGovernanceEffective?.summary || "当前主 Agent 会按下面这些层级叠加执行。",
effective: appState.onelinerGovernanceEffective,
actions: [
{ action: "open-user-global-policy", label: "编辑我的全局策略" },
{ action: "open-user-platform-policy", label: "编辑当前平台策略", platform },
{
action: "handoff-to-main-agent",
label: "交给主 Agent 调整",
platform,
sourceScreen: "strategy",
sourceActionKey: "governance-summary-handoff",
intentKey: "custom",
title: "调整当前策略",
goal: "调整当前策略",
summary: "先由主 Agent 读取当前治理层,再给一版确认卡。",
planSteps: ["读取当前生效策略", "结合管理员覆盖与个人策略生成方案", "等待用户确认后执行"]
}
]
})}
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>当前叠加层</h3><div class="panel-subtitle">系统默认、用户层和管理员覆盖会按优先级叠加。</div></div></div>
<div class="list">
${safeArray(appState.onelinerGovernanceEffective?.layers).map((layer) => `
<div class="task-item compact">
<h4>${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || platform))}</h4>
<p>${escapeHtml(layer.current_version?.summary || layer.scope?.summary || "当前层还没有补充摘要。")}</p>
<div class="task-meta">
${layer.current_version?.version_no ? `<span class="tag blue">版本 ${escapeHtml(formatNumber(layer.current_version.version_no || 0))}</span>` : ""}
${layer.scope?.platform ? `<span class="tag">${escapeHtml(platformLabel(layer.scope.platform))}</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有策略层</h4><p>当前会话还没有拉到治理层信息。</p></div>`}
</div>
</div>
</div>
</div>
` : activeTab === "global" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>我的全局策略</h3><div class="panel-subtitle">这层只影响你自己,会先于平台策略被主 Agent 读取。</div></div></div>
${renderPolicyVersionSummary(appState.userGlobalPolicy || {}, "你还没有发布自己的全局策略。")}
<div class="task-meta" style="margin-top:12px;">
<span class="tag clickable-tag" data-action="open-user-global-policy">编辑</span>
<span class="tag clickable-tag" data-action="open-user-global-policy-history">历史与回滚</span>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近全局版本</h3><div class="panel-subtitle">你的全局策略回滚不会覆盖旧记录,而是生成一个新的版本。</div></div></div>
<div class="list">${renderPolicyVersionsHtml(appState.userGlobalPolicy?.versions?.items || appState.userGlobalPolicy?.versions || [], "你的全局策略还没有历史版本。")}</div>
</div>
</div>
</div>
` : activeTab === "platform" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>${escapeHtml(platformLabel(platform))} 当前平台策略</h3><div class="panel-subtitle">只影响当前平台,不会连带改动其他平台。</div></div></div>
${renderPolicyVersionSummary(appState.userCurrentPlatformPolicy || {}, `你还没有发布 ${platformLabel(platform)} 平台策略。`)}
<div class="task-meta" style="margin-top:12px;">
<span class="tag clickable-tag" data-action="open-user-platform-policy" data-platform="${escapeHtml(platform)}">编辑</span>
<span class="tag clickable-tag" data-action="open-user-platform-policy-history" data-platform="${escapeHtml(platform)}">历史与回滚</span>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近平台版本</h3><div class="panel-subtitle"></div></div></div>
<div class="list">${renderPolicyVersionsHtml(appState.userCurrentPlatformPolicy?.versions?.items || appState.userCurrentPlatformPolicy?.versions || [], `${platformLabel(platform)} 还没有历史版本。`)}</div>
</div>
</div>
</div>
` : `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近策略变更</h3><div class="panel-subtitle"></div></div></div>
<div class="list">${renderPolicyAuditFeed(appState.userPolicyAudits, "当前项目还没有治理记录。")}</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>治理提醒</h3><div class="panel-subtitle"></div></div></div>
<div class="list">
<div class="task-item compact">
<h4>用户层不会影响其他人</h4>
<p>你发布或回滚自己的策略只会影响你当前账户下的工作方式</p>
</div>
<div class="task-item compact">
<h4>管理员覆盖会明确可见</h4>
<p>如果管理员对你当前项目施加了覆盖层这里会出现对应的记录与摘要</p>
</div>
</div>
</div>
</div>
</div>
`}
</div>
`
);
}
function renderCreditsScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("额度", "工作区就绪后,这里会自动展示真实额度和运营看板。");
}
return screenShell("额度", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("额度未加载", "自动连接成功后,这里会展示真实额度和运营看板。"));
}
const jobs = safeArray(appState.dashboard.recent_jobs);
const quota = appState.tenantQuota;
const usage = appState.tenantUsage || quota?.usage || {};
const categories = usage?.categories || {};
const estimatedVideoUsage = (categories.ai_video?.quantity || 0) + (categories.real_cut?.quantity || 0);
return screenShell(
"额度",
"在接真实计费前,先按任务量给出运营看板。",
`${button("刷新", "refresh-data")}`,
`
<div class="layout-grid grid-3">
<div class="stat-card"><small>文案消耗预估</small><strong>${escapeHtml(formatNumber(categories.copy?.quantity || jobs.filter((item) => item.line_type === "analysis").length))}</strong><div class="stat-foot"><span>分析 / 生成链路</span><span class="positive">按任务量估算</span></div></div>
<div class="stat-card"><small>本周期预算</small><strong>${escapeHtml(formatNumber((quota?.monthly_budget_cents || usage?.total_cost_cents || 0) / 100))}</strong><div class="stat-foot"><span>元</span><span class="warn">已用 ${escapeHtml(formatNumber((usage?.total_cost_cents || 0) / 100))} 元</span></div></div>
<div class="stat-card"><small>视频消耗预估</small><strong>${escapeHtml(formatNumber(estimatedVideoUsage || jobs.filter((item) => item.line_type === "ai_video" || item.line_type === "real_cut").length))}</strong><div class="stat-foot"><span>AI 视频 / 实拍剪辑</span><span class="positive">可做套餐</span></div></div>
</div>
<div class="layout-grid grid-main" style="margin-top:18px;">
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>当前额度策略</h3><div class="panel-subtitle">先让用户能看懂“还剩多少、风险在哪、下一步怎么做”。</div></div></div>
<div class="list">
<div class="task-item">
<h4>预算与已用</h4>
<p>${escapeHtml(quota ? `当前预算 ${formatNumber((quota.monthly_budget_cents || 0) / 100)} 元,已用 ${formatNumber((usage?.total_cost_cents || 0) / 100)} 元。` : "后端尚未完全接入真实预算,当前先按任务量做用户可理解的额度看板。")}</p>
</div>
<div class="task-item">
<h4>动作额度</h4>
<p>${escapeHtml(quota ? `文案 ${formatNumber(quota.copy_quota || 0)} / AI 视频 ${formatNumber(quota.ai_video_quota || 0)} / 实拍剪辑 ${formatNumber(quota.real_cut_quota || 0)}` : "当前优先展示文案、封面、视频三类额度池,后续再接真实套餐。")}</p>
</div>
<div class="task-item">
<h4>使用建议</h4>
<p>${escapeHtml((quota?.enabled === false) ? "当前额度保护已关闭,适合内部联调,不适合正式对外。":"当前额度保护已开启,适合逐步转向对外产品表达。")}</p>
</div>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad">
<div class="panel-head"><div><h3>用户能理解的表达</h3><div class="panel-subtitle">不要只给数字,要给解释和风险提示。</div></div></div>
<div class="list">
<div class="review-card">
<h4>文案额度</h4>
<p>适合高频迭代,建议和项目阶段绑定,而不是裸数字展示。</p>
</div>
<div class="review-card">
<h4>视频额度</h4>
<p>成本更高,适合明确写出已用、剩余和推荐使用场景。</p>
</div>
<div class="review-card">
<h4>风险提示</h4>
<p>${escapeHtml(quota?.storage_over_limit ? "当前存储已超限,后续应优先处理清理或扩容。" : "当前没有明显超限风险,但仍建议补齐真实计费链路。")}</p>
</div>
</div>
</div>
</div>
</div>
`
);
}
function renderSettingsScreen() {
const session = appState.session;
const project = getSelectedProject();
const tabs = [
{ value: "workspace", label: "连接与工作区" },
{ value: "display", label: "界面与帮助" }
];
const activeTab = getActiveDetailTab("settingsDetailTab", tabs);
return screenShell(
"设置",
"这里不放系统治理内容,只处理当前用户需要理解的连接、界面和帮助信息。",
`${button("连接状态", "open-auth")} ${isSuperAdmin() ? button("管理员配置台", "goto-admin-workbench", "primary") : button("刷新", "refresh-data", "primary")}`,
`
<div class="hero-card">
<h3>设置与帮助</h3>
<p>把连接状态、当前工作区和使用说明放在一起,避免和管理员控制面混在同一页。</p>
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head"><div><h3>当前设置</h3><div class="panel-subtitle">先看你现在接到了哪里,再决定是否要调整。</div></div></div>
${renderDetailTabs("settingsDetailTab", tabs)}
${activeTab === "workspace" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>当前连接</h3><div class="panel-subtitle">当前前端与后端的真实连接状态。</div></div></div>
<div class="list">
<div class="task-item">
<h4>${escapeHtml(session?.account?.display_name || session?.account?.username || "未连接")}</h4>
<p>${escapeHtml(session?.backendUrl || DEFAULT_BACKEND_URL)}</p>
<div class="task-meta">
<span class="tag ${session ? "green" : "red"}">${escapeHtml(session ? "已自动连接" : "等待连接")}</span>
${project ? `<span class="tag blue">${escapeHtml(project.name)}</span>` : ""}
</div>
</div>
<div class="task-item">
<h4>自动连接说明</h4>
<p>当前站点不会要求用户手输账号密码,而是直接向固定后端请求自动会话。</p>
</div>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>快捷入口</h3><div class="panel-subtitle">从设置页快速回到最常用的工作区。</div></div></div>
<div class="list">
<div class="task-item">
<h4>项目与 Agent</h4>
<p>如果你想切项目、看默认 Agent 或确认当前工作上下文,可以直接从这里回去。</p>
<div class="task-meta">
${actionTag("去我的项目", "goto-intake")}
${actionTag("去 Agent", "goto-playbook")}
</div>
</div>
<div class="task-item">
<h4>生产与跟踪</h4>
<p>如果你更关心最近任务和跟踪动态,可以直接回到这两个工作页。</p>
<div class="task-meta">
${actionTag("去生产中心", "goto-production")}
${actionTag("去跟踪账号", "goto-tracking")}
</div>
</div>
</div>
</div>
</div>
</div>
` : `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>界面原则</h3><div class="panel-subtitle">帮助用户理解当前产品的使用方式。</div></div></div>
<div class="list">
<div class="task-item"><h4>首页优先看动作</h4><p>首页只负责“今天先做什么”,深层信息回到各工作页处理。</p></div>
<div class="task-item"><h4>单页只做一类决策</h4><p>重页面已经收成页内 tab避免一次看到太多层信息。</p></div>
<div class="task-item"><h4>系统治理不混进用户页</h4><p>管理员相关配置统一收口到管理员配置台。</p></div>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>帮助入口</h3><div class="panel-subtitle">用户先看到帮助和定位,不直接掉进系统控制面。</div></div></div>
<div class="list">
<div class="task-item">
<h4>连接状态</h4>
<p>如果页面看起来没有数据,先确认当前工作区是否已自动连接。</p>
<div class="task-meta">${actionTag("打开连接状态", "open-auth")}</div>
</div>
${isSuperAdmin() ? `
<div class="task-item">
<h4>管理员配置台</h4>
<p>系统依赖、存储、平台 Agent 和运维审计都在管理员配置台里,不放在普通用户页。</p>
<div class="task-meta">${actionTag("去管理员配置台", "goto-admin-workbench")}</div>
</div>
` : ""}
</div>
</div>
</div>
</div>
`}
</div>
`
);
}
function renderTopbar() {
const workspaceStrong = document.querySelector(".workspace-switch strong");
const workspaceSpan = document.querySelector(".workspace-switch span");
const searchInput = document.querySelector(".search input");
const avatar = document.querySelector(".avatar");
const topPills = document.querySelectorAll(".top-pill");
const platforms = document.querySelector(".topbar-left .chip-row");
const project = getSelectedProject();
if (workspaceStrong) {
workspaceStrong.textContent = project?.name || (appState.session ? "已连接工作区" : "未连接工作区");
}
if (workspaceSpan) {
workspaceSpan.textContent = appState.dashboard
? `${safeArray(appState.dashboard.projects).length} 个项目 · ${safeArray(appState.dashboard.assistants).length} 个 Agent`
: "连接后加载项目和 Agent";
}
if (searchInput) {
searchInput.value = "";
searchInput.placeholder = "搜项目、账号、内容、Agent";
}
if (avatar) {
avatar.textContent = initials(appState.me?.display_name || appState.me?.username || appState.session?.account?.display_name || "SF");
}
if (topPills.length >= 3) {
topPills[0].textContent = `项目 ${formatNumber(appState.dashboard?.projects?.length || 0)}`;
topPills[1].textContent = `对标 ${formatNumber(appState.accounts.length)}`;
topPills[2].textContent = `任务 ${formatNumber(appState.dashboard?.recent_jobs?.length || 0)}`;
}
if (platforms) {
const currentPlatform = getCurrentPlatformValue();
platforms.innerHTML = [
`<span class="chip">已接入平台</span>`,
...getPlatformOptions().map((item) => `
<span class="chip clickable-tag ${item.value === currentPlatform ? "active" : ""}" data-action="select-platform" data-platform="${escapeHtml(item.value)}">
${escapeHtml(getPlatformShortLabel(item.value))}
</span>
`)
].join("");
}
}
function syncRoleGatedNav() {
const allowAdmin = isSuperAdmin();
document.querySelectorAll("[data-role-gate]").forEach((element) => {
const gate = element.getAttribute("data-role-gate");
const visible = gate === "super_admin" ? allowAdmin : true;
element.classList.toggle("hidden", !visible);
element.hidden = !visible;
});
if (!allowAdmin && appState.screen === "admin-workbench") {
appState.screen = "dashboard";
}
}
function renderAll() {
renderTopbar();
renderAuthUi();
syncRoleGatedNav();
screenMap.dashboard.innerHTML = renderDashboardScreen();
screenMap.intake.innerHTML = renderProjectsScreen();
screenMap.discovery.innerHTML = renderDiscoveryScreen();
screenMap.tracking.innerHTML = renderTrackingScreen();
screenMap.automation.innerHTML = renderAutomationScreen();
screenMap.owned.innerHTML = renderOwnedScreen();
screenMap.playbook.innerHTML = renderPlaybookScreen();
if (screenMap.strategy) {
screenMap.strategy.innerHTML = renderStrategyScreen();
}
screenMap.production.innerHTML = renderProductionScreen();
screenMap.review.innerHTML = renderReviewScreen();
screenMap.credits.innerHTML = renderCreditsScreen();
if (screenMap.settings) {
screenMap.settings.innerHTML = renderSettingsScreen();
}
if (screenMap["admin-workbench"]) {
screenMap["admin-workbench"].innerHTML = renderAdminWorkbenchScreen();
}
renderOneLinerUi();
setScreen(screenMap[appState.screen] ? appState.screen : "dashboard");
}
async function createProject() {
if (!appState.session) {
openAuthModal();
return;
}
const name = window.prompt("输入项目名称");
if (!name) return;
const description = window.prompt("输入项目说明(可选)") || "";
setBusy(true, "正在创建项目...");
try {
await storyforgeFetch("/v2/projects", {
method: "POST",
body: { name, description }
});
await bootstrap();
} catch (error) {
alert("创建项目失败: " + error.message);
} finally {
setBusy(false, "");
}
}
function openPreferredModelAction() {
const models = getModelOptions();
const currentProfile = getCurrentModelProfile();
const currentId = currentProfile?.id || models[0]?.value || "";
const localCatalog = appState.localModelCatalog || {};
const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean);
openActionModal({
title: "设置分析主模型",
description: "后续导入分析、市场调研和风格学习会优先使用这里设置的模型。",
submitLabel: "保存模型",
fields: [
{
type: "html",
label: "本机模型网关",
html: `
<div class="task-item compact">
<h4>${escapeHtml(localCatalog.reachable ? "网关在线" : "网关离线")}</h4>
<p>${escapeHtml(currentProfile ? `当前主模型:${currentProfile.name} · ${currentProfile.model_name || "-"}` : `默认模型:${localCatalog.default_model || "GLM-5"}`)}</p>
<div class="task-meta">
${gatewayModels.slice(0, 6).map((model) => `<span class="tag">${escapeHtml(model)}</span>`).join("") || `<span class="tag red">暂未读取到模型目录</span>`}
${localCatalog.management_url ? `<a class="tag blue" href="${escapeHtml(localCatalog.management_url)}" target="_blank" rel="noreferrer">打开管理页</a>` : ""}
</div>
</div>
`
},
{ name: "modelProfileId", label: "主模型", type: "select", value: currentId, options: models }
],
onSubmit: async (values) => {
if (!values.modelProfileId) throw new Error("请先选择一个模型");
await storyforgeFetch("/v2/me/preferences/analysis-model", {
method: "POST",
body: { model_profile_id: values.modelProfileId }
});
rememberAction("主模型已更新", "新的分析主模型已经保存。", "green");
await bootstrap();
}
});
}
function rememberAction(title, summary, tone = "blue", payload = null) {
appState.lastAction = {
title,
summary,
tone,
payload,
createdAt: new Date().toISOString()
};
}
function extractGeneratedCopy(payload) {
const raw = payload?.content || payload?.text || payload?.copy || payload?.result?.content || "";
return brief(raw, 2400);
}
function renderLastActionCard() {
if (!appState.lastAction) return "";
const payload = appState.lastAction.payload || {};
const recommendedAction = payload?.result?.recommended_action || payload?.recommended_action || null;
const runId = payload?.id || payload?.run_id || "";
const landingAttrs = buildMainAgentLandingAttrs({
runId,
screen: recommendedAction?.screen || "",
title: appState.lastAction.title || "",
summary: recommendedAction?.summary || appState.lastAction.summary || ""
});
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>最近动作</h3>
<div class="panel-subtitle">${escapeHtml(formatDateTime(appState.lastAction.createdAt))}</div>
</div>
<span class="tag ${escapeHtml(appState.lastAction.tone || "blue")}">${escapeHtml(appState.lastAction.title)}</span>
</div>
<div class="task-item">
<h4>${escapeHtml(appState.lastAction.title)}</h4>
<p>${escapeHtml(appState.lastAction.summary)}</p>
${(runId || recommendedAction?.action) ? `
<div class="task-meta" style="margin-top:10px;">
${runId ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(runId)}">查看结果</span>` : ""}
${recommendedAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(recommendedAction.action)}" ${landingAttrs}>${escapeHtml(recommendedAction.label || "回到对应页面")}</span>` : ""}
</div>
` : ""}
</div>
</div>
`;
}
function getJobRecoveryCategory(job) {
const sourceType = String(job?.source_type || "").toLowerCase();
const lineType = String(job?.line_type || "").toLowerCase();
if (sourceType === "text" || lineType === "analysis") return "analysis";
if (sourceType === "video_link") return "analysis";
if (sourceType === "content_source_sync") return "analysis";
if (sourceType === "upload_video") return "analysis";
if (lineType === "copy") return "copy";
if (lineType === "ai_video" || sourceType === "ai_video") return "ai_video";
if (lineType === "real_cut" || sourceType === "real_cut") return "real_cut";
if (lineType === "live_recorder" || sourceType === "live_recorder") return "recorder";
return "";
}
function getQuotaSummaryForCategory(category) {
const quota = appState.tenantQuota || {};
const usage = appState.tenantUsage || quota?.usage || {};
if (!quota || quota.enabled === false || !category) {
return { enabled: true, blocked: false, reason: "", usage, quota };
}
const categoryQuotaField = {
analysis: "analysis_quota",
copy: "copy_quota",
ai_video: "ai_video_quota",
real_cut: "real_cut_quota",
recorder: "recorder_quota"
}[category];
const categoryUsage = usage?.categories?.[category] || {};
const consumed = Number(categoryUsage.quantity || 0);
const allowed = Number(quota[categoryQuotaField] || 0);
if (allowed && consumed >= allowed) {
return {
enabled: true,
blocked: true,
reason: `当前租户本周期的 ${category} 配额已用完`,
usage,
quota
};
}
const budget = Number(quota.monthly_budget_cents || 0);
const totalCost = Number(usage.total_cost_cents || 0);
const categoryCost = {
analysis: 6,
copy: 3,
ai_video: 30,
real_cut: 20,
recorder: 2
}[category] || 0;
if (budget && totalCost + categoryCost > budget) {
return {
enabled: true,
blocked: true,
reason: "当前租户本周期预算不足,已阻止本次动作执行",
usage,
quota
};
}
if (quota.storage_over_limit && ["analysis", "ai_video", "real_cut"].includes(category)) {
return {
enabled: true,
blocked: true,
reason: "当前租户存储额度已满,已阻止继续产生大文件缓存",
usage,
quota
};
}
return { enabled: true, blocked: false, reason: "", usage, quota };
}
function getJobRecoverability(job) {
const status = String(job?.status || "").toLowerCase();
const sourceType = String(job?.source_type || "").toLowerCase();
const lineType = String(job?.line_type || "").toLowerCase();
const category = getJobRecoveryCategory(job);
const quotaGuard = getQuotaSummaryForCategory(category);
const sourceJobId = String(job?.artifacts?.source_job_id || job?.result?.source_job_id || job?.parent_job_id || "").trim();
const sourceAccountUrl = String(job?.artifacts?.source_account_url || job?.source_url || "").trim();
const uploadedPath = String(job?.artifacts?.uploaded_path || "").trim();
const cutvideoRequest = job?.artifacts?.cutvideo_request || {};
const base = {
status,
sourceType,
lineType,
category,
quotaGuard,
sourceJobId,
sourceAccountUrl,
uploadedPath,
cutvideoRequest
};
if (!job?.id) {
return {
...base,
state: "missing",
label: "未加载",
reason: "当前任务详情还没有加载完成。",
recoverable: false,
actionLabel: "刷新后再试",
actionKey: "refresh-data"
};
}
if (["completed", "done", "succeeded"].includes(status)) {
return {
...base,
state: "completed",
label: "已完成",
reason: "这条任务已经完成,不需要恢复。",
recoverable: false,
actionLabel: "查看详情",
actionKey: "open-job-detail"
};
}
if (status !== "failed") {
return {
...base,
state: "active",
label: "运行中",
reason: "这条任务还没有失败,先等待当前流程跑完。",
recoverable: false,
actionLabel: "继续观察",
actionKey: "open-job-detail"
};
}
if (quotaGuard.blocked) {
return {
...base,
state: "blocked",
label: "额度拦截",
reason: quotaGuard.reason,
recoverable: false,
actionLabel: "先补额度",
actionKey: "open-tenant-quota"
};
}
if (sourceType === "upload_video") {
if (backendSupports("/v2/explore/jobs/{job_id}/retry") && uploadedPath) {
return {
...base,
state: "recoverable",
label: "可恢复",
reason: "上传素材仍可复用,可以直接重新入队这条分析任务。",
recoverable: true,
actionLabel: "恢复上传分析",
actionKey: "recover-job"
};
}
return {
...base,
state: "manual",
label: "需补素材",
reason: uploadedPath
? "这条上传任务需要重新上传原始素材后再恢复。"
: "这条任务缺少可复用的上传素材,需要先补回原文件。",
recoverable: false,
actionLabel: "重新上传",
actionKey: "open-upload-video"
};
}
if (lineType === "real_cut") {
if (!sourceJobId) {
return {
...base,
state: "manual",
label: "缺少源任务",
reason: "实拍剪辑缺少源任务,暂时无法自动恢复。",
recoverable: false,
actionLabel: "看源任务",
actionKey: "open-job-detail"
};
}
return {
...base,
state: "recoverable",
label: "可恢复",
reason: "可以基于源任务重新发起实拍剪辑。",
recoverable: true,
actionLabel: "恢复实拍剪辑",
actionKey: "recover-job"
};
}
if (lineType === "ai_video") {
if (!sourceJobId) {
return {
...base,
state: "manual",
label: "缺少源任务",
reason: "AI 视频缺少源任务,暂时无法自动恢复。",
recoverable: false,
actionLabel: "看源任务",
actionKey: "open-job-detail"
};
}
return {
...base,
state: "recoverable",
label: "可恢复",
reason: "可以基于源任务和当前 brief 重新发起 AI 视频。",
recoverable: true,
actionLabel: "恢复 AI 视频",
actionKey: "recover-job"
};
}
if (sourceType === "content_source_sync") {
if (!sourceAccountUrl) {
return {
...base,
state: "manual",
label: "缺少主页",
reason: "内容源同步缺少主页地址,暂时无法自动恢复。",
recoverable: false,
actionLabel: "去导入主页",
actionKey: "open-import-homepage"
};
}
return {
...base,
state: "recoverable",
label: "可恢复",
reason: "可以基于同一主页重新触发内容源同步。",
recoverable: true,
actionLabel: "恢复同步",
actionKey: "recover-job"
};
}
if (sourceType === "text" || sourceType === "video_link") {
const sourceValue = sourceType === "text"
? String(job?.artifacts?.input_text || job?.transcript_text || "").trim()
: String(job?.source_url || job?.artifacts?.video_url || job?.artifacts?.source_url || "").trim();
if (!sourceValue) {
return {
...base,
state: "manual",
label: "缺少输入",
reason: sourceType === "text" ? "缺少原始文本,暂时无法自动恢复。" : "缺少原始视频链接,暂时无法自动恢复。",
recoverable: false,
actionLabel: "查看详情",
actionKey: "open-job-detail"
};
}
return {
...base,
state: "recoverable",
label: "可恢复",
reason: sourceType === "text"
? "可以直接用原始文本重新发起分析。"
: "可以直接用原始视频链接重新发起分析。",
recoverable: true,
actionLabel: sourceType === "text" ? "恢复分析" : "恢复分析",
actionKey: "recover-job"
};
}
return {
...base,
state: "manual",
label: "需人工处理",
reason: "当前链路没有可自动恢复的模板,建议交给管理员处理。",
recoverable: false,
actionLabel: "管理员处理",
actionKey: "goto-production"
};
}
function getJobRecoveryRequest(job) {
const recovery = getJobRecoverability(job);
if (!recovery.recoverable) {
throw new Error(recovery.reason || "当前任务暂不支持自动恢复");
}
const projectId = job?.project_id || appState.selectedProjectId || "";
const assistantId = job?.assistant_id || "";
const knowledgeBaseId = job?.knowledge_base_id || "";
const analysisModelProfileId = job?.analysis_model_profile_id || "";
const title = String(job?.title || "任务").trim();
if (job?.source_type === "text") {
return {
endpoint: "/v2/explore/text",
body: {
project_id: projectId,
knowledge_base_id: knowledgeBaseId,
assistant_id: assistantId,
analysis_model_profile_id: analysisModelProfileId,
title,
content: String(job?.artifacts?.input_text || job?.transcript_text || "").trim()
},
reason: "基于原始文本重新发起分析"
};
}
if (job?.source_type === "video_link") {
return {
endpoint: "/v2/explore/video-link",
body: {
project_id: projectId,
knowledge_base_id: knowledgeBaseId,
assistant_id: assistantId,
analysis_model_profile_id: analysisModelProfileId,
title,
language: job?.language || "auto",
video_url: String(job?.source_url || job?.artifacts?.video_url || job?.artifacts?.source_url || "").trim()
},
reason: "基于原始视频链接重新发起分析"
};
}
if (job?.source_type === "content_source_sync") {
const sourceUrl = String(job?.artifacts?.source_account_url || job?.source_url || "").trim();
return {
endpoint: "/v2/pipelines/content-source-sync",
body: {
project_id: projectId,
knowledge_base_id: knowledgeBaseId,
assistant_id: assistantId,
analysis_model_profile_id: analysisModelProfileId,
content_source_id: String(job?.content_source_id || ""),
platform: String(job?.artifacts?.platform || ""),
handle: String(job?.artifacts?.handle || ""),
source_url: sourceUrl,
title,
language: job?.language || "auto",
max_items: Number(job?.artifacts?.max_items || 5),
skip_existing: Boolean(job?.artifacts?.skip_existing !== false),
auto_trigger_analysis: Boolean(job?.artifacts?.auto_trigger_analysis !== false)
},
reason: "基于同一主页重新触发同步"
};
}
if (job?.line_type === "real_cut" || job?.source_type === "real_cut") {
const request = job?.artifacts?.cutvideo_request || {};
const sourceJobId = String(job?.artifacts?.source_job_id || job?.parent_job_id || "").trim();
return {
endpoint: "/v2/pipelines/real-cut",
body: {
project_id: projectId,
title,
source_job_id: sourceJobId,
base_config: String(request.base_config || ""),
objective: String(request.objective || ""),
target_duration_sec: Number(request.target_duration_sec || 60),
target_aspect_ratio: String(request.target_aspect_ratio || "9:16"),
ideal_segment_duration_sec: Number(request.ideal_segment_duration_sec || 8),
max_segment_duration_sec: Number(request.max_segment_duration_sec || 20),
transcript_backend: String(request.transcript_backend || "auto"),
transcript_device: String(request.transcript_device || ""),
review_enabled: Boolean(request.review_enabled),
dry_run: Boolean(request.dry_run)
},
reason: "基于源任务重新发起实拍剪辑"
};
}
if (job?.line_type === "ai_video" || job?.source_type === "ai_video") {
const sourceJobId = String(job?.artifacts?.source_job_id || job?.result?.source_job_id || job?.parent_job_id || "").trim();
return {
endpoint: "/v2/pipelines/ai-video",
body: {
project_id: projectId,
assistant_id: assistantId,
knowledge_base_id: knowledgeBaseId,
source_job_id: sourceJobId,
title,
brief: String(job?.artifacts?.brief || job?.result?.brief || job?.style_summary || job?.transcript_text || "").trim(),
style: String(job?.artifacts?.style || "realistic"),
shots: Number(job?.artifacts?.shots || 4),
duration: Number(job?.artifacts?.duration || 5),
image_provider: String(job?.artifacts?.image_provider || ""),
image_model: String(job?.artifacts?.image_model || ""),
video_provider: String(job?.artifacts?.video_provider || ""),
video_model: String(job?.artifacts?.video_model || ""),
aspect_ratio: String(job?.artifacts?.aspect_ratio || "9:16")
},
reason: "基于源任务重新发起 AI 视频"
};
}
throw new Error("当前任务暂不支持自动恢复");
}
async function recoverJobAction(jobId, options = {}) {
const sourceJob = safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === jobId)
|| (appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null);
const job = options.job || sourceJob || await storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}`).catch(() => null);
if (!job) {
throw new Error("没有找到这条任务");
}
const recovery = getJobRecoverability(job);
if (!recovery.recoverable) {
throw new Error(recovery.reason || "当前任务暂不支持恢复");
}
if (backendSupports("/v2/explore/jobs/{job_id}/retry")) {
const retried = await storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(job.id)}/retry`, {
method: "POST"
});
const retriedJob = retried?.job || retried;
recordRecoveryEvent({
job_id: job.id,
job_title: job.title,
job_line_type: job.line_type,
job_source_type: job.source_type,
job_status: job.status,
action_key: "recover-job",
mode: options.mode || "single",
summary: `${job.title || job.id} 已重新入队`,
reason: "通过任务重试接口恢复",
target_job_id: retriedJob?.id || job.id,
target_job_title: retriedJob?.title || job.title || "",
target_status: retriedJob?.status || "",
result_label: "已重新入队",
result_reason: recovery.reason,
user_feedback: String(options.user_feedback || "")
});
return {
created: retriedJob,
source: job,
request: {
endpoint: `/v2/explore/jobs/${encodeURIComponent(job.id)}/retry`,
body: {},
reason: "通过任务重试接口恢复"
}
};
}
const request = getJobRecoveryRequest(job);
const payload = await storyforgeFetch(request.endpoint, {
method: "POST",
body: request.body
});
const created = payload?.job || payload?.result?.job || payload;
const createdJobId = created?.id || created?.job_id || "";
recordRecoveryEvent({
job_id: job.id,
job_title: job.title,
job_line_type: job.line_type,
job_source_type: job.source_type,
job_status: job.status,
action_key: "recover-job",
mode: options.mode || "single",
summary: `${job.title || job.id} 已重新发起`,
reason: request.reason,
target_job_id: createdJobId,
target_job_title: created?.title || created?.job?.title || "",
target_status: created?.status || created?.job?.status || "",
result_label: "已创建恢复任务",
result_reason: request.reason,
user_feedback: String(options.user_feedback || "")
});
return {
created,
source: job,
request
};
}
function getRecoverableFailedJobs() {
return safeArray(appState.dashboard?.recent_jobs)
.filter((job) => String(job?.status || "").toLowerCase() === "failed")
.map((job) => ({ job, recovery: getJobRecoverability(job) }))
.sort((left, right) => compareDateDesc(left.job.updated_at || left.job.created_at, right.job.updated_at || right.job.created_at));
}
function renderRecoveryHistoryPanel() {
const records = getRecoveryRecords();
const recentRecords = records.slice(0, 6);
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>最近恢复记录</h3>
<div class="panel-subtitle">本地记录最近一次恢复动作,方便量产阶段回看恢复是否真的跑通。</div>
</div>
<span class="tag blue">${escapeHtml(formatNumber(records.length))} 条</span>
</div>
<div class="list">
${recentRecords.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.job_title || "恢复记录")}</h4>
<p>${escapeHtml(item.summary || item.reason || "恢复动作已完成。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(item.mode || "single")}</span>
<span class="tag ${item.target_job_id ? "green" : "orange"}">${escapeHtml(item.target_job_id ? "已创建恢复任务" : "待确认结果")}</span>
${item.job_line_type ? `<span class="tag">${escapeHtml(item.job_line_type)}</span>` : ""}
${item.created_at ? `<span class="tag">${escapeHtml(formatDateTime(item.created_at))}</span>` : ""}
${item.target_job_id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(item.target_job_id)}">看恢复任务</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有恢复记录</h4><p>你执行过的恢复动作会出现在这里。</p></div>`}
</div>
</div>
`;
}
function renderQuotaBlockingNotice() {
const quota = appState.tenantQuota || {};
const usage = appState.tenantUsage || quota?.usage || {};
const labels = [
{ key: "analysis", label: "分析" },
{ key: "copy", label: "文案" },
{ key: "ai_video", label: "AI 视频" },
{ key: "real_cut", label: "实拍剪辑" },
{ key: "recorder", label: "录制" }
];
const blocked = labels
.map((item) => {
const guard = getQuotaSummaryForCategory(item.key);
return { ...item, guard };
})
.filter((item) => item.guard.blocked);
const storageBlocked = Boolean(quota?.storage_over_limit);
if (!blocked.length && !storageBlocked) return "";
return `
<div class="panel pad quota-notice">
<div class="panel-head">
<div>
<h3>租户拦截提示</h3>
<div class="panel-subtitle">当前租户已经触发了硬拦截,前端会先提示原因,再引导你补额度或清理存储。</div>
</div>
<span class="tag red">已拦截</span>
</div>
<div class="task-item">
<h4>${escapeHtml(storageBlocked ? "存储额度已满" : "动作额度已满")}</h4>
<p>${escapeHtml(storageBlocked
? "当前租户存储已经达到上限,建议先清理旧产物或提升存储配额。"
: blocked.map((item) => item.guard.reason).join(""))}</p>
<div class="task-meta">
${blocked.map((item) => `<span class="tag red">${escapeHtml(item.label)}已满</span>`).join("")}
${storageBlocked ? `<span class="tag red">存储超限</span>` : ""}
<span class="tag clickable-tag" data-action="open-tenant-quota">调整额度</span>
</div>
</div>
<div class="mini-grid" style="margin-top:14px;">
<div class="mini-card">
<small>预算</small>
<strong>${escapeHtml(formatNumber((quota?.monthly_budget_cents || 0) / 100))} 元</strong>
<span>已用 ${escapeHtml(formatNumber((usage?.total_cost_cents || 0) / 100))} 元</span>
</div>
<div class="mini-card">
<small>存储</small>
<strong>${escapeHtml(formatBytes(usage?.storage_bytes || 0))}</strong>
<span>${escapeHtml(quota?.storage_limit_bytes ? `上限 ${formatBytes(quota.storage_limit_bytes)}` : "未设置上限")}</span>
</div>
<div class="mini-card">
<small>分析</small>
<strong>${escapeHtml(formatNumber(usage?.categories?.analysis?.quantity || 0))}</strong>
<span>${escapeHtml(`上限 ${formatNumber(quota?.analysis_quota || 0)}`)}</span>
</div>
<div class="mini-card">
<small>AI 视频</small>
<strong>${escapeHtml(formatNumber(usage?.categories?.ai_video?.quantity || 0))}</strong>
<span>${escapeHtml(`上限 ${formatNumber(quota?.ai_video_quota || 0)}`)}</span>
</div>
</div>
</div>
`;
}
function renderLastJobDetailCard() {
const detail = appState.lastJobDetail;
if (!detail?.job) return "";
const previewLinks = getJobPreviewLinks(detail.job);
const recovery = getJobRecoverability(detail.job);
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>最近任务详情</h3>
<div class="panel-subtitle">${escapeHtml(formatDateTime(detail.job.created_at))}</div>
</div>
<div class="task-meta">
<span class="tag ${statusTone(detail.job.status)}">${escapeHtml(detail.job.status || "-")}</span>
<span class="tag ${recovery.recoverable ? "green" : recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.label)}</span>
</div>
</div>
<div class="task-item">
<h4>${escapeHtml(detail.job.title || detail.job.id)}</h4>
<p>${escapeHtml(brief(detail.job.style_summary || detail.job.transcript_text || detail.job.error || "暂无摘要", 120))}</p>
<div class="task-meta">
<span class="tag">${escapeHtml(detail.job.line_type || "-")}</span>
${detail.job.status === "failed" ? `<span class="tag ${recovery.recoverable ? "green" : "orange"}">${escapeHtml(recovery.reason)}</span>` : ""}
${detail.job.status === "completed" ? actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(detail.job.id)}"`) : ""}
${detail.job.status === "failed" ? actionTag(
recovery.actionLabel,
recovery.recoverable ? "recover-job" : recovery.actionKey,
`data-job-id="${escapeHtml(detail.job.id)}"`,
{ disabledReason: recovery.recoverable ? "" : recovery.reason, title: recovery.reason }
) : ""}
${canDeriveAiVideo(detail.job) ? renderPipelineJobTag("aiVideo", detail.job, "做 AI 视频") : ""}
${canDeriveRealCut(detail.job) ? renderPipelineJobTag("realCut", detail.job, "做实拍剪辑") : ""}
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(detail.job.id)}"`)}
</div>
</div>
${detail.job.status === "failed" ? `
<div class="task-item compact">
<h4>恢复判断</h4>
<p>${escapeHtml(recovery.reason)}</p>
<div class="task-meta">
<span class="tag ${recovery.recoverable ? "green" : recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.actionLabel)}</span>
${recovery.sourceJobId ? `<span class="tag">源任务 ${escapeHtml(brief(recovery.sourceJobId, 12))}</span>` : ""}
${recovery.quotaGuard?.blocked ? `<span class="tag red">额度拦截</span>` : ""}
</div>
</div>
` : ""}
${previewLinks.length ? `
<div class="list">
${previewLinks.slice(0, 3).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.label.replace(/^result\./, "").replace(/^artifacts\./, ""))}</h4>
<p>${escapeHtml(item.url)}</p>
</div>
`).join("")}
</div>
` : ""}
</div>
`;
}
function requireSelectedProject() {
const project = getSelectedProject();
if (!project) throw new Error("请先创建项目");
return project;
}
function requireSelectedAssistant() {
const assistant = getSelectedAssistant();
if (!assistant) throw new Error("请先创建 Agent");
return assistant;
}
function requireSelectedAccountRow() {
const account = getSelectedAccount();
if (!account) throw new Error("请先在“找对标”里选中一个账号");
return account;
}
function openImportHomepageAction() {
const project = requireSelectedProject();
const kb = getProjectKnowledgeBases(project.id)[0];
const assistants = getAssistantOptions(project.id);
openActionModal({
title: "导入主页并同步",
description: "适合抖音 / 小红书 / B站 / 快手 / 视频号主页。先建内容源,再触发同步与分析。",
submitLabel: "开始同步",
fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "platform", label: "平台", type: "select", value: "douyin", options: getPlatformOptions() },
{ name: "title", label: "标题", placeholder: "例如:创业口播对标账号" },
{ name: "handle", label: "账号名 / handle", placeholder: "可选" },
{ name: "sourceUrl", label: "主页链接", type: "url", placeholder: "https://..." },
{ name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] },
{ name: "maxItems", label: "最多同步作品数", type: "number", value: 5, min: 1, max: 20 }
],
onSubmit: async (values) => {
if (!values.sourceUrl?.trim()) throw new Error("请填写主页链接");
const projectId = values.projectId || project.id;
const platform = normalizePlatformValue(values.platform, "douyin");
const source = await storyforgeFetch("/v2/content-sources", {
method: "POST",
body: {
project_id: projectId,
source_kind: "creator_account",
platform,
handle: values.handle || "",
source_url: values.sourceUrl.trim(),
title: values.title || values.handle || "主页对标",
metadata: {}
}
});
const job = await storyforgeFetch("/v2/pipelines/content-source-sync", {
method: "POST",
body: {
project_id: projectId,
knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || kb?.id || "",
assistant_id: values.assistantId || "",
content_source_id: source.id,
platform,
handle: values.handle || "",
source_url: values.sourceUrl.trim(),
title: values.title || values.handle || "主页对标",
max_items: Number(values.maxItems || 5),
skip_existing: true,
auto_trigger_analysis: true
}
});
rememberAction("主页同步已启动", `已把主页加入项目,并创建同步任务 ${job.title || job.id}`, "blue", job);
await bootstrap();
}
});
}
function openImportSelectedAccountAction() {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) {
rememberAction("当前平台待接入", gate.reason, "orange");
renderAll();
return;
}
const platform = gate.platform;
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
const currentSources = getCurrentProjectSourcesForAccount(account, project.id);
const currentSource = currentSources[0];
const kb = getProjectKnowledgeBases(project.id)[0];
openActionModal({
title: currentSource ? "继续同步当前对标" : "导入当前对标",
description: currentSource
? "当前项目里已经有这个对标账号,继续触发同步并可切换绑定 Agent。"
: "把当前选中的对标账号加入项目,并绑定 Agent 进入持续同步。",
submitLabel: currentSource ? "继续同步" : "导入并同步",
fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "platform", label: "平台", type: "select", value: normalizePlatformValue(currentSource?.platform || platform), options: getPlatformOptions() },
{ name: "title", label: "内容源标题", value: currentSource?.title || `${getAccountName(account)} 对标主页` },
{ name: "handle", label: "账号标识", value: currentSource?.handle || getAccountHandle(account) || "" },
{ name: "sourceUrl", label: "主页链接", type: "url", value: currentSource?.source_url || getAccountProfileUrl(account) || "", placeholder: "https://..." },
{ name: "assistantId", label: "绑定 Agent", type: "select", value: getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] },
{ name: "maxItems", label: "最多同步作品数", type: "number", value: Number(currentSource?.metadata?.max_items || 6), min: 1, max: 20 },
{ name: "skipExisting", label: "跳过已存在作品", type: "checkbox", value: true },
{ name: "autoAnalyze", label: "同步后自动分析", type: "checkbox", value: true }
],
onSubmit: async (values) => {
if (!values.sourceUrl?.trim()) throw new Error("请先填写主页链接");
const projectId = values.projectId || project.id;
const platform = normalizePlatformValue(values.platform, "douyin");
const source = currentSource && currentSource.project_id === projectId
? currentSource
: await storyforgeFetch("/v2/content-sources", {
method: "POST",
body: {
project_id: projectId,
source_kind: "creator_account",
platform,
handle: values.handle || "",
source_url: values.sourceUrl.trim(),
title: values.title || values.handle || getAccountName(account) || "对标主页",
metadata: {
imported_from_account_id: account.id,
imported_from_workspace: "discovery"
}
}
});
const job = await storyforgeFetch("/v2/pipelines/content-source-sync", {
method: "POST",
body: {
project_id: projectId,
knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || kb?.id || "",
assistant_id: values.assistantId || "",
content_source_id: source.id,
platform,
handle: values.handle || getAccountHandle(account) || "",
source_url: values.sourceUrl.trim(),
title: values.title || getAccountName(account) || values.handle || "对标主页",
max_items: Number(values.maxItems || 6),
skip_existing: Boolean(values.skipExisting),
auto_trigger_analysis: Boolean(values.autoAnalyze)
}
});
rememberAction("对标已接入项目", `已把「${getAccountName(account) || "当前对标"}」接入项目,并创建同步任务 ${job.title || job.id}`, "green", { source, job });
await bootstrap();
}
});
}
function openTrackSelectedAccountAction() {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) {
rememberAction("当前平台待接入", gate.reason, "orange");
renderAll();
return;
}
const platform = gate.platform;
const trackingAccountsPath = getWorkbenchRoute(platform, "trackingAccounts");
if (!trackingAccountsPath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
const trackedItem = safeArray(appState.trackingAccounts).find((item) => item.tracked_account_id === account.id);
openActionModal({
title: trackedItem ? "更新跟踪账号" : "加入跟踪",
description: trackedItem
? "这个账号已经在跟踪中,可以切换负责 Agent 或补充备注。"
: "把当前对标账号加入每日跟踪,后续自动生成更新日报。",
submitLabel: trackedItem ? "保存跟踪" : "开始跟踪",
fields: [
{ name: "accountName", label: "账号", type: "html", html: `<div class="sheet-html"><strong>${escapeHtml(getAccountName(account) || "未命名账号")}</strong><p>${escapeHtml(getAccountProfileUrl(account) || account.signature || "")}</p></div>` },
{ name: "assistantId", label: "负责 Agent", type: "select", value: trackedItem?.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "note", label: "跟踪备注", value: trackedItem?.note || "", placeholder: "例如:重点观察开头结构、成交句式和更新频率" }
],
onSubmit: async (values) => {
await storyforgeFetch(trackingAccountsPath, {
method: "POST",
body: {
tracked_account_id: account.id,
assistant_id: values.assistantId || "",
note: values.note || ""
}
});
rememberAction(trackedItem ? "跟踪已更新" : "已加入跟踪", `账号「${getAccountName(account) || "当前对标"}」现在会进入更新日报。`, "green");
await bootstrap();
}
});
}
function openImportVideoLinkAction() {
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
openActionModal({
title: "导入作品链接",
description: "直接把单条视频链接送进分析链。",
submitLabel: "开始分析",
fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "title", label: "标题", placeholder: "可选,不填则使用默认标题" },
{ name: "videoUrl", label: "作品链接", type: "url", placeholder: "https://..." },
{ name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] },
{ name: "language", label: "语言", type: "select", value: "auto", options: [{ value: "auto", label: "自动" }, { value: "zh-CN", label: "中文" }] }
],
onSubmit: async (values) => {
if (!values.videoUrl?.trim()) throw new Error("请填写作品链接");
const projectId = values.projectId || project.id;
const job = await storyforgeFetch("/v2/explore/video-link", {
method: "POST",
body: {
video_url: values.videoUrl.trim(),
title: values.title || "",
project_id: projectId,
knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || "",
assistant_id: values.assistantId || "",
language: values.language || "auto"
}
});
rememberAction("作品分析已启动", `已创建分析任务 ${job.title || job.id}`, "blue", job);
await bootstrap();
}
});
}
function openImportTextAction() {
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
openActionModal({
title: "导入文本素材",
description: "把口播稿、拆解稿或灵感文本直接送进知识与分析链。",
submitLabel: "开始分析",
fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "title", label: "标题", placeholder: "例如:创业口播拆解" },
{ name: "content", label: "正文", type: "textarea", rows: 8, placeholder: "粘贴需要分析的文本" },
{ name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] }
],
onSubmit: async (values) => {
if (!values.title?.trim()) throw new Error("请填写标题");
if (!values.content?.trim()) throw new Error("请填写正文");
const projectId = values.projectId || project.id;
const job = await storyforgeFetch("/v2/explore/text", {
method: "POST",
body: {
title: values.title.trim(),
content: values.content.trim(),
project_id: projectId,
knowledge_base_id: getProjectKnowledgeBases(projectId)[0]?.id || "",
assistant_id: values.assistantId || ""
}
});
rememberAction("文本分析已启动", `已创建文本分析任务 ${job.title || job.id}`, "blue", job);
await bootstrap();
}
});
}
function openUploadVideoAction() {
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
openActionModal({
title: "上传本地视频",
description: "上传本地素材,直接进入分析链。",
submitLabel: "上传并分析",
fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "title", label: "标题", placeholder: "可选,不填则用文件名" },
{ name: "assistantId", label: "绑定 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] },
{ name: "file", label: "本地视频", type: "file", accept: ".mp4,.mov,.m4v,.avi,.mkv,.webm" }
],
onSubmit: async (values) => {
if (!values.file) throw new Error("请先选择本地视频");
const projectId = values.projectId || project.id;
const form = new FormData();
form.append("file", values.file);
form.append("title", values.title || "");
form.append("project_id", projectId);
form.append("knowledge_base_id", getProjectKnowledgeBases(projectId)[0]?.id || "");
form.append("assistant_id", values.assistantId || "");
const job = await storyforgeFetch("/v2/explore/upload-video", {
method: "POST",
body: form
});
rememberAction("上传分析已启动", `已上传素材并创建任务 ${job.title || job.id}`, "blue", job);
await bootstrap();
}
});
}
function openOneLinerProfileAction() {
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
const profile = appState.onelinerProfile || {};
openActionModal({
title: "配置 OneLiner",
description: "绑定总控主 Agent 的默认平台、长期目标和默认执行 Agent。",
submitLabel: "保存配置",
fields: [
{ name: "assistantId", label: "默认执行 Agent", type: "select", value: profile.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "displayName", label: "显示名", value: profile.display_name || "OneLiner", placeholder: "例如:增长总控 OneLiner" },
{ name: "defaultPlatform", label: "默认平台", type: "select", value: normalizePlatformValue(profile.default_platform || getPreferredPlatform(), "douyin"), options: getPlatformOptions() },
{ name: "longTermGoal", label: "长期目标", type: "textarea", rows: 4, value: profile.long_term_goal || "", placeholder: "例如:围绕创业 IP 做跨平台增长与成交转化" },
{ name: "notes", label: "补充说明", type: "textarea", rows: 4, value: profile.notes || "", placeholder: "例如:前端没产品化的需求先由 OneLiner 承接,不允许直接改核心代码" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/oneliner/profile", {
method: "PUT",
body: {
project_id: project.id,
assistant_id: values.assistantId || "",
display_name: values.displayName || "OneLiner",
default_platform: values.defaultPlatform || "douyin",
long_term_goal: values.longTermGoal || "",
notes: values.notes || "",
config: {
chat_only_for_unreleased_ui: true,
commercial_ready: true,
tenant_isolation_required: true
}
}
});
appState.onelinerProfile = saved;
await loadAgentControlSurfaces(project.id);
rememberAction("OneLiner 已保存", `已更新 OneLiner「${saved.display_name || "OneLiner"}」配置。`, "green", saved);
renderAll();
}
});
}
function parsePolicyJsonField(rawValue, label = "策略 JSON") {
const text = String(rawValue || "").trim();
if (!text) return {};
try {
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${label} 必须是 JSON 对象`);
}
return parsed;
} catch (error) {
throw new Error(`${label} 格式不正确:${error.message}`);
}
}
function renderPolicyVersionSummary(bundle, emptyText) {
if (!bundle?.current_version) {
return `
<div class="sheet-html">
<div class="task-item compact">
<h4>还没有已发布版本</h4>
<p>${escapeHtml(emptyText)}</p>
</div>
</div>
`;
}
return `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(bundle.current_version.title || bundle.scope?.title || "当前版本")}</h4>
<p>${escapeHtml(bundle.current_version.summary || "当前版本还没有补摘要。")}</p>
<div class="task-meta">
<span class="tag blue">版本 ${escapeHtml(formatNumber(bundle.current_version.version_no || 0))}</span>
<span class="tag">历史 ${escapeHtml(formatNumber(bundle.versions?.count || 0))}</span>
${bundle.effectivity?.effect_mode ? `<span class="tag">${escapeHtml(bundle.effectivity.effect_mode)}</span>` : ""}
</div>
</div>
</div>
`;
}
function getAdminGovernanceDirectoryItems() {
return safeArray(appState.adminGovernanceDirectory);
}
function findAdminGovernanceDirectoryItem(targetUserId) {
return getAdminGovernanceDirectoryItems().find((item) => item.id === targetUserId) || null;
}
function getAdminOverrideTargetState() {
const directoryItems = getAdminGovernanceDirectoryItems();
const existing = appState.adminOverrideTarget || {};
const targetUserId = String(existing.targetUserId || existing.target_user_id || directoryItems[0]?.id || "");
const targetUser = findAdminGovernanceDirectoryItem(targetUserId) || directoryItems[0] || null;
const targetProjects = safeArray(targetUser?.projects);
const targetProjectId = String(existing.targetProjectId || existing.target_project_id || targetProjects[0]?.id || "");
return {
targetUserId,
targetProjectId,
platform: normalizePlatformValue(existing.platform || getPreferredPlatform(), "douyin")
};
}
function formatAdminGovernanceTargetLabel(target) {
const directoryItem = findAdminGovernanceDirectoryItem(target?.targetUserId || target?.target_user_id || "");
const project = safeArray(directoryItem?.projects).find((item) => item.id === (target?.targetProjectId || target?.target_project_id || ""));
const userLabel = directoryItem ? `${directoryItem.display_name || directoryItem.username || directoryItem.id}${directoryItem.role ? ` · ${directoryItem.role}` : ""}` : "未选择目标";
const projectLabel = project ? project.name || project.id : "默认用户全局";
return `${userLabel} / ${projectLabel}`;
}
function getAdminGovernanceDirectoryUserOptions() {
return getAdminGovernanceDirectoryItems().map((item) => ({
value: item.id,
label: `${item.display_name || item.username || item.id}${item.project_count ? ` · ${formatNumber(item.project_count)} 项目` : ""}`
}));
}
function getAdminGovernanceDirectoryProjectOptions(targetUserId) {
const directoryItem = findAdminGovernanceDirectoryItem(targetUserId);
return safeArray(directoryItem?.projects).map((item) => ({
value: item.id,
label: item.name || item.id
}));
}
function renderPolicyVersionsHtml(items, emptyText = "暂无历史版本。") {
const versions = safeArray(items);
if (!versions.length) {
return `<div class="task-item compact"><h4>还没有历史版本</h4><p>${escapeHtml(emptyText)}</p></div>`;
}
return versions.slice(0, 8).map((version) => `
<div class="task-item compact">
<h4>${escapeHtml(version.title || `版本 ${formatNumber(version.version_no || 0)}`)}</h4>
<p>${escapeHtml(version.summary || "没有补充摘要。")}</p>
<div class="task-meta">
<span class="tag blue">版本 ${escapeHtml(formatNumber(version.version_no || 0))}</span>
${version.created_at ? `<span class="tag">${escapeHtml(formatDateTime(version.created_at))}</span>` : ""}
${version.rollback_from_version_id ? `<span class="tag orange">回滚生成</span>` : ""}
</div>
</div>
`).join("");
}
async function loadPolicyVersions(url) {
const payload = await storyforgeFetch(url).catch(() => ({ items: [] }));
const items = safeArray(payload?.items || payload);
return { items, count: Number(payload?.count || items.length) };
}
function renderPolicyAuditFeed(items, emptyText = "还没有策略变更记录。") {
const records = safeArray(items);
if (!records.length) {
return `<div class="task-item compact"><h4>还没有治理动作</h4><p>${escapeHtml(emptyText)}</p></div>`;
}
return records.slice(0, 10).map((item) => {
const scopeKind = item.scope_kind || item.scope?.scope_kind || "";
const platform = item.platform || item.scope?.platform || "";
const version = item.version || {};
const details = item.details || {};
const rollbackId = version.rollback_from_version_id || details.rollback_to_version_id || "";
return `
<div class="task-item compact">
<h4>${escapeHtml(item.summary || version.title || item.action_key || "策略变更")}</h4>
<p>${escapeHtml(version.summary || version.reason || "当前记录没有补充摘要。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(policyScopeTagLabel(scopeKind, platform))}</span>
${version.version_no ? `<span class="tag">版本 ${escapeHtml(formatNumber(version.version_no || 0))}</span>` : ""}
${item.action_key ? `<span class="tag">${escapeHtml(item.action_key)}</span>` : ""}
${platform ? `<span class="tag">${escapeHtml(platformLabel(platform))}</span>` : ""}
${rollbackId ? `<span class="tag orange">回滚动作</span>` : ""}
${item.created_at ? `<span class="tag">${escapeHtml(formatDateTime(item.created_at))}</span>` : ""}
</div>
</div>
`;
}).join("");
}
function buildPolicyVersionOptions(history) {
return safeArray(history?.items).map((item) => ({
value: item.id,
label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}`
}));
}
function openUserGlobalPolicyAction() {
const project = requireSelectedProject();
const bundle = appState.userGlobalPolicy || {};
const current = bundle.current_version || {};
openActionModal({
title: "编辑我的全局策略",
description: "这层策略只影响你自己,会优先被主 Agent 读取,再决定是否下发到各个平台 Agent。",
submitLabel: "保存全局策略",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, "你还没有发布自己的全局策略,当前会沿用系统默认和平台默认。") },
{ name: "title", label: "策略标题", value: current.title || bundle.scope?.title || "用户全局策略", placeholder: "例如:创业内容增长策略" },
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚这层策略主要在约束什么、优化什么" },
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"tone\":{\"style\":\"analytical\"}}" },
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:用户要求首页动作更聚焦,默认走分析型语气" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/oneliner/governance/user/global", {
method: "PUT",
body: {
project_id: project.id,
title: values.title || "用户全局策略",
summary: values.summary || "",
policy: parsePolicyJsonField(values.policyJson, "全局策略 JSON"),
reason: values.reason || ""
}
});
appState.userGlobalPolicy = saved;
await loadAgentControlSurfaces(project.id);
rememberAction("我的全局策略已保存", `已发布版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
function openUserPlatformPolicyAction(platform) {
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
const project = requireSelectedProject();
const bundle = appState.userCurrentPlatformPolicy || {};
const current = bundle.current_version || {};
openActionModal({
title: `编辑 ${platformLabel(normalizedPlatform)} 平台策略`,
description: "这层策略只作用于你当前项目下的单个平台,会覆盖你的全局策略,但不会影响其他平台。",
submitLabel: "保存平台策略",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, "你还没有发布单平台策略,当前会沿用全局策略和系统默认。") },
{ name: "title", label: "策略标题", value: current.title || `${platformLabel(normalizedPlatform)} 用户平台策略`, placeholder: "例如:抖音对标拆解策略" },
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚这个平台的特殊规则和工作方式" },
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"actions\":{\"max_cards\":1}}" },
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:抖音只保留 1 条首页动作,优先高分作品拆解" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(normalizedPlatform)}`, {
method: "PUT",
body: {
project_id: project.id,
title: values.title || `${platformLabel(normalizedPlatform)} 用户平台策略`,
summary: values.summary || "",
policy: parsePolicyJsonField(values.policyJson, "平台策略 JSON"),
reason: values.reason || ""
}
});
appState.userCurrentPlatformPolicy = saved;
await loadAgentControlSurfaces(project.id);
rememberAction(`${platformLabel(normalizedPlatform)} 平台策略已保存`, `已发布版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
async function openUserGlobalPolicyHistoryAction() {
const project = requireSelectedProject();
const history = await loadPolicyVersions(`/v2/oneliner/governance/user/global/versions?project_id=${encodeURIComponent(project.id)}`);
const selectedVersionId = history.items[0]?.id || "";
const versionOptions = buildPolicyVersionOptions(history);
openActionModal({
title: "我的全局策略历史",
description: "查看你自己的全局策略版本,并从历史里选择一个版本回滚。回滚不会改旧记录,而是会生成一个新的生效版本。",
submitLabel: "回滚到所选版本",
hideSubmit: !selectedVersionId,
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(appState.userGlobalPolicy || {}, "你还没有发布自己的全局策略。") },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, "你的全局策略还没有历史版本。") },
...(selectedVersionId ? [
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: versionOptions },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到更稳妥的首页动作和语气策略" }
] : [])
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/oneliner/governance/user/global/rollback", {
method: "POST",
body: {
project_id: project.id,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.userGlobalPolicy = saved;
await loadAgentControlSurfaces(project.id);
rememberAction("我的全局策略已回滚", `已生成回滚版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
async function openUserPlatformPolicyHistoryAction(platform) {
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
const project = requireSelectedProject();
const history = await loadPolicyVersions(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(normalizedPlatform)}/versions?project_id=${encodeURIComponent(project.id)}`);
const selectedVersionId = history.items[0]?.id || "";
const versionOptions = buildPolicyVersionOptions(history);
openActionModal({
title: `${platformLabel(normalizedPlatform)} 平台策略历史`,
description: "查看该平台的个人策略版本,并从历史里选择一个版本回滚。回滚只影响当前平台,不会改动其他平台。",
submitLabel: "回滚到所选版本",
hideSubmit: !selectedVersionId,
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(appState.userCurrentPlatformPolicy || {}, `你还没有发布 ${platformLabel(normalizedPlatform)} 平台策略。`) },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, `${platformLabel(normalizedPlatform)} 还没有历史版本。`) },
...(selectedVersionId ? [
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: versionOptions },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到更适合这个平台的拆解方式" }
] : [])
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(normalizedPlatform)}/rollback`, {
method: "POST",
body: {
project_id: project.id,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.userCurrentPlatformPolicy = saved;
await loadAgentControlSurfaces(project.id);
rememberAction(`${platformLabel(normalizedPlatform)} 平台策略已回滚`, `已生成回滚版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
function openSystemMainPolicyAction() {
const projectId = getOneLinerProjectId();
const bundle = appState.adminSystemMainPolicy || {};
const current = bundle.current_version || {};
openActionModal({
title: "编辑系统主 Agent 策略",
description: "这是所有用户共享的系统级主 Agent 基座能力,后续用户层和管理员覆盖都会叠加在它上面。",
submitLabel: "保存系统策略",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, "系统主 Agent 还没有系统默认策略。") },
{ name: "title", label: "策略标题", value: current.title || bundle.scope?.title || "系统主 Agent 策略", placeholder: "例如StoryForge 主 Agent 默认策略" },
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚当前系统主 Agent 主要服务的方向和约束" },
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"homepage\":{\"focus\":\"ops\"}}" },
{ name: "reason", label: "发布原因", type: "textarea", rows: 3, value: "", placeholder: "例如:更新市场节奏后,需要调整首页推荐和调度逻辑" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent", {
method: "PUT",
body: {
title: values.title || "系统主 Agent 策略",
summary: values.summary || "",
policy: parsePolicyJsonField(values.policyJson, "系统策略 JSON"),
reason: values.reason || ""
}
});
appState.adminSystemMainPolicy = saved;
await loadAgentControlSurfaces(projectId);
rememberAction("系统主 Agent 策略已保存", `已发布版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
function openSystemPlatformPolicyAction(platform) {
const normalizedPlatform = normalizePlatformValue(platform, "douyin");
const projectId = getOneLinerProjectId();
const bundle = safeArray(appState.adminSystemPlatformPolicies).find((item) => item?.scope?.platform === normalizedPlatform) || {};
const current = bundle.current_version || {};
openActionModal({
title: `编辑 ${platformLabel(normalizedPlatform)} 系统平台策略`,
description: "这是所有用户共享的系统级平台默认策略,用户自己的平台偏好会在这层之上覆盖。",
submitLabel: "保存平台默认策略",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, `当前 ${platformLabel(normalizedPlatform)} 还没有系统平台默认策略。`) },
{ name: "title", label: "策略标题", value: current.title || `${platformLabel(normalizedPlatform)} 系统平台策略`, placeholder: "例如:抖音系统平台策略" },
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚这个平台默认遵循的拆解与执行逻辑" },
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"douyin\":{\"benchmark_mode\":\"strict\"}}" },
{ name: "reason", label: "发布原因", type: "textarea", rows: 3, value: "", placeholder: "例如:平台节奏变化,需要调整系统默认方法论" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}`, {
method: "PUT",
body: {
title: values.title || `${platformLabel(normalizedPlatform)} 系统平台策略`,
summary: values.summary || "",
policy: parsePolicyJsonField(values.policyJson, "平台默认策略 JSON"),
reason: values.reason || ""
}
});
appState.adminSystemPlatformPolicies = safeArray(appState.adminSystemPlatformPolicies)
.filter((item) => item?.scope?.platform !== normalizedPlatform)
.concat(saved)
.sort((a, b) => String(a?.scope?.platform || "").localeCompare(String(b?.scope?.platform || "")));
await loadAgentControlSurfaces(projectId);
rememberAction(`${platformLabel(normalizedPlatform)} 系统平台策略已保存`, `已发布版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
async function openAdminOverrideTargetAction() {
const current = getAdminOverrideTargetState();
const directoryItems = getAdminGovernanceDirectoryItems();
openActionModal({
title: "选择管理员覆盖目标",
description: "先选中要覆盖的用户、项目和平台,再去编辑覆盖策略或查看历史。",
submitLabel: "保存目标",
fields: [
{ type: "html", label: "当前目标", html: renderPolicyVersionSummary(appState.adminOverridePolicy || {}, `当前目标是 ${formatAdminGovernanceTargetLabel(current)}`) },
{ name: "targetUserId", label: "目标用户", type: "select", value: current.targetUserId, options: getAdminGovernanceDirectoryUserOptions() },
{ name: "targetProjectId", label: "目标项目", type: "select", value: current.targetProjectId, options: [{ value: "", label: "用户全局" }, ...getAdminGovernanceDirectoryProjectOptions(current.targetUserId)] },
{ name: "platform", label: "平台", type: "select", value: current.platform, options: getPlatformOptions() },
{ type: "html", label: "目录提示", html: directoryItems.length ? `<div class="task-item compact"><h4>可选目标</h4><p>当前目录里有 ${escapeHtml(formatNumber(directoryItems.length))} 位已审核账号。</p></div>` : `<div class="task-item compact"><h4>目录为空</h4><p>后端还没有返回可选账号。</p></div>` }
],
onOpen: () => {
const userSelect = document.querySelector('[data-action-field="targetUserId"]');
if (!(userSelect instanceof HTMLSelectElement)) return;
syncAdminOverrideProjectOptions(userSelect.value, current.targetProjectId);
userSelect.addEventListener("change", () => {
syncAdminOverrideProjectOptions(userSelect.value, "");
});
},
onSubmit: async (values) => {
appState.adminOverrideTarget = {
targetUserId: String(values.targetUserId || ""),
targetProjectId: String(values.targetProjectId || ""),
platform: normalizePlatformValue(values.platform || "douyin", "douyin")
};
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("管理员覆盖目标已更新", `当前目标已切换到 ${formatAdminGovernanceTargetLabel(appState.adminOverrideTarget)}`, "green");
renderAll();
}
});
}
function syncAdminOverrideProjectOptions(targetUserId, preferredProjectId = "") {
const projectSelect = document.querySelector('[data-action-field="targetProjectId"]');
if (!(projectSelect instanceof HTMLSelectElement)) return;
const options = [{ value: "", label: "用户全局" }, ...getAdminGovernanceDirectoryProjectOptions(targetUserId)];
projectSelect.innerHTML = options.map((option) => `
<option value="${escapeHtml(option.value)}">${escapeHtml(option.label)}</option>
`).join("");
const normalizedPreferred = String(preferredProjectId ?? "");
const nextValue = normalizedPreferred === "" || options.some((option) => String(option.value) === normalizedPreferred)
? normalizedPreferred
: String(options[0]?.value || "");
projectSelect.value = nextValue;
}
function openAdminOverridePolicyAction() {
const target = getAdminOverrideTargetState();
const bundle = appState.adminOverridePolicy || {};
const current = bundle.current_version || {};
openActionModal({
title: "编辑管理员覆盖策略",
description: "这层策略只作用于当前选中的目标,会叠加在用户策略和系统默认之上。",
submitLabel: "保存覆盖策略",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, `当前还没有为 ${formatAdminGovernanceTargetLabel(target)} 发布覆盖策略。`) },
{ name: "title", label: "策略标题", value: current.title || `管理员覆盖:${formatAdminGovernanceTargetLabel(target)}`, placeholder: "例如:重点账号短期放量覆盖" },
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚这层覆盖是为了什么目标" },
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"guardrails\":{\"require_admin_review\":true}}" },
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:对该账号/项目临时放宽首页动作数量" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/overrides", {
method: "POST",
body: {
target_user_id: target.targetUserId,
target_project_id: target.targetProjectId,
platform: target.platform,
title: values.title || `管理员覆盖:${formatAdminGovernanceTargetLabel(target)}`,
summary: values.summary || "",
policy: parsePolicyJsonField(values.policyJson, "管理员覆盖策略 JSON"),
reason: values.reason || ""
}
});
appState.adminOverridePolicy = saved;
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("管理员覆盖策略已保存", `已为 ${formatAdminGovernanceTargetLabel(target)} 发布版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
async function openAdminOverrideHistoryAction() {
const target = getAdminOverrideTargetState();
const history = await loadPolicyVersions(`/v2/admin/oneliner/governance/overrides/versions?target_user_id=${encodeURIComponent(target.targetUserId)}&target_project_id=${encodeURIComponent(target.targetProjectId)}&platform=${encodeURIComponent(target.platform)}`);
const selectedVersionId = history.items[0]?.id || "";
openActionModal({
title: "管理员覆盖历史",
description: "查看当前目标的管理员覆盖版本,并从历史里选择一个版本回滚。",
submitLabel: "回滚到所选版本",
fields: [
{ type: "html", label: "当前目标", html: renderPolicyVersionSummary(appState.adminOverridePolicy || {}, `当前查看的是 ${formatAdminGovernanceTargetLabel(target)} 的覆盖历史。`) },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, "当前目标还没有历史版本。") },
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:这版覆盖太激进,需要恢复到上一版" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/overrides/rollback", {
method: "POST",
body: {
target_user_id: target.targetUserId,
target_project_id: target.targetProjectId,
platform: target.platform,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.adminOverridePolicy = saved;
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("管理员覆盖已回滚", `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
async function openSystemMainPolicyHistoryAction() {
const history = await loadPolicyVersions("/v2/admin/oneliner/governance/system/main-agent/versions");
const selectedVersionId = history.items[0]?.id || "";
openActionModal({
title: "系统主 Agent 历史",
description: "查看系统主 Agent 的历史版本,并选择某个版本回滚。",
submitLabel: "回滚到所选版本",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(appState.adminSystemMainPolicy || {}, "系统主 Agent 还没有历史版本。") },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, "系统主 Agent 还没有历史版本。") },
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版系统主 Agent 策略" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent/rollback", {
method: "POST",
body: {
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.adminSystemMainPolicy = saved;
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("系统主 Agent 已回滚", `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
async function openSystemPlatformPolicyHistoryAction(platform) {
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
const history = await loadPolicyVersions(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}/versions`);
const selectedVersionId = history.items[0]?.id || "";
const bundle = safeArray(appState.adminSystemPlatformPolicies).find((item) => item?.scope?.platform === normalizedPlatform) || {};
openActionModal({
title: `${platformLabel(normalizedPlatform)} 系统平台历史`,
description: "查看该平台的系统默认策略历史,并选择某个版本回滚。",
submitLabel: "回滚到所选版本",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, `当前 ${platformLabel(normalizedPlatform)} 还没有系统平台历史版本。`) },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, `${platformLabel(normalizedPlatform)} 还没有历史版本。`) },
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版平台默认方法论" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}/rollback`, {
method: "POST",
body: {
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.adminSystemPlatformPolicies = safeArray(appState.adminSystemPlatformPolicies)
.filter((item) => item?.scope?.platform !== normalizedPlatform)
.concat(saved)
.sort((a, b) => String(a?.scope?.platform || "").localeCompare(String(b?.scope?.platform || "")));
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction(`${platformLabel(normalizedPlatform)} 系统平台策略已回滚`, `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
function openPlatformAgentProfileAction(platform) {
const project = requireSelectedProject();
const agents = safeArray(appState.platformAgents);
const current = agents.find((item) => item.platform === platform) || {};
const assistants = getAssistantOptions(project.id);
openActionModal({
title: `配置 ${platformLabel(platform)} Agent`,
description: "给这个平台绑定自己的执行 Agent并补充任务目标和方法论定位。",
submitLabel: "保存平台 Agent",
fields: [
{ name: "assistantId", label: "绑定执行 Agent", type: "select", value: current.assistant_id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "name", label: "名称", value: current.name || `${platformLabel(platform)} Agent`, placeholder: "例如:快手增长 Agent" },
{ name: "mission", label: "任务目标", type: "textarea", rows: 4, value: current.mission || "", placeholder: "例如:沉淀快手平台的开场结构、停留逻辑和转化方法论" },
{ name: "notes", label: "补充说明", type: "textarea", rows: 4, value: current.notes || "", placeholder: "例如:优先观察短句节奏、直播切片和成交句式" },
{ name: "status", label: "状态", type: "select", value: current.status || "active", options: [{ value: "active", label: "启用" }, { value: "draft", label: "草稿" }, { value: "paused", label: "暂停" }] }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/profile`, {
method: "PUT",
body: {
project_id: project.id,
assistant_id: values.assistantId || "",
name: values.name || `${platformLabel(platform)} Agent`,
mission: values.mission || "",
notes: values.notes || "",
status: values.status || "active",
config: {
self_optimize: true,
tenant_scoped_memory: true,
ui_escalation_via_oneliner: true
}
}
});
appState.platformAgents = safeArray(appState.platformAgents).filter((item) => item.platform !== platform).concat(saved).sort((a, b) => String(a.platform).localeCompare(String(b.platform)));
rememberAction("平台 Agent 已保存", `已更新 ${platformLabel(platform)} Agent。`, "green", saved);
renderAll();
}
});
}
function openPlatformAgentMemoryAction(platform) {
const project = requireSelectedProject();
openActionModal({
title: `补充 ${platformLabel(platform)} Agent 记忆`,
description: "把当前阶段已经验证有效的平台方法、结论或注意事项沉淀为租户级长期记忆。",
submitLabel: "保存记忆",
fields: [
{ name: "memoryKey", label: "记忆键", value: "lesson.current", placeholder: "例如hook.pattern.v1" },
{ name: "title", label: "标题", placeholder: "例如:快手直播切片更吃冲突前置" },
{ name: "summary", label: "摘要", type: "textarea", rows: 4, placeholder: "写清楚这条记忆的结论和适用场景" }
],
onSubmit: async (values) => {
if (!values.memoryKey?.trim()) throw new Error("请填写记忆键");
if (!values.summary?.trim()) throw new Error("请填写记忆摘要");
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/memories`, {
method: "POST",
body: {
project_id: project.id,
memory_key: values.memoryKey.trim(),
title: values.title || values.memoryKey.trim(),
summary: values.summary.trim(),
subject_type: "project",
subject_id: project.id,
details: {
source: "manual-ui",
platform,
captured_at: new Date().toISOString()
},
confidence: 0.82
}
});
rememberAction("平台记忆已保存", `已把这条方法沉淀到 ${platformLabel(platform)} Agent 记忆中。`, "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openPlatformAgentSkillAction(platform) {
const project = requireSelectedProject();
openActionModal({
title: `补充 ${platformLabel(platform)} Agent 技能`,
description: "把子 Agent 当前阶段验证过的方法论固化成可复用技能,并保留测试规范。",
submitLabel: "保存技能",
fields: [
{ name: "skillKey", label: "技能键", value: "skill.current", placeholder: "例如crawler.profile.dom.v2" },
{ name: "name", label: "名称", placeholder: "例如:主页结构适配技能" },
{ name: "status", label: "状态", type: "select", value: "validated", options: [{ value: "draft", label: "草稿" }, { value: "validated", label: "已验证" }, { value: "paused", label: "暂停" }] },
{ name: "method", label: "方法摘要", type: "textarea", rows: 4, placeholder: "写清楚当前方法是怎么拿到结果的" },
{ name: "testSpec", label: "验收标准", type: "textarea", rows: 4, placeholder: "例如:主页抓取成功率 >= 95%,作品标题和发布时间都齐全" }
],
onSubmit: async (values) => {
if (!values.skillKey?.trim()) throw new Error("请填写技能键");
if (!values.name?.trim()) throw new Error("请填写技能名称");
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/skills`, {
method: "POST",
body: {
project_id: project.id,
skill_key: values.skillKey.trim(),
name: values.name.trim(),
status: values.status || "validated",
method: { summary: values.method || "" },
test_spec: { summary: values.testSpec || "" },
last_result: { source: "manual-ui" },
success_count: 1,
failure_count: 0,
last_score: 0.9
}
});
rememberAction("平台技能已保存", `已把方法固化到 ${platformLabel(platform)} Agent 技能中。`, "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
async function openPlatformAgentDetailAction(platform) {
const project = requireSelectedProject();
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
const profile = safeArray(appState.platformAgents).find((item) => item.platform === normalizedPlatform) || null;
if (!profile) {
alert("没有找到这个平台 Agent。");
return;
}
const [memoriesPayload, skillsPayload] = await Promise.all([
storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/memories?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] })),
storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] }))
]);
const memories = safeArray(memoriesPayload?.items || memoriesPayload).slice(0, 6);
const skills = safeArray(skillsPayload?.items || skillsPayload).slice(0, 6);
const skillVersionEntries = await Promise.all(
skills.map(async (item) => {
if (!backendSupports("/v2/platform-agents/{platform}/skills/{skill_id}/versions")) {
return [item.id, []];
}
const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(item.id)}/versions?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] }));
return [item.id, safeArray(payload?.items || payload).slice(0, 3)];
})
);
const skillVersions = Object.fromEntries(skillVersionEntries);
openActionModal({
title: `${platformLabel(normalizedPlatform)} Agent 详情`,
description: "查看当前平台 Agent 最近沉淀的记忆、技能和就绪度。",
hideSubmit: true,
fields: [
{
type: "html",
label: "详情",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(profile.name || `${platformLabel(normalizedPlatform)} Agent`)}</h4>
<p>${escapeHtml(profile.mission || profile.notes || "暂无任务目标说明")}</p>
<div class="task-meta">
<span class="tag ${profile.status === "active" ? "green" : "blue"}">${escapeHtml(profile.status || "draft")}</span>
${profile.readiness_label ? `<span class="tag ${profile.readiness_score >= 75 ? "green" : profile.readiness_score >= 50 ? "blue" : "orange"}">${escapeHtml(profile.readiness_label)} ${escapeHtml(formatNumber(profile.readiness_score || 0))}</span>` : ""}
<span class="tag">${escapeHtml(profile.assistant?.name || "未绑执行 Agent")}</span>
</div>
</div>
<div class="two-col" style="margin-top:12px;">
<div>
<div class="panel-subtitle">最近记忆</div>
<div class="list" style="margin-top:8px;">
${memories.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.memory_key || "未命名")}</h4>
<p>${escapeHtml(item.summary || "暂无摘要")}</p>
<div class="task-meta"><span class="tag blue">${escapeHtml(item.memory_key || "memory")}</span><span class="tag">${escapeHtml(formatNumber(item.confidence || 0))}</span></div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有平台记忆</h4><p>先把这段时间验证有效的方法沉淀进来。</p></div>`}
</div>
</div>
<div>
<div class="panel-subtitle">最近技能</div>
<div class="list" style="margin-top:8px;">
${skills.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.name || item.skill_key || "未命名")}</h4>
<p>${escapeHtml(item.test_spec?.summary || item.method?.summary || "暂无方法摘要")}</p>
<div class="task-meta">
<span class="tag ${item.status === "validated" ? "green" : item.status === "needs_revision" ? "orange" : "blue"}">${escapeHtml(item.status || "draft")}</span>
<span class="tag blue">得分 ${escapeHtml(formatNumber(item.last_score || 0))}</span>
<span class="tag clickable-tag" data-action="review-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-accepted="true">验收通过</span>
<span class="tag clickable-tag" data-action="review-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-accepted="false">标记待优化</span>
</div>
${safeArray(skillVersions[item.id]).length ? `
<div class="task-meta" style="margin-top:8px;">
${safeArray(skillVersions[item.id]).map((version, index) => `
<span class="tag ${index === 0 ? "blue" : ""}${index !== 0 ? "" : ""} ${index === 0 ? "" : "clickable-tag"}"
${index === 0 ? "" : `data-action="rollback-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-version-id="${escapeHtml(version.id || "")}"`}>
${escapeHtml(`v${formatNumber(version.version_no || 0)} · ${version.snapshot_reason || "snapshot"}`)}
</span>
`).join("")}
</div>
` : ""}
</div>
`).join("") || `<div class="task-item compact"><h4>还没有平台技能</h4><p>等子 Agent 跑出稳定结果后,把方法固化成技能。</p></div>`}
</div>
</div>
</div>
<div class="task-meta" style="margin-top:12px;">
<span class="tag clickable-tag" data-action="run-oneliner-action" data-executor-key="platform-self-check" data-platform="${escapeHtml(normalizedPlatform)}">运行平台自检</span>
<span class="tag clickable-tag" data-action="open-platform-agent-profile" data-platform="${escapeHtml(normalizedPlatform)}">编辑配置</span>
<span class="tag clickable-tag" data-action="open-platform-agent-memory" data-platform="${escapeHtml(normalizedPlatform)}">继续补记忆</span>
<span class="tag clickable-tag" data-action="open-platform-agent-skill" data-platform="${escapeHtml(normalizedPlatform)}">继续补技能</span>
<span class="tag clickable-tag" data-action="handoff-to-main-agent" data-platform="${escapeHtml(normalizedPlatform)}" data-source-screen="playbook" data-source-action-key="platform-agent-handoff" data-intent-key="custom" data-title="继续完善平台 Agent" data-goal="继续完善平台 Agent" data-summary="让主 Agent 结合当前平台记忆和技能,给出下一步执行计划。" data-plan-steps="${escapeHtml(JSON.stringify(["读取当前平台 Agent 配置", "检查记忆与技能缺口", "生成下一步执行计划"]))}">交给主 Agent 继续</span>
</div>
</div>
`
}
]
});
}
function openPlatformSkillReviewAction(platform, skillId, accepted) {
const project = requireSelectedProject();
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
const profile = safeArray(appState.platformAgents).find((item) => item.platform === normalizedPlatform) || null;
const skill = safeArray(profile?.recent_skill ? [profile.recent_skill] : [])
.concat([])
.find((item) => item.id === skillId)
|| null;
openActionModal({
title: accepted ? "验收平台技能" : "标记技能待优化",
description: accepted
? `把这条 ${platformLabel(normalizedPlatform)} 技能标记为当前可复用的方法。`
: `这条 ${platformLabel(normalizedPlatform)} 技能暂时不通过,要求继续优化。`,
submitLabel: accepted ? "确认通过" : "确认待优化",
fields: [
{ name: "summary", label: "结论摘要", placeholder: accepted ? "例如:当前抓取结果和验收数据一致,可固化成技能" : "例如:账号匹配不稳定,需要继续优化抓取方式" },
{ name: "reviewNotes", label: "审计备注", type: "textarea", rows: 4, value: skill?.last_result?.review_notes || "", placeholder: "写清楚为什么通过或退回" },
{ name: "score", label: "得分", type: "number", value: accepted ? 0.9 : 0.45, min: 0, max: 1, step: 0.05 }
],
onSubmit: async (values) => {
if (!backendSupports("/v2/platform-agents/{platform}/skills/{skill_id}/review")) {
throw new Error("当前后端还没有接入平台技能验收接口。");
}
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(skillId)}/review`, {
method: "POST",
body: {
project_id: project.id,
accepted,
score: Number(values.score || (accepted ? 0.9 : 0.45)),
summary: values.summary || "",
review_notes: values.reviewNotes || ""
}
});
rememberAction(
accepted ? "平台技能已通过" : "平台技能待优化",
`技能「${saved.name || saved.skill_key || skillId}」已更新为 ${saved.status || (accepted ? "validated" : "needs_revision")}`,
accepted ? "green" : "orange",
saved
);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openPlatformSkillRollbackAction(platform, skillId, versionId) {
const project = requireSelectedProject();
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
openActionModal({
title: "回滚平台技能",
description: "把当前技能回退到旧版本,并保留新的回滚快照,方便继续追踪。",
submitLabel: "确认回滚",
fields: [
{
name: "summary",
label: "回滚说明",
type: "html",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(platformLabel(normalizedPlatform))} 技能回滚</h4>
<p>${escapeHtml(`将 skill ${skillId} 回滚到版本 ${versionId}`)}</p>
</div>
</div>
`
}
],
onSubmit: async () => {
const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(skillId)}/rollback`, {
method: "POST",
body: {
project_id: project.id,
version_id: versionId
}
});
rememberAction("技能已回滚", `已回滚到版本 ${payload.rollback_from_version?.version_no || "指定版本"}`, "green", payload);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openActionRegistryEditAction(actionKey) {
const project = requireSelectedProject();
const actionDef = safeArray(appState.onelinerActionRegistry).find((item) => item.action_key === actionKey) || null;
if (!actionDef) {
alert("没有找到这条动作定义。");
return;
}
openActionModal({
title: "编辑 OneLiner 动作",
description: "在租户范围内控制动作名称、说明、开关和少量配置。",
submitLabel: "保存动作",
fields: [
{ name: "label", label: "动作名称", value: actionDef.label || "" },
{ name: "description", label: "动作说明", type: "textarea", rows: 4, value: actionDef.description || "" },
{ name: "status", label: "状态", type: "select", value: actionDef.status || "enabled", options: [{ value: "enabled", label: "启用" }, { value: "disabled", label: "禁用" }] },
{ name: "configJson", label: "配置 JSON", type: "textarea", rows: 5, value: JSON.stringify(actionDef.config || {}, null, 2) }
],
onSubmit: async (values) => {
let config = {};
if (String(values.configJson || "").trim()) {
config = JSON.parse(values.configJson);
}
const saved = await storyforgeFetch(`/v2/oneliner/action-registry/${encodeURIComponent(actionKey)}?project_id=${encodeURIComponent(project.id)}`, {
method: "PUT",
body: {
label: values.label || "",
description: values.description || "",
category: actionDef.category || "custom",
status: values.status || "enabled",
config
}
});
rememberAction("动作已更新", `OneLiner 动作「${saved.label || saved.action_key}」已保存。`, "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openTenantQuotaAction() {
const project = requireSelectedProject();
const quota = appState.tenantQuota || {};
openActionModal({
title: "编辑租户额度",
description: "当前额度按租户 + 项目隔离,用于商业化预算、动作配额和存储保护。",
submitLabel: "保存额度",
fields: [
{ name: "enabled", label: "启用额度保护", type: "checkbox", value: quota.enabled !== false },
{ name: "monthlyBudgetCents", label: "月预算(分)", type: "number", value: quota.monthly_budget_cents || 0, min: 0 },
{ name: "storageLimitBytes", label: "存储上限(字节)", type: "number", value: quota.storage_limit_bytes || 0, min: 0 },
{ name: "analysisQuota", label: "分析配额", type: "number", value: quota.analysis_quota || 0, min: 0 },
{ name: "copyQuota", label: "文案配额", type: "number", value: quota.copy_quota || 0, min: 0 },
{ name: "aiVideoQuota", label: "AI 视频配额", type: "number", value: quota.ai_video_quota || 0, min: 0 },
{ name: "realCutQuota", label: "实拍剪辑配额", type: "number", value: quota.real_cut_quota || 0, min: 0 },
{ name: "recorderQuota", label: "录制配额", type: "number", value: quota.recorder_quota || 0, min: 0 }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(project.id)}`, {
method: "PUT",
body: {
enabled: Boolean(values.enabled),
monthly_budget_cents: Number(values.monthlyBudgetCents || 0),
storage_limit_bytes: Number(values.storageLimitBytes || 0),
analysis_quota: Number(values.analysisQuota || 0),
copy_quota: Number(values.copyQuota || 0),
ai_video_quota: Number(values.aiVideoQuota || 0),
real_cut_quota: Number(values.realCutQuota || 0),
recorder_quota: Number(values.recorderQuota || 0),
config: quota.config || {}
}
});
rememberAction("租户额度已更新", "当前项目的预算与配额已经保存。", "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openCreateAssistantAction() {
const project = requireSelectedProject();
const kbOptions = getKnowledgeBaseOptions(project.id);
const modelOptions = getModelOptions();
openActionModal({
title: "创建 Agent",
description: "先定义用途、平台与目标,再让 Agent 学习内容。",
submitLabel: "创建 Agent",
fields: [
{ name: "projectId", label: "归属项目", type: "select", value: project.id, options: getProjectOptions() },
{ name: "name", label: "名称", placeholder: "例如:创业成交助手" },
{ name: "description", label: "说明", placeholder: "例如:服务创业 IP 与成交型短视频" },
{ name: "goal", label: "生成目标", placeholder: "例如:输出创业口播、对标拆解和成交文案" },
{ name: "systemPrompt", label: "系统提示词", type: "textarea", rows: 5, placeholder: "可选,不填则后续再补" },
{ name: "knowledgeBaseId", label: "默认知识库", type: "select", value: kbOptions[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...kbOptions] },
{ name: "modelProfileId", label: "主模型", type: "select", value: modelOptions.find((item) => item.value === safeArray(appState.dashboard?.model_profiles).find((m) => m.is_default)?.id)?.value || modelOptions[0]?.value || "", options: modelOptions }
],
onSubmit: async (values) => {
if (!values.name?.trim()) throw new Error("请填写 Agent 名称");
const projectId = values.projectId || project.id;
const assistant = await storyforgeFetch("/v2/assistants", {
method: "POST",
body: {
project_id: projectId,
name: values.name.trim(),
description: values.description || "",
generation_goal: values.goal || "",
system_prompt: values.systemPrompt || "",
knowledge_base_ids: values.knowledgeBaseId ? [values.knowledgeBaseId] : [],
model_profile_id: values.modelProfileId || ""
}
});
appState.selectedAssistantId = assistant.id;
rememberAction("Agent 已创建", `已创建 Agent「${assistant.name}」。`, "green", assistant);
await bootstrap();
}
});
}
function openEditAssistantAction(assistantId = "") {
const assistant = safeArray(appState.dashboard?.assistants).find((item) => item.id === assistantId) || getSelectedAssistant();
if (!assistant) {
alert("请先选择一个 Agent");
return;
}
const modelOptions = getModelOptions();
openActionModal({
title: "编辑 Agent",
description: "更新当前 Agent 的名称、目标和主模型,不会影响已完成任务。",
submitLabel: "保存 Agent",
fields: [
{ name: "name", label: "名称", value: assistant.name || "", placeholder: "例如:创业成交助手" },
{ name: "description", label: "说明", value: assistant.description || "", placeholder: "例如:服务创业 IP 与成交型短视频" },
{ name: "goal", label: "生成目标", value: assistant.generation_goal || "", placeholder: "例如:输出创业口播、对标拆解和成交文案" },
{ name: "systemPrompt", label: "系统提示词", type: "textarea", rows: 5, value: assistant.system_prompt || "", placeholder: "可选,不填则后续再补" },
{ name: "modelProfileId", label: "主模型", type: "select", value: assistant.model_profile_id || modelOptions[0]?.value || "", options: modelOptions }
],
onSubmit: async (values) => {
if (!values.name?.trim()) throw new Error("请填写 Agent 名称");
const updated = await storyforgeFetch(`/v2/assistants/${encodeURIComponent(assistant.id)}`, {
method: "PATCH",
body: {
name: values.name.trim(),
description: values.description || "",
generation_goal: values.goal || "",
system_prompt: values.systemPrompt || "",
model_profile_id: values.modelProfileId || ""
}
});
appState.selectedAssistantId = updated.id;
rememberAction("Agent 已更新", `已更新 Agent「${updated.name}」。`, "green", updated);
await bootstrap();
}
});
}
function openAnalyzeSelectedAccountAction() {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) {
rememberAction("当前平台待接入", gate.reason, "orange");
renderAll();
return;
}
const platform = gate.platform;
const analyzePath = getWorkbenchRoute(platform, "analyzeAccount", account.id);
if (!analyzePath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
openActionModal({
title: "分析当前对标账号",
description: "从商业化和内容运营角度重跑一次账号分析。",
submitLabel: "开始分析",
fields: [
{ name: "maxVideos", label: "纳入分析作品数", type: "number", value: 6, min: 3, max: 20 },
{ name: "extraFocus", label: "额外关注点", type: "textarea", rows: 4, placeholder: "例如:更关注商业化承接与私域转化" },
{ name: "autoAnalyzeTopVideos", label: "分析后自动补高分作品", type: "checkbox", value: true },
{ name: "topVideoCount", label: "高分作品分析数", type: "number", value: 4, min: 1, max: 10 }
],
onSubmit: async (values) => {
const result = await storyforgeFetch(analyzePath, {
method: "POST",
body: {
model_profile_ids: [],
linked_account_ids: [],
include_linked_accounts: true,
include_recent_similar_candidates: true,
max_videos: Number(values.maxVideos || 6),
extra_focus: values.extraFocus || "",
temperature: 0.35,
auto_analyze_top_videos: Boolean(values.autoAnalyzeTopVideos),
top_video_analysis_count: Number(values.topVideoCount || 4)
}
});
const summary = result?.suggestions?.[0]?.parsed_json?.executive_summary || result?.suggestions?.[0]?.suggestion_text || "已生成新的账号分析。";
rememberAction("对标账号分析完成", brief(summary, 120), "green", result);
await loadPlatformAccount(platform, account.id);
renderAll();
}
});
}
function openAnalyzeTopVideosAction() {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) {
rememberAction("当前平台待接入", gate.reason, "orange");
renderAll();
return;
}
const platform = gate.platform;
const analyzePath = getWorkbenchRoute(platform, "analyzeTopVideos", account.id);
if (!analyzePath || !backendSupports(`/v2/${platform}/accounts/{account_id}/videos/analyze-top`)) {
rememberAction("当前后端暂不支持", "这套 live collector 还没有接入高分作品批量分析。", "orange");
renderAll();
return;
}
openActionModal({
title: "分析高分作品",
description: "对当前对标账号的高分作品批量补分析。",
submitLabel: "开始分析",
fields: [
{ name: "topVideoCount", label: "分析作品数", type: "number", value: 5, min: 1, max: 12 },
{ name: "minScore", label: "最低分阈值", type: "number", value: 45, min: 0, max: 100 }
],
onSubmit: async (values) => {
const result = await storyforgeFetch(analyzePath, {
method: "POST",
body: {
model_profile_id: "",
top_video_count: Number(values.topVideoCount || 5),
min_score: Number(values.minScore || 45),
temperature: 0.25
}
});
rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result);
await loadPlatformAccount(platform, account.id);
renderAll();
}
});
}
function openSimilaritySearchAction() {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) {
rememberAction("当前平台待接入", gate.reason, "orange");
renderAll();
return;
}
const platform = gate.platform;
const createPath = getWorkbenchRoute(platform, "similarSearches");
if (!createPath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
openActionModal({
title: "查相似账号",
description: "让 Agent 基于当前账号画像找更多可借鉴对象。",
submitLabel: "开始查找",
fields: [
{ name: "maxCandidates", label: "最多候选数", type: "number", value: 8, min: 3, max: 20 },
{ name: "extraRequirements", label: "额外要求", type: "textarea", rows: 4, placeholder: "例如:优先找创业成交类、口播结构强的账号" }
],
onSubmit: async (values) => {
const created = await storyforgeFetch(createPath, {
method: "POST",
body: {
source_account_id: account.id,
candidate_urls: [],
seed_linked_accounts: true,
search_public_pages: true,
model_profile_id: "",
max_candidates: Number(values.maxCandidates || 8),
extra_requirements: values.extraRequirements || ""
}
});
const searchId = created.id || created.search_id;
const detailPath = searchId ? getWorkbenchRoute(platform, "similarSearchDetail", searchId) : "";
const detail = searchId
? await storyforgeFetch(detailPath)
: created;
appState.lastSimilaritySearch = detail;
rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail);
await loadPlatformAccount(platform, account.id);
renderAll();
}
});
}
function openBenchmarkLinkAction(defaults = {}) {
const account = requireSelectedAccountRow();
const gate = getAccountWorkbenchGate(account);
if (!gate.enabled) {
rememberAction("当前平台待接入", gate.reason, "orange");
renderAll();
return;
}
const platform = gate.platform;
const benchmarkPath = getWorkbenchRoute(platform, "benchmarkLinks", account.id);
if (!benchmarkPath) {
rememberAction("当前平台待接入", getPendingWorkbenchReason(platform), "orange");
renderAll();
return;
}
const options = safeArray(appState.accounts)
.filter((item) => item.id !== account.id)
.map((item) => ({ value: item.id, label: getAccountName(item) || item.id }));
const candidate = typeof defaults.candidateIndex === "number"
? safeArray(appState.lastSimilaritySearch?.candidates)[defaults.candidateIndex] || null
: null;
openActionModal({
title: "保存对标关系",
description: "把当前账号和另一个账号关联成对标关系,便于后续持续跟踪。",
submitLabel: "保存关系",
fields: [
{ name: "targetAccountId", label: "目标账号", type: "select", value: defaults.targetAccountId || candidate?.candidate_account_id || options[0]?.value || "", options: [{ value: "", label: "仅保存主页链接" }, ...options] },
{ name: "targetProfileUrl", label: "目标主页链接", type: "url", value: defaults.targetProfileUrl || candidate?.candidate_profile_url || "", placeholder: "没有本地账号时可直接保存主页链接" },
{ name: "relationType", label: "关系类型", type: "select", value: "benchmark", options: [
{ value: "benchmark", label: "对标" },
{ value: "learn", label: "学习" },
{ value: "watch", label: "跟踪" }
] },
{ name: "note", label: "备注", value: defaults.note || brief(candidate?.rationale_text || "", 120), placeholder: "例如:开场结构很强,适合持续跟踪" }
],
onSubmit: async (values) => {
if (!values.targetAccountId && !values.targetProfileUrl?.trim()) throw new Error("请先选择一个目标账号或填写主页链接");
const result = await storyforgeFetch(benchmarkPath, {
method: "POST",
body: {
target_account_ids: values.targetAccountId ? [values.targetAccountId] : [],
target_profile_urls: values.targetAccountId ? [] : [values.targetProfileUrl.trim()],
relation_type: values.relationType || "benchmark",
note: values.note || "",
search_id: appState.lastSimilaritySearch?.id || ""
}
});
if (candidate) {
markSavedCandidate(candidate, result.links);
} else if (appState.selectedWorkspace) {
appState.selectedWorkspace = {
...appState.selectedWorkspace,
linked_accounts: safeArray(result.links)
};
}
rememberAction("对标关系已保存", "当前账号的对标关系已更新。", "green");
renderAll();
}
});
}
async function scanAdminOpsAction() {
if (!isSuperAdmin()) throw new Error("只有平台管理者才能调用运维 Agent。");
setBusy(true, "运维 Agent 正在扫描故障事件...");
try {
const payload = await storyforgeFetch("/v2/admin/ops/incidents/scan", {
method: "POST",
body: {}
});
rememberAction("运维扫描已完成", `本轮共归集 ${formatNumber(payload.count)} 条故障事件。`, payload.count ? "orange" : "green", payload);
await loadAgentControlSurfaces(getOneLinerProjectId());
} finally {
setBusy(false, "");
renderAll();
}
}
function openAdminIncidentReviewAction(incidentId) {
if (!isSuperAdmin()) {
alert("只有平台管理者才能审计处理故障事件。");
return;
}
const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId);
if (!incident) {
alert("没有找到这条故障事件。");
return;
}
openActionModal({
title: "审计处理故障事件",
description: "这里代表管理员侧审计 Agent 的放行/退回动作。",
submitLabel: "保存审计结果",
fields: [
{
name: "summary",
label: "事件摘要",
type: "html",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(incident.title)}</h4>
<p>${escapeHtml(incident.summary || "暂无摘要")}</p>
<div class="task-meta">
<span class="tag ${incident.severity === "error" ? "red" : "orange"}">${escapeHtml(incident.severity || "warn")}</span>
<span class="tag">${escapeHtml(incident.status || "open")}</span>
${incident.source_type ? `<span class="tag">${escapeHtml(incident.source_type)}</span>` : ""}
${incident.tenant_user_id ? `<span class="tag">租户 ${escapeHtml(brief(incident.tenant_user_id, 12))}</span>` : ""}
</div>
<div class="task-meta" style="margin-top:10px;">
${incident.source_type === "job" ? actionTag("看任务详情", "open-job-detail", `data-job-id="${escapeHtml(incident.source_id || "")}"`) : ""}
${incident.source_type === "integration" ? actionTag("去自动流程", "goto-automation") : ""}
${incident.tenant_project_id ? actionTag("去生产中心", "goto-production") : ""}
<span class="tag clickable-tag" data-action="scan-admin-ops">重新扫描</span>
</div>
</div>
</div>
`
},
{ name: "status", label: "处理状态", type: "select", value: incident.status || "reviewed", options: [{ value: "reviewed", label: "已审阅" }, { value: "watching", label: "继续观察" }, { value: "resolved", label: "已解决" }, { value: "rejected", label: "驳回修复方案" }] },
{ name: "reviewNotes", label: "审计备注", type: "textarea", rows: 5, value: incident.review_notes || "", placeholder: "写清楚为什么放行、退回或继续观察" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/ops/incidents/${encodeURIComponent(incident.id)}`, {
method: "PATCH",
body: {
status: values.status || "reviewed",
review_notes: values.reviewNotes || ""
}
});
rememberAction("审计结果已保存", `事件「${saved.title}」已更新为 ${saved.status}`, "green", saved);
await loadAgentControlSurfaces(getOneLinerProjectId());
renderAll();
}
});
}
function openAdminRepairPlanAction(incidentId) {
if (!isSuperAdmin()) {
alert("只有平台管理者才能生成修复计划。");
return;
}
const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId);
if (!incident) {
alert("没有找到这条故障事件。");
return;
}
openActionModal({
title: "生成修复计划",
description: "让运维 Agent 先生成一版 repair plan再由审计 Agent 决定是否放行。",
submitLabel: "生成计划",
fields: [
{ name: "scope", label: "计划范围", type: "select", value: "plan", options: [{ value: "plan", label: "标准计划" }, { value: "hotfix", label: "热修建议" }, { value: "watch", label: "仅观察" }] },
{ name: "notes", label: "附加说明", type: "textarea", rows: 4, placeholder: "例如:优先验证 cutvideo 上传链,不要动核心代码" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/ops/incidents/${encodeURIComponent(incidentId)}/repair-plan`, {
method: "POST",
body: {
incident_id: incidentId,
scope: values.scope || "plan",
notes: values.notes || ""
}
});
rememberAction("修复计划已生成", `已为事件「${incident.title}」生成 repair plan。`, "green", saved);
await loadAgentControlSurfaces(getOneLinerProjectId());
renderAll();
}
});
}
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.adminFixRuns.length ? appState.adminFixRuns : appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId);
if (!run) {
alert("没有找到这条修复计划。");
return;
}
openActionModal({
title: "审计修复计划",
description: "审计 Agent 只做放行、驳回或继续观察,不会直接让用户一句话改核心代码。",
submitLabel: "保存审计",
fields: [
{
name: "summary",
label: "计划摘要",
type: "html",
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" 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>
`
},
{ name: "reviewStatus", label: "审计状态", type: "select", value: run.audit_status || "approved", options: [{ value: "approved", label: "通过" }, { value: "watching", label: "继续观察" }, { value: "rejected", label: "驳回" }] },
{ name: "reviewNotes", label: "审计备注", type: "textarea", rows: 4, value: run.review_notes || "", placeholder: "写清楚为什么通过、驳回或继续观察" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/ops/fix-runs/${encodeURIComponent(runId)}/audit`, {
method: "POST",
body: {
review_status: values.reviewStatus || "approved",
review_notes: values.reviewNotes || ""
}
});
rememberAction("修复计划已审计", `修复计划 ${runId} 已更新为 ${saved.audit_status || values.reviewStatus}`, "green", saved);
await loadAgentControlSurfaces(getOneLinerProjectId());
renderAll();
}
});
}
function openJobDetailAction(jobId) {
if (!jobId) return;
setBusy(true, "正在加载任务详情...");
loadJobDetail(jobId)
.then(({ job, events, childJobs }) => {
const artifacts = JSON.stringify(job.artifacts || {}, null, 2);
const result = JSON.stringify(job.result || {}, null, 2);
const previewLinks = getJobPreviewLinks(job);
const recovery = getJobRecoverability(job);
openActionModal({
title: job.title || "任务详情",
description: `状态:${job.status || "-"} · 类型:${job.line_type || job.source_type || "-"}`,
hideSubmit: true,
fields: [
{
type: "html",
label: "任务摘要",
html: `
<div class="detail-grid">
<div class="mini-card"><small>任务 ID</small><strong>${escapeHtml(job.id)}</strong></div>
<div class="mini-card"><small>状态</small><strong>${escapeHtml(job.status || "-")}</strong></div>
<div class="mini-card"><small>链路</small><strong>${escapeHtml(job.line_type || "-")}</strong></div>
<div class="mini-card"><small>创建时间</small><strong>${escapeHtml(formatDateTime(job.created_at))}</strong></div>
</div>
`
},
{
type: "html",
label: "事件时间线",
html: `
<div class="list">
${safeArray(events).slice(-6).map((event) => `
<div class="task-item compact">
<h4>${escapeHtml(event.event_type || "event")}</h4>
<p>${escapeHtml(brief(event.message || JSON.stringify(event.payload || {}), 120))}</p>
</div>
`).join("") || `<div class="task-item compact"><h4>暂无事件</h4><p>当前任务还没有可显示的事件。</p></div>`}
</div>
`
},
{
type: "html",
label: "结果预览",
html: `
<div class="list">
${previewLinks.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.label.replace(/^result\./, "").replace(/^artifacts\./, ""))}</h4>
<p>${escapeHtml(item.url)}</p>
<div class="task-meta">
<a class="tag" href="${escapeHtml(item.url)}" target="_blank" rel="noreferrer">打开结果</a>
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>暂无外部结果链接</h4><p>当前先保留 artifacts 和 result 原始数据供查看。</p></div>`}
</div>
`
},
{
type: "html",
label: "下一步动作",
html: `
<div class="task-meta">
${job.status === "failed" ? actionTag(
recovery.actionLabel,
recovery.recoverable ? "recover-job" : recovery.actionKey,
`data-job-id="${escapeHtml(job.id)}"`,
{ disabledReason: recovery.recoverable ? "" : recovery.reason, title: recovery.reason }
) : ""}
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "继续做 AI 视频") : ""}
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "继续做实拍剪辑") : ""}
${actionTag("用摘要写文案", "job-to-generate-copy", `data-job-id="${escapeHtml(job.id)}"`)}
</div>
`
},
{
type: "html",
label: "恢复判断",
html: `
<div class="task-item compact">
<h4>${escapeHtml(recovery.label)}</h4>
<p>${escapeHtml(recovery.reason)}</p>
<div class="task-meta">
<span class="tag ${recovery.recoverable ? "green" : recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.actionLabel)}</span>
${recovery.sourceJobId ? `<span class="tag">源任务 ${escapeHtml(brief(recovery.sourceJobId, 12))}</span>` : ""}
${recovery.quotaGuard?.blocked ? `<span class="tag red">额度拦截</span>` : ""}
</div>
</div>
`
},
{
type: "html",
label: "子任务",
html: `
<div class="list">
${safeArray(childJobs).slice(0, 6).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title || item.id)}</h4>
<p>${escapeHtml(brief(item.style_summary || item.transcript_text || item.error || "暂无摘要", 96))}</p>
<div class="task-meta">
<span class="tag ${statusTone(item.status)}">${escapeHtml(item.status || "-")}</span>
<span class="tag">${escapeHtml(item.line_type || "-")}</span>
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(item.id)}">看详情</span>
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>暂无子任务</h4><p>当前任务还没有派生出下一层任务。</p></div>`}
</div>
`
},
{ type: "textarea", name: "artifactsReadonly", label: "Artifacts", value: artifacts, rows: 8 },
{ type: "textarea", name: "resultReadonly", label: "Result", value: result, rows: 8 }
]
});
document.querySelector('[data-action-field="artifactsReadonly"]')?.setAttribute("readonly", "readonly");
document.querySelector('[data-action-field="resultReadonly"]')?.setAttribute("readonly", "readonly");
})
.catch((error) => {
presentActionFailure(error, "加载任务详情失败");
})
.finally(() => {
setBusy(false, "");
});
}
function openRecoverJobAction(jobId) {
const job = safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === jobId)
|| (appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null);
if (!job) {
alert("没有找到这条任务。");
return;
}
const recovery = getJobRecoverability(job);
if (!recovery.recoverable) {
presentActionFailure(new Error(recovery.reason || "当前任务暂不支持恢复"), "当前任务暂不可恢复");
return;
}
openActionModal({
title: "恢复失败任务",
description: recovery.reason,
submitLabel: recovery.actionLabel,
fields: [
{
type: "html",
label: "恢复摘要",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(job.title || job.id)}</h4>
<p>${escapeHtml(brief(job.error || recovery.reason || "任务失败,可重新发起。", 120))}</p>
<div class="task-meta">
<span class="tag ${recovery.recoverable ? "green" : recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.label)}</span>
<span class="tag">${escapeHtml(job.line_type || job.source_type || "analysis")}</span>
${recovery.sourceJobId ? `<span class="tag">源任务 ${escapeHtml(brief(recovery.sourceJobId, 12))}</span>` : ""}
</div>
</div>
${recovery.quotaGuard?.blocked ? `
<div class="task-item compact">
<h4>额度提醒</h4>
<p>${escapeHtml(recovery.quotaGuard.reason)}</p>
</div>
` : ""}
</div>
`
},
{ name: "note", label: "恢复备注", type: "textarea", rows: 4, value: "", placeholder: "可写明为什么需要恢复,或补充额外背景" }
],
onSubmit: async (values) => {
setBusy(true, "正在恢复任务...");
try {
const result = await recoverJobAction(job.id, { mode: "single", job, user_feedback: values.note?.trim() || "" });
rememberAction("任务已恢复", `${job.title || job.id} 已重新发起,下一步可以去生产中心继续跟进。`, "green", result);
await bootstrap();
} finally {
setBusy(false, "");
}
}
});
}
function openBatchRecoverJobsAction() {
const failedJobs = getRecoverableFailedJobs();
const selectable = failedJobs.filter((item) => item.recovery.recoverable);
if (!selectable.length) {
rememberAction("没有可恢复任务", "当前没有可直接恢复的失败任务。", "orange");
renderAll();
return;
}
openActionModal({
title: "批量恢复失败任务",
description: "只会选中当前可自动恢复的失败任务;需要人工补素材或补额度的任务会保留在列表里。",
submitLabel: "批量恢复",
fields: [
{
type: "html",
label: "批量摘要",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>可恢复 ${escapeHtml(formatNumber(selectable.length))} 条</h4>
<p>${escapeHtml(`最近失败任务共 ${formatNumber(failedJobs.length)} 条,其中 ${formatNumber(failedJobs.length - selectable.length)} 条需要人工处理。`)}</p>
<div class="task-meta">
<span class="tag green">${escapeHtml(formatNumber(selectable.length))} 可恢复</span>
<span class="tag orange">${escapeHtml(formatNumber(failedJobs.length - selectable.length))} 需人工</span>
</div>
</div>
</div>
`
},
...selectable.map(({ job, recovery }, index) => ({
name: `recoverJob_${index}`,
label: `${job.title || job.id} · ${recovery.reason}`,
type: "checkbox",
value: true
})),
{ name: "note", label: "批量恢复备注", type: "textarea", rows: 4, value: "", placeholder: "可写明这次批量恢复的背景或处理策略" }
],
onSubmit: async (values) => {
const chosen = selectable.filter((item, index) => values[`recoverJob_${index}`]).map((item) => item.job);
if (!chosen.length) throw new Error("请至少选择一个可恢复任务");
setBusy(true, "正在批量恢复任务...");
const successes = [];
const failures = [];
try {
for (const job of chosen) {
try {
const result = await recoverJobAction(job.id, { mode: "batch", job });
successes.push({ job, result });
} catch (error) {
failures.push({ job, error: formatActionErrorMessage(error, "恢复失败") });
}
}
rememberAction(
"批量恢复已完成",
failures.length
? `已恢复 ${successes.length} 条,另有 ${failures.length} 条失败。`
: `已恢复 ${successes.length} 条失败任务。`,
failures.length ? "orange" : "green",
{ successes, failures }
);
if (values.note?.trim()) {
recordRecoveryEvent({
id: `batch_${Date.now()}`,
account_id: appState.session?.account?.id || "",
project_id: appState.selectedProjectId || "",
job_title: "批量恢复",
job_line_type: "batch",
job_source_type: "production",
job_status: "completed",
action_key: "batch-recover-jobs",
mode: "batch",
summary: `批量恢复 ${successes.length} 条任务`,
reason: values.note.trim(),
result_label: failures.length ? "部分成功" : "全部成功",
result_reason: failures.length ? `失败 ${failures.length}` : "全部成功"
});
}
await bootstrap();
} finally {
setBusy(false, "");
}
}
});
}
function openGenerateCopyAction(defaults = {}) {
const assistant = getSelectedAssistant() || requireSelectedAssistant();
const sourceJob = defaults.sourceJob || null;
openActionModal({
title: "生成文案",
description: "用当前 Agent 和知识库生成一版短视频文案。",
submitLabel: "开始生成",
fields: [
{ name: "brief", label: "创作需求", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "例如:给创业者写一条 60 字内的短视频开场文案" },
{ name: "platform", label: "平台", type: "select", value: normalizePlatformValue(defaults.platform || "douyin"), options: getPlatformOptions() },
{ name: "audience", label: "受众", value: "创业者" },
{ name: "extraRequirements", label: "额外要求", placeholder: "例如:强结论开头,结尾带 CTA" }
],
onSubmit: async (values) => {
if (!values.brief?.trim()) throw new Error("请填写创作需求");
const result = await storyforgeFetch(`/v2/assistants/${encodeURIComponent(assistant.id)}/generate`, {
method: "POST",
body: {
brief: values.brief.trim(),
platform: platformLabel(values.platform || "douyin"),
audience: values.audience || "创业者",
extra_requirements: values.extraRequirements || "",
knowledge_base_ids: safeArray(assistant.knowledge_base_ids)
}
});
appState.lastGeneratedCopy = {
assistantId: assistant.id,
assistantName: assistant.name,
prompt: values.brief.trim(),
content: extractGeneratedCopy(result),
usedDocuments: safeArray(result.used_documents).slice(0, 3)
};
rememberAction("文案生成完成", `已用 Agent「${assistant.name}」生成一版文案。`, "green", result);
renderAll();
}
});
}
function openCreateAiVideoAction(defaults = {}) {
const guard = getPipelineGuard("aiVideo");
if (!guard.enabled) {
presentActionFailure(new Error(guard.reason), "AI 视频暂不可用");
return;
}
const project = requireSelectedProject();
const assistant = getSelectedAssistant();
const kb = getProjectKnowledgeBases(project.id)[0];
const sourceJob = defaults.sourceJob || null;
openActionModal({
title: "创建 AI 视频任务",
description: "输入 brief 后,直接触发 AI 视频链。",
submitLabel: "开始生产",
fields: [
{ name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · AI 视频` : ""), placeholder: "例如:创业口播 AI 视频测试" },
{ name: "brief", label: "视频 brief", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "写明主题、风格、镜头和目标受众" },
{ name: "sourceJobId", label: "关联源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联" }, ...getCompletedJobOptions()] },
{ name: "style", label: "风格", value: defaults.style || "realistic" },
{ name: "shots", label: "镜头数", type: "number", value: defaults.shots || 4, min: 1, max: 12 },
{ name: "duration", label: "单镜头秒数", type: "number", value: defaults.duration || 5, min: 3, max: 12 }
],
onSubmit: async (values) => {
if (!values.title?.trim()) throw new Error("请填写任务标题");
if (!values.brief?.trim()) throw new Error("请填写视频 brief");
const job = await storyforgeFetch("/v2/pipelines/ai-video", {
method: "POST",
body: {
project_id: project.id,
assistant_id: assistant?.id || "",
knowledge_base_id: kb?.id || "",
source_job_id: values.sourceJobId || "",
title: values.title.trim(),
brief: values.brief.trim(),
style: values.style || "realistic",
shots: Number(values.shots || 4),
duration: Number(values.duration || 5)
}
});
rememberAction("AI 视频任务已创建", `已创建任务 ${job.title || job.id}`, "blue", job);
await bootstrap();
}
});
}
function openCreateRealCutAction(defaults = {}) {
const guard = getPipelineGuard("realCut");
if (!guard.enabled) {
presentActionFailure(new Error(guard.reason), "实拍剪辑暂不可用");
return;
}
const project = requireSelectedProject();
const sourceJob = defaults.sourceJob || null;
openActionModal({
title: "创建实拍剪辑任务",
description: "基于已完成的源任务,把素材发到 cutvideo。",
submitLabel: "开始剪辑",
fields: [
{ name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · 实拍剪辑` : ""), placeholder: "例如:创业素材粗剪" },
{ name: "sourceJobId", label: "源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || getCompletedJobOptions()[0]?.value || "", options: getCompletedJobOptions() },
{ name: "targetDurationSec", label: "目标时长(秒)", type: "number", value: defaults.targetDurationSec || 60, min: 10, max: 300 },
{ name: "aspectRatio", label: "画幅", value: defaults.aspectRatio || "9:16" },
{ name: "objective", label: "目标", type: "textarea", rows: 4, value: defaults.objective || "", placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" }
],
onSubmit: async (values) => {
if (!values.title?.trim()) throw new Error("请填写任务标题");
if (!values.sourceJobId) throw new Error("请先选择一个已完成的源任务");
const job = await storyforgeFetch("/v2/pipelines/real-cut", {
method: "POST",
body: {
project_id: project.id,
title: values.title.trim(),
source_job_id: values.sourceJobId,
target_duration_sec: Number(values.targetDurationSec || 60),
target_aspect_ratio: values.aspectRatio || "9:16",
objective: values.objective || "保留高信息密度片段,输出适合短视频平台的粗剪结果"
}
});
rememberAction("实拍剪辑任务已创建", `已创建任务 ${job.title || job.id}`, "blue", job);
await bootstrap();
}
});
}
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: "新增录制源",
description: status.reachable
? "新增的是你当前租户名下的录制源。文件访问和录制状态也只会回到你的账号视图里。"
: "当前 NAS 录制服务不可达,先检查集成健康。",
submitLabel: "保存录制源",
fields: [
{ type: "html", label: "当前租户", html: renderLiveRecorderSummaryHtml() },
{ name: "projectId", label: "归属项目", type: "select", value: project?.id || "", options: getProjectOptions() },
{ name: "assistantId", label: "关联 Agent", type: "select", value: assistants[0]?.value || "", options: [{ value: "", label: "暂不绑定" }, ...assistants] },
{ name: "platform", label: "平台", type: "select", value: "kuaishou", options: getPlatformOptions() },
{ name: "title", label: "录制名称", placeholder: "例如A 类目直播跟踪" },
{ name: "quality", label: "清晰度", type: "select", value: "原画", options: ["原画", "蓝光", "超清", "高清", "标清", "流畅"].map((item) => ({ value: item, label: item })) },
{ name: "sourceUrl", label: "直播源", type: "url", placeholder: "https://..." },
{ name: "autoStart", label: "导入后立即开始", type: "checkbox", value: true }
],
onSubmit: async (values) => {
if (!values.sourceUrl?.trim()) throw new Error("请填写直播源链接");
const saved = await storyforgeFetch("/v2/live-recorder/sources", {
method: "POST",
body: {
project_id: values.projectId || project?.id || "",
assistant_id: values.assistantId || "",
platform: normalizePlatformValue(values.platform, "kuaishou"),
source_url: values.sourceUrl.trim(),
title: values.title || "",
quality: values.quality || "原画",
enabled: true
}
});
let started = null;
if (values.autoStart) {
try {
started = await storyforgeFetch("/v2/live-recorder/recorder/start", { method: "POST", body: {} });
} catch (error) {
started = { ok: false, message: error.message };
}
}
rememberAction("直播录制已下发", "当前租户的直播源已经保存到服务端并同步到 NAS。", "green", { saved, started });
await bootstrap();
}
});
}
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) {
throw new Error("当前录像文件不存在,可能已经被移除");
}
const blob = await storyforgeFetchBlob(target.content_url);
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank", "noopener,noreferrer");
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000);
}
async function openStorageArtifactAction(fileId) {
const usage = appState.storageStatus?.tenant_usage || {};
const candidates = [
...safeArray(usage.recent_download_artifacts),
...safeArray(usage.recent_job_artifacts),
...safeArray(appState.storageStatus?.recent_files),
...safeArray(appState.storageStatus?.recent_artifacts)
];
const target = candidates.find((item) => item.id === fileId);
if (!target?.content_url) {
throw new Error("当前产物不存在,可能已经被清理");
}
const blob = await storyforgeFetchBlob(target.content_url);
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank", "noopener,noreferrer");
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000);
}
function openReviewAction(defaults = {}) {
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
const sourceJob = defaults.sourceJob || null;
const existingReview = defaults.review || null;
const metrics = existingReview?.metrics || {};
openActionModal({
title: existingReview ? "编辑复盘" : "写复盘",
description: existingReview
? "补充表现数据、判断和下一步动作,持续迭代项目策略。"
: "把完成任务写成一条可追踪复盘,后续可按项目累计。",
submitLabel: existingReview ? "保存复盘" : "创建复盘",
fields: [
{ name: "title", label: "标题", value: existingReview?.title || defaults.title || sourceJob?.title || "", placeholder: "例如:创业口播 3 月 22 日复盘" },
{ name: "sourceJobId", label: "关联任务", type: "select", value: existingReview?.source_job_id || defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联任务" }, ...getCompletedJobOptions()] },
{ name: "assistantId", label: "负责 Agent", type: "select", value: existingReview?.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "platform", label: "平台", type: "select", value: normalizePlatformValue(existingReview?.platform || defaults.platform || "douyin"), options: getPlatformOptions() },
{ name: "contentType", label: "内容类型", type: "select", value: existingReview?.content_type || "video", options: [
{ value: "video", label: "视频" },
{ value: "image_text", label: "图文" },
{ value: "live_clip", label: "直播切片" }
] },
{ name: "publishUrl", label: "发布链接", type: "url", value: existingReview?.publish_url || "", placeholder: "https://..." },
{ name: "publishedAt", label: "发布时间", value: existingReview?.published_at || "", placeholder: "2026-03-22T20:00:00+08:00" },
{ name: "playCount", label: "播放", type: "number", value: metrics.play_count || 0, min: 0 },
{ name: "likeCount", label: "点赞", type: "number", value: metrics.like_count || 0, min: 0 },
{ name: "commentCount", label: "评论", type: "number", value: metrics.comment_count || 0, min: 0 },
{ name: "shareCount", label: "分享", type: "number", value: metrics.share_count || 0, min: 0 },
{ name: "verdict", label: "结论", type: "select", value: existingReview?.verdict || "", options: [
{ value: "", label: "先不下结论" },
{ value: "worth_scaling", label: "值得放大" },
{ value: "needs_rework", label: "需要重做" },
{ value: "good_reference", label: "适合借鉴" },
{ value: "hold", label: "先观察" }
] },
{ name: "highlights", label: "亮点", type: "textarea", rows: 4, value: existingReview?.highlights || "", placeholder: "例如:开头 3 秒抓人、评论区问题很集中" },
{ name: "nextActions", label: "下一步", type: "textarea", rows: 4, value: existingReview?.next_actions || "", placeholder: "例如:保留结构,换一个细分人群再做一条" },
{ name: "notes", label: "备注", type: "textarea", rows: 4, value: existingReview?.notes || "", placeholder: "补充团队讨论、平台环境、发布时间段等信息" }
],
onSubmit: async (values) => {
if (!values.title?.trim()) throw new Error("请填写复盘标题");
const payload = {
project_id: project.id,
source_job_id: values.sourceJobId || "",
assistant_id: values.assistantId || "",
title: values.title.trim(),
platform: normalizePlatformValue(values.platform, "douyin"),
content_type: values.contentType || "video",
publish_url: values.publishUrl || "",
published_at: values.publishedAt || "",
metrics: {
play_count: Number(values.playCount || 0),
like_count: Number(values.likeCount || 0),
comment_count: Number(values.commentCount || 0),
share_count: Number(values.shareCount || 0)
},
verdict: values.verdict || "",
highlights: values.highlights || "",
next_actions: values.nextActions || "",
notes: values.notes || ""
};
const review = existingReview
? await storyforgeFetch(`/v2/reviews/${encodeURIComponent(existingReview.id)}`, {
method: "PATCH",
body: payload
})
: await storyforgeFetch("/v2/reviews", {
method: "POST",
body: payload
});
rememberAction(existingReview ? "复盘已更新" : "复盘已创建", `已保存「${review.title}」并回写到项目复盘。`, "green", review);
await bootstrap();
}
});
}
document.addEventListener("click", async (event) => {
if (event.target instanceof HTMLElement && event.target.classList.contains("oneliner-backdrop")) {
closeOneLinerPanel();
return;
}
const action = event.target.closest("[data-action]");
if (action) {
const name = action.dataset.action;
if (name === "open-mobile-sidebar") {
setMobileSidebarOpen(true);
return;
}
if (name === "close-mobile-sidebar") {
if (action.closest(".sidebar") || action.classList.contains("mobile-sidebar-backdrop")) {
setMobileSidebarOpen(false);
} else {
setMobileSidebarOpen(false);
}
return;
}
if (name === "open-auth") {
openAuthModal();
return;
}
if (name === "close-auth") {
closeAuthModal();
return;
}
if (name === "open-oneliner") {
try {
setBusy(true, "正在打开 OneLiner...");
if (appState.session) {
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else if (backendSupports("/v2/oneliner/sessions")) {
await ensureOneLinerSession();
}
}
openOneLinerPanel();
renderAll();
} finally {
setBusy(false, "");
}
return;
}
if (name === "close-oneliner") {
closeOneLinerPanel();
return;
}
if (name === "close-sheet") {
closeActionModal();
return;
}
if (name === "submit-sheet") {
await submitActionModal();
return;
}
if (name === "submit-auth") {
setBusy(true, "正在自动连接并加载...");
try {
const message = document.querySelector('[data-role="auth-message"]');
if (message) message.textContent = "";
appState.autoConnectSuppressed = false;
appState.autoConnectAttempted = false;
await ensureAutoSession({ force: true });
closeAuthModal();
await bootstrap();
} catch (error) {
const message = document.querySelector('[data-role="auth-message"]');
if (message) message.textContent = formatActionErrorMessage(error, "自动连接失败");
} finally {
setBusy(false, "");
}
return;
}
if (name === "show-disabled-reason") {
const reason = action.dataset.disabledReason || action.title || "当前动作暂不可用";
rememberAction("动作已拦截", reason, "orange");
renderAll();
alert(reason);
return;
}
if (name === "auth-refresh" || name === "refresh-data") {
setBusy(true, name === "auth-refresh" ? "正在重新自动连接..." : "正在刷新数据...");
try {
if (name === "auth-refresh") {
const message = document.querySelector('[data-role="auth-message"]');
if (message) message.textContent = "";
await refreshFromAuthModal();
} else {
await bootstrap();
}
} catch (error) {
const message = document.querySelector('[data-role="auth-message"]');
if (name === "auth-refresh" && message) {
message.textContent = formatActionErrorMessage(error, "自动连接失败");
} else {
alert("刷新数据失败: " + error.message);
}
} finally {
setBusy(false, "");
}
return;
}
if (name === "refresh-tracking") {
await refreshTrackingAccountsAction();
return;
}
if (name === "open-live-recorder") {
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;
}
if (name === "open-storage-artifact") {
await openStorageArtifactAction(action.dataset.fileId || "");
return;
}
if (name === "mark-tracking-read") {
await markTrackingDigestRead();
rememberAction("日报已标记", "当前跟踪摘要已更新为已读,下次会从新的时间点继续汇总。", "green");
await bootstrap();
return;
}
if (name === "logout-session") {
await logoutSession();
return;
}
if (name === "dismiss-main-agent-landing") {
appState.mainAgentLanding = null;
renderAll();
return;
}
if (name === "goto-discovery") {
captureMainAgentLandingContext(action, "goto-discovery");
setScreen("discovery");
return;
}
if (name === "goto-intake") {
captureMainAgentLandingContext(action, "goto-intake");
setScreen("intake");
return;
}
if (name === "goto-automation") {
captureMainAgentLandingContext(action, "goto-automation");
setScreen("automation");
return;
}
if (name === "goto-playbook") {
captureMainAgentLandingContext(action, "goto-playbook");
setScreen("playbook");
return;
}
if (name === "goto-owned") {
setScreen("owned");
return;
}
if (name === "goto-tracking") {
captureMainAgentLandingContext(action, "goto-tracking");
setScreen("tracking");
return;
}
if (name === "goto-production") {
captureMainAgentLandingContext(action, "goto-production");
setScreen("production");
return;
}
if (name === "goto-strategy") {
captureMainAgentLandingContext(action, "goto-strategy");
setScreen("strategy");
return;
}
if (name === "goto-review") {
captureMainAgentLandingContext(action, "goto-review");
setScreen("review");
return;
}
if (name === "goto-settings") {
setScreen("settings");
return;
}
if (name === "goto-admin-workbench") {
setScreen("admin-workbench");
return;
}
if (name === "select-page-tab") {
const key = action.dataset.pageTabKey;
const value = action.dataset.pageTabValue;
if (key && value) {
appState[key] = value;
renderAll();
}
return;
}
if (name === "open-import-homepage") {
openImportHomepageAction();
return;
}
if (name === "open-import-selected-account") {
openImportSelectedAccountAction();
return;
}
if (name === "open-track-selected-account") {
openTrackSelectedAccountAction();
return;
}
if (name === "refresh-tracked-account") {
await refreshTrackedAccountAction(action.dataset.trackedAccountId || "");
return;
}
if (name === "open-import-video-link") {
openImportVideoLinkAction();
return;
}
if (name === "open-import-text") {
openImportTextAction();
return;
}
if (name === "open-upload-video") {
openUploadVideoAction();
return;
}
if (name === "open-create-assistant") {
openCreateAssistantAction();
return;
}
if (name === "open-dashboard-project-switcher") {
openDashboardProjectSwitcher();
return;
}
if (name === "open-dashboard-action-reason") {
openDashboardActionReasonAction(action.dataset.dashboardActionIndex || "0");
return;
}
if (name === "select-dashboard-tab") {
appState.dashboardOverviewTab = action.dataset.dashboardTab || "project_progress";
renderAll();
return;
}
if (name === "open-oneliner-profile") {
openOneLinerProfileAction();
return;
}
if (name === "open-user-global-policy") {
openUserGlobalPolicyAction();
return;
}
if (name === "open-user-global-policy-history") {
await openUserGlobalPolicyHistoryAction();
return;
}
if (name === "open-user-platform-policy") {
openUserPlatformPolicyAction(action.dataset.platform || "");
return;
}
if (name === "open-user-platform-policy-history") {
await openUserPlatformPolicyHistoryAction(action.dataset.platform || "");
return;
}
if (name === "open-system-main-policy") {
openSystemMainPolicyAction();
return;
}
if (name === "open-system-main-policy-history") {
await openSystemMainPolicyHistoryAction();
return;
}
if (name === "open-system-platform-policy") {
openSystemPlatformPolicyAction(action.dataset.platform || "");
return;
}
if (name === "open-system-platform-policy-history") {
await openSystemPlatformPolicyHistoryAction(action.dataset.platform || "");
return;
}
if (name === "open-admin-override-target") {
await openAdminOverrideTargetAction();
return;
}
if (name === "open-admin-override-policy") {
openAdminOverridePolicyAction();
return;
}
if (name === "open-admin-override-history") {
await openAdminOverrideHistoryAction();
return;
}
if (name === "handoff-to-main-agent") {
try {
setBusy(true, "正在为主 Agent 创建执行计划...");
const payload = await createOneLinerRun({
source_screen: action.dataset.sourceScreen || appState.screen || "dashboard",
source_action_key: action.dataset.sourceActionKey || name,
title: action.dataset.title || action.textContent?.trim() || "交给主 Agent 处理",
summary: action.dataset.summary || "",
intent_key: action.dataset.intentKey || "custom",
platform: action.dataset.platform || getPreferredPlatform(),
platform_scope: action.dataset.platformScope || "single_platform",
plan_request: {
goal: action.dataset.goal || action.dataset.title || action.textContent?.trim() || "交给主 Agent 处理",
steps: parseJsonSafe(action.dataset.planSteps, []),
summary: action.dataset.summary || ""
}
});
appState.selectedOnelinerRunId = payload?.id || "";
openOneLinerPanel();
renderAll();
} catch (error) {
presentActionFailure(error, "主 Agent 接单失败");
openOneLinerPanel();
} finally {
setBusy(false, "");
}
return;
}
if (name === "open-oneliner-run-result") {
openCurrentOneLinerRunResultAction(action.dataset.runId || "");
return;
}
if (name === "confirm-oneliner-run") {
openConfirmOneLinerRunAction(action.dataset.runId || "");
return;
}
if (name === "cancel-oneliner-run") {
try {
setBusy(true, "正在取消当前任务...");
await cancelOneLinerRun(action.dataset.runId || "", "user cancelled");
} catch (error) {
presentActionFailure(error, "主 Agent 取消失败");
} finally {
setBusy(false, "");
}
return;
}
if (name === "retry-oneliner-run") {
try {
setBusy(true, "正在重新生成待确认卡...");
await retryOneLinerRun(action.dataset.runId || "", "user requested retry");
openOneLinerPanel();
} catch (error) {
presentActionFailure(error, "主 Agent 重开失败");
} finally {
setBusy(false, "");
}
return;
}
if (name === "select-oneliner-run") {
appState.selectedOnelinerRunId = action.dataset.runId || "";
renderAll();
return;
}
if (name === "select-oneliner-run-filter") {
appState.onelinerRunFilter = action.dataset.runFilter || "focus";
renderAll();
return;
}
if (name === "select-oneliner-session") {
appState.selectedOnelinerSessionId = action.dataset.sessionId || "";
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
renderAll();
return;
}
if (name === "select-assistant") {
appState.selectedAssistantId = action.dataset.assistantId || "";
rememberAction("已切换当前 Agent", `当前默认 Agent 已更新为「${getSelectedAssistant()?.name || "未选择"}」。`, "green");
renderAll();
return;
}
if (name === "open-edit-assistant") {
openEditAssistantAction(action.dataset.assistantId || "");
return;
}
if (name === "open-platform-agent-profile") {
openPlatformAgentProfileAction(action.dataset.platform || "");
return;
}
if (name === "open-platform-agent-detail") {
await openPlatformAgentDetailAction(action.dataset.platform || "");
return;
}
if (name === "open-action-registry-edit") {
openActionRegistryEditAction(action.dataset.actionKey || "");
return;
}
if (name === "open-tenant-quota") {
openTenantQuotaAction();
return;
}
if (name === "run-oneliner-action") {
setBusy(true, "OneLiner 正在执行动作...");
try {
await executeOneLinerAction(action.dataset.executorKey || "", {
platform: action.dataset.platform || "",
sessionId: action.dataset.sessionId || "",
payload: collectOneLinerActionPayload(action)
});
} catch (error) {
presentActionFailure(error, "OneLiner 动作失败");
} finally {
setBusy(false, "");
}
return;
}
if (name === "open-platform-agent-memory") {
openPlatformAgentMemoryAction(action.dataset.platform || "");
return;
}
if (name === "open-platform-agent-skill") {
openPlatformAgentSkillAction(action.dataset.platform || "");
return;
}
if (name === "review-platform-skill") {
openPlatformSkillReviewAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.accepted !== "false");
return;
}
if (name === "rollback-platform-skill") {
openPlatformSkillRollbackAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.versionId || "");
return;
}
if (name === "analyze-selected-account") {
openAnalyzeSelectedAccountAction();
return;
}
if (name === "analyze-top-videos") {
openAnalyzeTopVideosAction();
return;
}
if (name === "open-generate-copy") {
openGenerateCopyAction();
return;
}
if (name === "open-ai-video") {
openCreateAiVideoAction();
return;
}
if (name === "open-real-cut") {
openCreateRealCutAction();
return;
}
if (name === "open-create-review") {
openReviewAction();
return;
}
if (name === "open-preferred-model") {
openPreferredModelAction();
return;
}
if (name === "open-review-from-job") {
const jobId = action.dataset.jobId || "";
const fromDashboard = safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === jobId) || null;
const fromDetail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
openReviewAction({
sourceJobId: jobId,
sourceJob: fromDetail || fromDashboard,
title: (fromDetail || fromDashboard)?.title || ""
});
return;
}
if (name === "open-review-edit") {
const review = getReviewById(action.dataset.reviewId || "");
if (!review) {
alert("复盘记录不存在,请先刷新页面");
return;
}
openReviewAction({ review });
return;
}
if (name === "open-similar-search") {
openSimilaritySearchAction();
return;
}
if (name === "open-benchmark-link") {
openBenchmarkLinkAction();
return;
}
if (name === "save-candidate-benchmark") {
setBusy(true, "正在保存对标关系...");
try {
await saveCandidateAsBenchmark(action.dataset.candidateIndex || "");
} catch (error) {
presentActionFailure(error, "保存对标失败");
} finally {
setBusy(false, "");
}
return;
}
if (name === "open-job-detail") {
openJobDetailAction(action.dataset.jobId || "");
return;
}
if (name === "recover-job") {
await openRecoverJobAction(action.dataset.jobId || "");
return;
}
if (name === "batch-recover-jobs") {
await openBatchRecoverJobsAction();
return;
}
if (name === "scan-admin-ops") {
await scanAdminOpsAction();
return;
}
if (name === "review-admin-incident") {
openAdminIncidentReviewAction(action.dataset.incidentId || "");
return;
}
if (name === "open-admin-repair-plan") {
openAdminRepairPlanAction(action.dataset.incidentId || "");
return;
}
if (name === "open-admin-fix-run-audit") {
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;
closeActionModal();
openCreateAiVideoAction({ sourceJobId: jobId, sourceJob: detail });
return;
}
if (name === "job-to-real-cut") {
const jobId = action.dataset.jobId || "";
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
closeActionModal();
openCreateRealCutAction({ sourceJobId: jobId, sourceJob: detail, objective: detail ? `基于任务「${detail.title}」保留高信息密度片段,输出适合短视频平台的粗剪结果。` : "" });
return;
}
if (name === "job-to-generate-copy") {
const jobId = action.dataset.jobId || "";
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
closeActionModal();
openGenerateCopyAction({ sourceJob: detail, brief: detail ? `基于任务「${detail.title}」的结果,生成一版可发布的短视频文案。参考摘要:${getJobSeedBrief(detail)}` : "" });
return;
}
if (name === "create-project") {
await createProject();
return;
}
if (name === "select-project") {
appState.selectedProjectId = action.dataset.projectId || "";
setBusy(true, "正在切换项目视图...");
try {
if (backendSupports("/v2/storage/status")) {
await loadStorageStatus(appState.selectedProjectId || "");
} else {
appState.storageStatus = null;
}
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else {
appState.onelinerMessages = [];
}
} finally {
setBusy(false, "");
}
renderAll();
return;
}
if (name === "select-platform") {
const nextPlatform = normalizePlatformValue(action.dataset.platform || "", "");
if (!nextPlatform || nextPlatform === getCurrentPlatformValue()) {
renderAll();
return;
}
setCurrentPlatform(nextPlatform);
setBusy(true, `正在切换到${platformLabel(nextPlatform)}...`);
try {
await bootstrap();
} finally {
setBusy(false, "");
}
return;
}
if (name === "select-account") {
const accountId = action.dataset.accountId;
if (!accountId) return;
const requestToken = (appState.selectedAccountRequestToken || 0) + 1;
appState.selectedAccountRequestToken = requestToken;
setBusy(true, "正在加载对标详情...");
try {
const account = safeArray(appState.accounts).find((item) => item.id === accountId) || null;
const committed = await loadPlatformAccount(getAccountPlatform(account), accountId, requestToken);
if (committed && requestToken === appState.selectedAccountRequestToken) {
renderAll();
}
} catch (error) {
if (requestToken === appState.selectedAccountRequestToken) {
alert("加载对标详情失败: " + error.message);
}
} finally {
if (requestToken === appState.selectedAccountRequestToken) {
setBusy(false, "");
}
}
return;
}
if (name === "submit-oneliner") {
return;
}
if (name === "scroll-selected") {
document.getElementById("selected-account-anchor")?.scrollIntoView({ behavior: "smooth", block: "start" });
return;
}
rememberAction("动作待接入", `前端还没有处理动作「${name}」,当前仍可继续通过 OneLiner 对话承接。`, "orange");
renderAll();
return;
}
});
document.addEventListener("input", (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return;
if (target.dataset.action === "discovery-query") {
appState.discoveryQuery = target.value.trim();
screenMap.discovery.innerHTML = renderDiscoveryScreen();
}
});
document.addEventListener("submit", async (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
if (form.dataset.role === "auth-form") {
event.preventDefault();
setBusy(true, "正在自动连接并加载...");
try {
appState.autoConnectSuppressed = false;
appState.autoConnectAttempted = false;
await ensureAutoSession({ force: true });
closeAuthModal();
await bootstrap();
} catch (error) {
const message = document.querySelector('[data-role="auth-message"]');
if (message) message.textContent = formatActionErrorMessage(error, "自动连接失败");
} finally {
setBusy(false, "");
}
return;
}
if (form.dataset.role === "oneliner-form") {
event.preventDefault();
const input = form.querySelector('[data-role="oneliner-input"]');
const value = input instanceof HTMLTextAreaElement ? input.value.trim() : "";
if (!value) return;
setBusy(true, "OneLiner 正在拆解任务...");
try {
await submitOneLinerMessage(value);
if (input instanceof HTMLTextAreaElement) input.value = "";
openOneLinerPanel();
renderAll();
} catch (error) {
presentActionFailure(error, "OneLiner 调度失败");
openOneLinerPanel();
} finally {
setBusy(false, "");
}
}
});
navButtons.forEach((button) => {
button.addEventListener("click", () => {
const next = button.dataset.screenTarget;
setScreen(next);
});
});
window.addEventListener("hashchange", () => {
const next = getScreenFromHash();
if (next !== appState.screen) {
setScreen(next, { updateHash: false });
}
});
window.addEventListener("resize", () => {
if (window.innerWidth > 760) {
setMobileSidebarOpen(false);
}
});
ensureAuthUi();
renderAll();
bootstrap();