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,
trackingRefreshNotice: null,
reviews: [],
reviewFocusId: "",
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: [],
workspaceHydrating: false,
autoConnectAttempted: false,
autoConnectSuppressed: false,
autoConnectError: "",
dashboardActionReason: null,
busy: false,
message: "",
lastAction: null,
mainAgentLanding: null,
lastGeneratedCopy: null,
lastSimilaritySearch: null,
lastJobDetail: null,
topVideoAnalysisResults: {}
};
let PLATFORM_RUNTIME = null;
let onelinerRunPollTimer = null;
let bootstrapHydrationToken = 0;
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: "direct-create-ai-video",
jobAction: "direct-create-ai-video",
dependencies: ["n8n", "huobao"]
},
realCut: {
label: "实拍剪辑",
openAction: "direct-create-real-cut",
jobAction: "direct-create-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("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
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 renderIntakeActionContextHtml(projectId = "", assistantId = "") {
const project = safeArray(appState.dashboard?.projects).find((item) => item.id === projectId)
|| getSelectedProject();
const assistant = safeArray(appState.dashboard?.assistants).find((item) => item.id === assistantId)
|| getSelectedAssistant();
const knowledgeBase = getProjectKnowledgeBases(project?.id || "")[0] || null;
return `
这次会写入哪里
项目:${escapeHtml(project?.name || "当前项目")} · Agent:${escapeHtml(assistant?.name || "暂不绑定")} · 知识库:${escapeHtml(knowledgeBase?.name || "默认知识库")}
`;
}
function renderSourceJobContextHtml(job) {
if (!job) return "";
return `
当前承接任务
${escapeHtml(job.title || "未命名任务")} · 平台:${escapeHtml(platformLabel(job.platform || "douyin"))} · 状态:${escapeHtml(job.status || "unknown")}
`;
}
function recommendAudienceForPlatform(platform) {
const normalized = normalizePlatformValue(platform, "douyin");
if (normalized === "bilibili") return "深度内容观众";
if (normalized === "xiaohongshu") return "兴趣消费人群";
if (normalized === "kuaishou") return "成交导向受众";
if (normalized === "wechat_video") return "私域关系链用户";
return "创业者";
}
function recommendAspectRatioForPlatform(platform) {
const normalized = normalizePlatformValue(platform, "douyin");
return normalized === "bilibili" ? "16:9" : "9:16";
}
function recommendCreativeStyle(sourceJob) {
const style = String(sourceJob?.artifacts?.style || sourceJob?.style || "").trim();
if (style) return style;
if (String(sourceJob?.job_type || "").includes("review")) return "structured";
return "realistic";
}
function recommendRealCutDuration(sourceJob) {
const candidate = Number(
sourceJob?.artifacts?.target_duration_sec
|| sourceJob?.artifacts?.duration
|| sourceJob?.result?.target_duration_sec
|| 0
);
if (Number.isFinite(candidate) && candidate > 0) {
return Math.max(10, Math.min(300, Math.round(candidate)));
}
return 60;
}
function recommendAiVideoShotDuration(sourceJob) {
const candidate = Number(
sourceJob?.artifacts?.duration
|| sourceJob?.result?.duration
|| 0
);
if (Number.isFinite(candidate) && candidate > 0) {
return Math.max(3, Math.min(12, Math.round(candidate)));
}
return 5;
}
function recommendDerivativeJobTitle(sourceJob, suffix, fallback) {
const base = String(sourceJob?.title || "").trim();
if (base) return `${base} · ${suffix}`;
return fallback;
}
function recommendRealCutObjective(sourceJob) {
const briefText = brief(getJobSeedBrief(sourceJob), 48);
if (briefText && briefText !== "暂无") {
return `围绕「${briefText}」保留高信息密度片段,输出适合短视频平台的粗剪结果`;
}
return "保留高信息密度片段,输出适合短视频平台的粗剪结果";
}
function recommendLiveRecorderTitle(project, platform) {
const normalized = normalizePlatformValue(platform, "kuaishou");
const projectName = String(project?.name || "").trim();
const platformName = platformLabel(normalized);
if (projectName) return `${brief(projectName, 12)} · ${platformName}直播跟踪`;
return `${platformName}直播跟踪`;
}
function recommendLiveRecorderImportSamples(platform) {
const normalized = normalizePlatformValue(platform, "kuaishou");
if (normalized === "douyin") {
return [
"https://live.douyin.com/1234567890",
"# 关闭的源会以 # 开头",
"原画, https://live.douyin.com/9988776655, 抖音主场跟踪"
].join("\n");
}
return [
"https://live.kuaishou.com/u/abcdef",
"# 关闭的源会以 # 开头",
"高清, https://live.kuaishou.com/u/abcdef, 快手主场跟踪"
].join("\n");
}
function recommendManualIntakeTitle(project, platform, kind) {
const normalized = normalizePlatformValue(platform, "douyin");
const projectName = String(project?.name || "").trim();
const platformName = platformLabel(normalized);
const suffix = String(kind || "").trim() || "素材";
if (projectName) return `${brief(projectName, 12)} · ${platformName}${suffix}`;
return `${platformName}${suffix}`;
}
function bindManualIntakeTitleRecommendation(fields, kind, options = {}) {
const titleInput = fields.querySelector('[data-action-field="title"]');
if (!(titleInput instanceof HTMLInputElement)) return;
const projectField = options.projectField || "projectId";
const platformField = options.platformField || "platform";
const defaultPlatform = options.defaultPlatform || "douyin";
const projectSelect = fields.querySelector(`[data-action-field="${projectField}"]`);
const platformSelect = fields.querySelector(`[data-action-field="${platformField}"]`);
const sync = () => {
const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : "";
const project = safeArray(appState.dashboard?.projects).find((item) => item.id === projectId)
|| getSelectedProject()
|| null;
const platform = normalizePlatformValue(
platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(),
defaultPlatform
);
titleInput.placeholder = recommendManualIntakeTitle(project, platform, kind);
};
projectSelect?.addEventListener("change", sync);
platformSelect?.addEventListener("change", sync);
sync();
}
function bindActionContextRecommendation(fields, options = {}) {
const contextHtml = fields.querySelector('[data-action-field="context"] .sheet-html');
const assistantSelect = fields.querySelector('[data-action-field="assistantId"]');
const projectField = options.projectField === undefined ? "projectId" : options.projectField;
const projectSelect = projectField ? fields.querySelector(`[data-action-field="${projectField}"]`) : null;
const defaultAssistantId = options.defaultAssistantId || "";
const syncAssistantOptions = () => {
if (!(assistantSelect instanceof HTMLSelectElement) || !(projectSelect instanceof HTMLSelectElement)) return;
const assistants = getAssistantOptions(projectSelect.value || "");
const currentValue = assistantSelect.value || "";
const fallbackValue = assistants.some((item) => item.value === currentValue)
? currentValue
: assistants.some((item) => item.value === defaultAssistantId)
? defaultAssistantId
: assistants[0]?.value || "";
assistantSelect.innerHTML = [{ value: "", label: "暂不绑定" }, ...assistants].map((item) => `
`).join("");
assistantSelect.value = fallbackValue;
};
const sync = () => {
syncAssistantOptions();
if (!(contextHtml instanceof HTMLElement)) return;
const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : getSelectedProject()?.id || "";
const assistantId = assistantSelect instanceof HTMLSelectElement ? assistantSelect.value : defaultAssistantId;
contextHtml.innerHTML = renderIntakeActionContextHtml(projectId, assistantId);
};
projectSelect?.addEventListener("change", sync);
assistantSelect?.addEventListener("change", sync);
sync();
}
function bindAssistantSheetRecommendations(fields, options = {}) {
const contextHtml = fields.querySelector('[data-action-field="context"] .sheet-html');
const projectSelect = fields.querySelector('[data-action-field="projectId"]');
const knowledgeBaseSelect = fields.querySelector('[data-action-field="knowledgeBaseId"]');
const defaultProjectId = String(options.defaultProjectId || getSelectedProject()?.id || "").trim();
const defaultKnowledgeBaseId = String(options.defaultKnowledgeBaseId || "").trim();
const defaultAssistantId = String(options.defaultAssistantId || "").trim();
const syncKnowledgeBaseOptions = () => {
if (!(knowledgeBaseSelect instanceof HTMLSelectElement)) return;
const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : defaultProjectId;
const knowledgeBases = getKnowledgeBaseOptions(projectId);
const currentValue = knowledgeBaseSelect.value || "";
const fallbackValue = knowledgeBases.some((item) => item.value === currentValue)
? currentValue
: knowledgeBases.some((item) => item.value === defaultKnowledgeBaseId)
? defaultKnowledgeBaseId
: knowledgeBases[0]?.value || "";
knowledgeBaseSelect.innerHTML = [{ value: "", label: "暂不绑定" }, ...knowledgeBases].map((item) => `
`).join("");
knowledgeBaseSelect.value = fallbackValue;
};
const syncContext = () => {
if (!(contextHtml instanceof HTMLElement)) return;
const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : defaultProjectId;
contextHtml.innerHTML = renderIntakeActionContextHtml(projectId, defaultAssistantId);
};
const sync = () => {
syncKnowledgeBaseOptions();
syncContext();
};
projectSelect?.addEventListener("change", sync);
sync();
}
function getCompletedJobById(jobId = "") {
const normalizedId = String(jobId || "").trim();
if (!normalizedId) return null;
return safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === normalizedId)
|| (appState.lastJobDetail?.job?.id === normalizedId ? appState.lastJobDetail.job : null)
|| null;
}
function bindCreativeSourceJobRecommendations(fields, options = {}) {
const sourceJobSelect = fields.querySelector('[data-action-field="sourceJobId"]');
if (!(sourceJobSelect instanceof HTMLSelectElement)) return;
const sourceContext = fields.querySelector('[data-action-field="sourceJobContext"] .sheet-html');
const contextHtml = fields.querySelector('[data-action-field="context"] .sheet-html');
const assistantSelect = fields.querySelector('[data-action-field="assistantId"]');
const platformSelect = fields.querySelector(`[data-action-field="${options.platformField || "platform"}"]`);
const defaultPlatform = options.defaultPlatform || "douyin";
const defaultAssistantId = String(options.defaultAssistantId || "").trim();
const projectId = String(options.projectId || getSelectedProject()?.id || "").trim();
const managedFields = [];
const resolveSourceJob = () => getCompletedJobById(sourceJobSelect.value) || options.sourceJob || null;
const recommendedPlatform = (job) => normalizePlatformValue(job?.platform || getPreferredPlatform(), defaultPlatform);
const registerManagedField = (field, compute, eventName = field instanceof HTMLSelectElement ? "change" : "input") => {
if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement)) return;
field.dataset.recommendationMode = field.dataset.recommendationMode || "auto";
field.addEventListener(eventName, () => {
if (field.dataset.recommendationApplying === "1") return;
field.dataset.recommendationMode = "manual";
});
managedFields.push({ field, compute });
};
const applyManagedValue = (field, value) => {
if (value == null || value === "") return;
field.dataset.recommendationApplying = "1";
field.value = String(value);
field.dataset.recommendationMode = "auto";
delete field.dataset.recommendationApplying;
};
const titleInput = fields.querySelector('[data-action-field="title"]');
if (options.titleSuffix && titleInput instanceof HTMLInputElement) {
registerManagedField(titleInput, (job) => recommendDerivativeJobTitle(job, options.titleSuffix, options.titleFallback || ""));
}
if (platformSelect instanceof HTMLSelectElement) {
registerManagedField(platformSelect, (job) => recommendedPlatform(job), "change");
}
if (assistantSelect instanceof HTMLSelectElement) {
registerManagedField(assistantSelect, (job) => {
const preferredAssistantId = String(job?.assistant_id || defaultAssistantId).trim();
if (preferredAssistantId && safeArray(Array.from(assistantSelect.options)).some((item) => item.value === preferredAssistantId)) {
return preferredAssistantId;
}
if (defaultAssistantId && safeArray(Array.from(assistantSelect.options)).some((item) => item.value === defaultAssistantId)) {
return defaultAssistantId;
}
return assistantSelect.value || "";
}, "change");
}
const audienceInput = fields.querySelector('[data-action-field="audience"]');
if (audienceInput instanceof HTMLInputElement) {
registerManagedField(audienceInput, (job) => {
const platformValue = platformSelect instanceof HTMLSelectElement ? platformSelect.value : recommendedPlatform(job);
return recommendAudienceForPlatform(platformValue);
});
}
const styleInput = fields.querySelector('[data-action-field="style"]');
if (styleInput instanceof HTMLInputElement) {
registerManagedField(styleInput, (job) => recommendCreativeStyle(job));
}
const aspectRatioField = fields.querySelector('[data-action-field="aspectRatio"]');
if (aspectRatioField instanceof HTMLInputElement || aspectRatioField instanceof HTMLSelectElement) {
registerManagedField(
aspectRatioField,
(job) => {
const platformValue = platformSelect instanceof HTMLSelectElement ? platformSelect.value : recommendedPlatform(job);
return job?.artifacts?.aspect_ratio || recommendAspectRatioForPlatform(platformValue);
},
aspectRatioField instanceof HTMLSelectElement ? "change" : "input"
);
}
const durationField = fields.querySelector('[data-action-field="duration"]');
if (durationField instanceof HTMLInputElement) {
registerManagedField(durationField, (job) => recommendAiVideoShotDuration(job));
}
const targetDurationField = fields.querySelector('[data-action-field="targetDurationSec"]');
if (targetDurationField instanceof HTMLInputElement) {
registerManagedField(targetDurationField, (job) => recommendRealCutDuration(job));
}
const objectiveField = fields.querySelector('[data-action-field="objective"]');
if (objectiveField instanceof HTMLTextAreaElement) {
registerManagedField(objectiveField, (job) => recommendRealCutObjective(job));
}
const sync = () => {
const sourceJob = resolveSourceJob();
if (sourceContext instanceof HTMLElement) {
sourceContext.innerHTML = sourceJob ? renderSourceJobContextHtml(sourceJob) : "";
sourceContext.parentElement?.classList.toggle("hidden", !sourceJob);
}
managedFields.forEach(({ field, compute }) => {
if (field.dataset.recommendationMode === "manual") return;
applyManagedValue(field, compute(sourceJob));
});
if (contextHtml instanceof HTMLElement) {
const assistantId = assistantSelect instanceof HTMLSelectElement ? assistantSelect.value : defaultAssistantId;
contextHtml.innerHTML = renderIntakeActionContextHtml(projectId, assistantId);
}
};
sourceJobSelect.addEventListener("change", sync);
platformSelect?.addEventListener("change", sync);
assistantSelect?.addEventListener("change", sync);
sync();
}
function bindLiveRecorderSheetRecommendations(fields, options = {}) {
const projectSelect = fields.querySelector('[data-action-field="projectId"]');
const platformSelect = fields.querySelector('[data-action-field="platform"]');
const titleInput = fields.querySelector('[data-action-field="title"]');
const assistantSelect = fields.querySelector('[data-action-field="assistantId"]');
const rawTextarea = fields.querySelector('[data-action-field="raw"]');
const defaultPlatform = options.defaultPlatform || "kuaishou";
const syncAssistants = () => {
if (!(assistantSelect instanceof HTMLSelectElement) || !(projectSelect instanceof HTMLSelectElement)) return;
const projectId = projectSelect.value || "";
const assistants = getAssistantOptions(projectId);
const currentValue = assistantSelect.value || "";
assistantSelect.innerHTML = [
{ value: "", label: "暂不绑定" },
...assistants
].map((item) => `
`).join("");
const stillExists = safeArray(assistants).some((item) => item.value === currentValue);
if (!stillExists && currentValue) {
assistantSelect.value = "";
}
};
const syncTitle = () => {
if (!(titleInput instanceof HTMLInputElement)) return;
const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : "";
const project = safeArray(appState.dashboard?.projects).find((item) => item.id === projectId)
|| getSelectedProject()
|| null;
const platform = normalizePlatformValue(
platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(),
defaultPlatform
);
titleInput.placeholder = recommendLiveRecorderTitle(project, platform);
};
const syncRawSamples = () => {
if (!(rawTextarea instanceof HTMLTextAreaElement)) return;
if (rawTextarea.dataset.recommendationMode === "manual") return;
const platform = normalizePlatformValue(
platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(),
defaultPlatform
);
rawTextarea.dataset.recommendationApplying = "1";
rawTextarea.value = recommendLiveRecorderImportSamples(platform);
rawTextarea.dataset.recommendationMode = "auto";
delete rawTextarea.dataset.recommendationApplying;
};
rawTextarea?.addEventListener("input", () => {
if (rawTextarea.dataset.recommendationApplying === "1") return;
rawTextarea.dataset.recommendationMode = "manual";
});
const sync = () => {
syncAssistants();
syncTitle();
syncRawSamples();
};
projectSelect?.addEventListener("change", sync);
platformSelect?.addEventListener("change", sync);
sync();
}
function bindLiveRecorderSheetRecommendations(fields, options = {}) {
const projectSelect = fields.querySelector('[data-action-field="projectId"]');
const assistantSelect = fields.querySelector('[data-action-field="assistantId"]');
const platformSelect = fields.querySelector('[data-action-field="platform"]');
const titleInput = fields.querySelector('[data-action-field="title"]');
const rawTextarea = fields.querySelector('[data-action-field="raw"]');
const contextHtml = fields.querySelector('[data-action-field="context"] .sheet-html');
const defaultPlatform = options.defaultPlatform || "kuaishou";
const defaultAssistantId = options.defaultAssistantId || "";
const keepAutoValue = (field, value) => {
if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) || !String(value || "").trim()) return;
field.placeholder = String(value);
if (field.dataset.recommendationMode === "manual") return;
field.dataset.recommendationApplying = "1";
field.value = String(value);
field.dataset.recommendationMode = "auto";
delete field.dataset.recommendationApplying;
};
const bindManualEscape = (field) => {
if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) return;
field.dataset.recommendationMode = field.dataset.recommendationMode || "auto";
field.addEventListener("input", () => {
if (field.dataset.recommendationApplying === "1") return;
field.dataset.recommendationMode = "manual";
});
};
bindManualEscape(titleInput);
bindManualEscape(rawTextarea);
const getProject = () => {
const projectId = projectSelect instanceof HTMLSelectElement ? projectSelect.value : "";
return safeArray(appState.dashboard?.projects).find((item) => item.id === projectId)
|| getSelectedProject()
|| null;
};
const getPlatform = () => normalizePlatformValue(
platformSelect instanceof HTMLSelectElement ? platformSelect.value : getPreferredPlatform(),
defaultPlatform
);
const syncAssistantOptions = () => {
if (!(assistantSelect instanceof HTMLSelectElement) || !(projectSelect instanceof HTMLSelectElement)) return;
const currentValue = assistantSelect.value || "";
const assistants = getAssistantOptions(projectSelect.value || "");
const fallbackValue = assistants.some((item) => item.value === currentValue)
? currentValue
: assistants.some((item) => item.value === defaultAssistantId)
? defaultAssistantId
: assistants[0]?.value || "";
assistantSelect.innerHTML = [{ value: "", label: "暂不绑定" }, ...assistants].map((item) => `
`).join("");
assistantSelect.value = fallbackValue;
};
const syncContext = () => {
if (!(contextHtml instanceof HTMLElement)) return;
const project = getProject();
const assistantId = assistantSelect instanceof HTMLSelectElement ? assistantSelect.value : defaultAssistantId;
contextHtml.innerHTML = renderIntakeActionContextHtml(project?.id || "", assistantId);
};
const syncTitle = () => {
const project = getProject();
const platform = getPlatform();
const recommended = recommendLiveRecorderTitle(project, platform);
if (titleInput instanceof HTMLInputElement) {
keepAutoValue(titleInput, recommended);
}
};
const syncRawSamples = () => {
if (!(rawTextarea instanceof HTMLTextAreaElement)) return;
keepAutoValue(rawTextarea, recommendLiveRecorderImportSamples(getPlatform()));
};
const syncDescription = () => {
if (!(options.description instanceof HTMLElement)) return;
options.description.textContent = `按行粘贴${platformLabel(getPlatform())}直播源,支持用逗号附带清晰度和标题,注释行会被视为停用源。`;
};
const sync = () => {
syncAssistantOptions();
syncContext();
syncTitle();
syncRawSamples();
syncDescription();
};
projectSelect?.addEventListener("change", sync);
assistantSelect?.addEventListener("change", syncContext);
platformSelect?.addEventListener("change", sync);
sync();
}
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 isMissingBackendCapability(error) {
const status = Number(error?.status || error?.payload?.status_code || 0);
if (status === 404) return true;
const raw = String(error?.message || error?.payload?.detail || error?.statusText || "").trim().toLowerCase();
return raw.includes("not found") || raw.includes("404");
}
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 isMobileViewport() {
return typeof window !== "undefined" && Boolean(window.matchMedia?.("(max-width: 760px)")?.matches);
}
function formatBackendDisplayLabel(value = DEFAULT_BACKEND_URL) {
const normalized = normalizeBackendUrlValue(value || DEFAULT_BACKEND_URL);
if (!normalized) return "未配置后端";
try {
const parsed = new URL(normalized);
return parsed.host || normalized;
} catch {
return normalized;
}
}
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 isWorkspaceBusy() {
return appState.busy || appState.workspaceHydrating;
}
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 = isWorkspaceBusy() ? "同步中" : (appState.session ? "已连接" : "连接状态");
}
}
function getScreenFromHash() {
const next = window.location.hash.replace("#", "");
return screenMap[next] ? next : "dashboard";
}
function getMobileTabGroup(screenId = appState.screen) {
const groups = {
dashboard: "dashboard",
credits: "dashboard",
settings: "dashboard",
intake: "intake",
owned: "intake",
discovery: "discovery",
tracking: "discovery",
production: "production",
review: "production",
automation: "production",
playbook: "playbook",
strategy: "playbook",
"admin-workbench": "playbook"
};
return groups[screenId] || screenId || "dashboard";
}
function setScreen(id, options = {}) {
const { updateHash = true } = options;
const resolvedId = screenMap[id] ? id : "dashboard";
const mobileGroup = getMobileTabGroup(resolvedId);
setMobileSidebarOpen(false);
appState.screen = resolvedId;
navButtons.forEach((button) => {
const active = button.dataset.screenTarget === resolvedId;
const mobileGroupActive = button.classList.contains("mobile-tabbar-item") && button.dataset.screenTarget === mobileGroup;
button.classList.toggle("is-active", active || mobileGroupActive);
});
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 = `
`;
topbarRight.insertBefore(inline, topbarRight.firstChild);
}
if (!document.querySelector(".auth-modal")) {
const modal = document.createElement("div");
modal.className = "auth-modal-backdrop hidden";
modal.innerHTML = `
`;
document.body.appendChild(modal);
}
}
function renderAuthUi() {
ensureAuthUi();
const session = appState.session;
const openButton = document.querySelector('[data-action="open-auth"]');
const logoutButtons = document.querySelectorAll('[data-action="logout-session"]');
const status = document.querySelector(".auth-status");
const message = document.querySelector('[data-role="auth-message"]');
if (openButton) openButton.textContent = session ? "连接状态" : "自动连接";
logoutButtons.forEach((button) => {
button.hidden = !session;
});
if (status) {
status.textContent = isWorkspaceBusy()
? appState.message || "正在加载..."
: session
? `${session.account?.display_name || session.account?.username || "已连接"} · ${session.backendUrl}`
: appState.autoConnectError || "等待自动连接";
}
if (message) {
message.textContent = isWorkspaceBusy() ? appState.message : (appState.autoConnectError || "");
}
syncMobileShell();
}
function openAuthModal() {
ensureAuthUi();
const modal = document.querySelector(".auth-modal-backdrop");
if (!modal) return;
const session = appState.session;
document.body.classList.add("sheet-open", "auth-sheet-open");
modal.classList.remove("hidden");
setAuthField("backendUrl", session?.backendUrl || DEFAULT_BACKEND_URL);
}
function closeAuthModal() {
document.body.classList.remove("sheet-open", "auth-sheet-open");
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 = `
`;
document.body.appendChild(modal);
}
function renderActionFields(fields) {
return fields.map((field) => {
if (field.hidden) {
return "";
}
const common = `data-action-field="${escapeHtml(field.name)}"`;
if (field.type === "html") {
return `
${field.html || ""}
`;
}
if (field.type === "textarea") {
return `
`;
}
if (field.type === "select") {
return `
`;
}
if (field.type === "checkbox") {
return `
`;
}
if (field.type === "file") {
return `
`;
}
return `
`;
}).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);
document.body.classList.add("sheet-open", "action-sheet-open");
modal.classList.remove("hidden");
if (typeof config.onOpen === "function") {
config.onOpen({ modal, title, description, fields, message, submit });
}
}
function closeActionModal() {
currentActionConfig = null;
document.body.classList.remove("sheet-open", "action-sheet-open");
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 = `
1
OneLiner
`;
document.body.appendChild(fab);
}
if (!document.querySelector(".oneliner-backdrop")) {
const panel = document.createElement("div");
panel.className = "oneliner-backdrop hidden";
panel.innerHTML = `
OneLiner
前端没上的需求,先由总控主 Agent 承接。
`;
document.body.appendChild(panel);
}
}
function renderOneLinerSessionTabs() {
const sessions = safeArray(appState.onelinerSessions).slice(0, 6);
if (!sessions.length) {
return `还没有会话发送第一条需求后自动创建
`;
}
const currentId = getCurrentOneLinerSession()?.id || "";
return `
${sessions.map((session) => `
${escapeHtml(brief(session.title || "新会话", 14))}
`).join("")}
`;
}
function renderOneLinerRunsHtml() {
const runs = safeArray(appState.onelinerRuns);
const currentRun = getCurrentOneLinerRun();
if (!runs.length || !currentRun) {
return `
还没有主 Agent 运行任务
你在首页、策略页或 Agent 页点击“交给主 Agent”后,这里会先出现待确认执行卡。
`;
}
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 = buildRecommendedActionAttrs(recommendedAction, {
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);
const currentRunConfigVersion = currentRun.governance?.oneliner_profile_version || currentRun.governance?.oneliner_profile?.current_version || {};
const currentRunPlatformAgentProfile = currentRun.result?.execution_card?.platform_agent_profile || currentRun.governance?.platform_agent_profile || {};
const latestOnelinerConfigVersion = appState.onelinerProfile?.current_version || {};
const latestPlatformAgentProfile = safeArray(appState.platformAgents).find((item) => item.platform === currentRunPlatformAgentProfile.platform) || {};
const currentRunOnelinerConfigStale = isConfigurationVersionStale(currentRunConfigVersion, latestOnelinerConfigVersion);
const currentRunPlatformAgentConfigStale = isConfigurationVersionStale(
currentRunPlatformAgentProfile,
latestPlatformAgentProfile.current_version || {}
);
const retryRunLabel = currentRunOnelinerConfigStale || currentRunPlatformAgentConfigStale ? "按当前配置重跑" : "重新执行";
return `
近期运行概况
先看待确认和执行中的任务,再切回当前 run 继续推进。
待确认 ${escapeHtml(formatNumber(pendingRunCount))}
执行中 ${escapeHtml(formatNumber(activeRunCount))}
已完成 ${escapeHtml(formatNumber(completedRunCount))}
异常 ${escapeHtml(formatNumber(problemRunCount))}
重点运行
已完成
异常运行
全部
${runs.length > 1 ? `
${visibleRuns.map((item) => `
${escapeHtml(brief(item.title || item.plan?.goal || "主 Agent 任务", 14))} · ${escapeHtml({
needs_confirmation: "待确认",
queued: "排队中",
running: "执行中",
blocked: "阻塞",
done: "已完成",
failed: "失败",
cancelled: "已取消"
}[item.run_status] || item.run_status || "运行中")}
`).join("")}
${!filteredRuns.length ? `
当前筛选下还没有对应运行,已临时显示全部任务。
` : ""}
` : ""}
当前运行
${escapeHtml(runStatusLabel)}
${escapeHtml(currentRun.platform_scope === "all_platforms" ? "全平台" : "单平台")}
${currentRunConfigVersion.version_no ? `${escapeHtml(`配置 v${formatNumber(currentRunConfigVersion.version_no || 0)}`)}` : ""}
${escapeHtml(recommendedAction?.label || "继续这个任务")}
当前运行
${escapeHtml(runStatusLabel)}
${escapeHtml(currentRun.summary || currentRun.status_summary || "主 Agent 会先给你一张确认卡,再继续执行。")}
${currentRun.run_status === "needs_confirmation" ? `
确认执行
取消本轮
` : ""}
${canRetryCurrentRun ? `${escapeHtml(retryRunLabel)}` : ""}
${hasResultPayload ? `查看结果` : ""}
${recommendedAction?.action ? `${escapeHtml(recommendedAction.label || "继续这个任务")}` : ""}
${escapeHtml(currentRun.title || currentRun.plan?.goal || "主 Agent 任务")}
${escapeHtml(currentRun.summary || currentRun.status_summary || "主 Agent 会先给你一张确认卡,再继续执行。")}
${escapeHtml(runStatusLabel)}
${currentRun.platform_label ? `${escapeHtml(currentRun.platform_label)}` : ""}
${escapeHtml(onelinerIntentLabel(currentRun.intent_key))}
${currentRunConfigVersion.version_no ? `配置 v${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))}` : ""}
${currentRun.source_screen ? `${escapeHtml(currentRun.source_screen)}` : ""}
${currentRun.active_admin_override_notice?.title ? `
管理员覆盖生效中
${escapeHtml(currentRun.active_admin_override_notice.summary || "当前运行会优先遵循管理员覆盖层。")}
` : ""}
当前计划
${escapeHtml(currentRun.plan?.summary || currentRun.summary || "主 Agent 会先按这张确认卡理解目标,再继续执行。")}
${escapeHtml(currentRun.source_action_key || "manual-handoff")}
${escapeHtml(currentRun.platform_scope === "all_platforms" ? "全平台" : "单平台")}
${currentRun.delivery_mode ? `${escapeHtml(currentRun.delivery_mode)}` : ""}
${currentRunConfigVersion.version_no ? `配置版本 ${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))}` : ""}
${previewAction?.action ? `${escapeHtml(`预计落点 · ${previewAction.label || "对应页面"}`)}` : ""}
${currentRunConfigVersion.version_no ? `
本轮主配置版本
${escapeHtml(currentRunConfigVersion.summary || currentRunConfigVersion.title || `OneLiner 配置 v${formatNumber(currentRunConfigVersion.version_no || 0)}`)}
配置 v${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))}
${currentRunOnelinerConfigStale ? `主配置已更新` : ""}
查看配置历史
` : ""}
${currentRunPlatformAgentProfile.platform ? `
本轮平台 Agent
${escapeHtml(currentRunPlatformAgentProfile.mission || currentRunPlatformAgentProfile.name || `${currentRunPlatformAgentProfile.platform_label || platformLabel(currentRunPlatformAgentProfile.platform)} Agent 正在承接这轮执行。`)}
${escapeHtml(currentRunPlatformAgentProfile.platform_label || platformLabel(currentRunPlatformAgentProfile.platform))}
${currentRunPlatformAgentProfile.name ? `${escapeHtml(currentRunPlatformAgentProfile.name)}` : ""}
${currentRunPlatformAgentProfile.assistant_name ? `${escapeHtml(currentRunPlatformAgentProfile.assistant_name)}` : ""}
${currentRunPlatformAgentProfile.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(currentRunPlatformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(currentRunPlatformAgentProfile.readiness_score || 0))}` : ""}
${currentRunPlatformAgentConfigStale ? `平台 Agent 已更新` : ""}
` : ""}
${!hasResultPayload && previewAction?.summary ? `
${escapeHtml(previewAction.summary)}
` : ""}
${planSteps.length ? `
${planSteps.map((step, index) => `
步骤 ${escapeHtml(formatNumber(index + 1))}
${escapeHtml(step)}
`).join("")}
` : ""}
${currentRun.run_status === "needs_confirmation" ? `
确认执行
取消本轮
` : `
${escapeHtml(currentRun.status_summary || "主 Agent 正在推进中")}
`}
${canRetryCurrentRun ? `${escapeHtml(retryRunLabel)}` : ""}
${hasResultPayload ? `查看结果` : ""}
${recommendedAction?.action ? `${escapeHtml(recommendedAction.label || "回到对应页面")}` : ""}
${recommendedAction?.summary ? `
${escapeHtml(recommendedAction.summary)}
` : ""}
${hasResultPayload ? `
执行结果
${renderOneLinerExecutionPayloadHtml(currentRun.result)}
` : ""}
${runEvents.length ? `
${runEvents.map((item) => `
${escapeHtml(item.event_type || "run.progress")}
${escapeHtml(item.summary || "运行状态已更新。")}
`).join("")}
` : ""}
${recentCompletedRuns.length ? `
最近完成
${recentCompletedRuns.map((item) => {
const doneAction = item.result?.recommended_action || null;
const doneLandingAttrs = buildRecommendedActionAttrs(doneAction, {
runId: item.id || "",
screen: doneAction?.screen || "",
title: item.title || item.plan?.goal || "主 Agent 任务",
summary: doneAction?.summary || item.status_summary || ""
});
return `
${escapeHtml(item.title || item.plan?.goal || "主 Agent 任务")}
${escapeHtml(item.status_summary || item.summary || "已完成,可继续回看结果。")}
已完成
查看结果
${doneAction?.action ? `${escapeHtml(doneAction.label || "回到对应页面")}` : ""}
`;
}).join("")}
` : ""}
`;
}
function renderOneLinerMessagesHtml() {
const messages = safeArray(appState.onelinerMessages);
if (!messages.length) {
return `
还没有对话
你可以直接说目标,不用先理解平台有什么按钮。OneLiner 会先拆目标,再决定交给哪个平台 Agent。
`;
}
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 profileVersion = executionCard.oneliner_profile_version || {};
const platformAgentProfile = executionCard.platform_agent_profile || {};
const actions = safeArray(plan.suggested_actions);
const primaryAction = executionCard.primary_action || {};
const secondaryActions = safeArray(executionCard.secondary_actions);
const buildOnelinerActionAttrs = (item) => {
const attrs = [
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 || {}).forEach(([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);
attrs.push(`data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`);
});
return attrs.filter(Boolean).join(" ");
};
return `
${escapeHtml(message.role === "assistant" ? "OneLiner" : "你")}
${escapeHtml(message.content || result.summary_text || "")}
${plan.intent_key ? `
${escapeHtml(onelinerIntentLabel(plan.intent_key))}
${plan.platform_label ? `${escapeHtml(plan.platform_label)}` : ""}
${plan.delivery_mode ? `${escapeHtml(plan.delivery_mode === "oneliner" ? "对话承接" : "可走前端")}` : ""}
` : ""}
${actions.length ? `
${actions.map((item) => actionTag(
item.label || item.executor_label || item.key || "执行动作",
item.key || "",
buildOnelinerActionAttrs(item),
{ disabledReason: item.disabled_reason || "" }
)).join("")}
` : ""}
${message.role === "assistant" && (executionCard.intent_label || executionCard.platform_label || executionCard.primary_action?.label || safeArray(executionCard.evidence).length) ? `
${escapeHtml(executionCard.intent_label || "本轮执行建议")}
${escapeHtml(executionCard.blocked_reason || `${executionCard.platform_label || "待判断平台"} · ${executionCard.delivery_mode === "ui" ? "优先走前端固定动作" : "优先由 OneLiner 对话承接"}`)}
${executionCard.platform_label ? `${escapeHtml(executionCard.platform_label)}` : ""}
${executionCard.platform_agent_name ? `${escapeHtml(executionCard.platform_agent_name)}` : ""}
${executionCard.assistant_name ? `${escapeHtml(executionCard.assistant_name)}` : ""}
${profileVersion.version_no ? `配置 v${escapeHtml(formatNumber(profileVersion.version_no || 0))}` : ""}
${profileVersion.version_no ? `看主配置历史` : ""}
${executionCard.platform && platformAgentProfile.version_no ? `看平台配置历史` : ""}
${executionCard.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(executionCard.readiness_label)} ${escapeHtml(formatNumber(executionCard.readiness_score || 0))}` : ""}
${primaryAction.key ? actionTag(
primaryAction.label || "执行下一步",
primaryAction.key || "",
buildOnelinerActionAttrs(primaryAction),
{ disabledReason: primaryAction.disabled_reason || "" }
) : ""}
${profileVersion.version_no ? `
${escapeHtml(profileVersion.summary || profileVersion.title || `当前按 OneLiner 配置 v${formatNumber(profileVersion.version_no || 0)} 执行。`)}
` : ""}
${activeAdminOverrideNotice?.title ? `
管理员覆盖生效中
${escapeHtml(activeAdminOverrideNotice.summary || "当前这轮执行会优先遵循管理员覆盖,再叠加你的个人策略。")}
${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}
${activeAdminOverrideNotice.platform_label ? `${escapeHtml(activeAdminOverrideNotice.platform_label)}` : ""}
` : ""}
${safeArray(executionCard.evidence).length ? `
${safeArray(executionCard.evidence).slice(0, 2).map((item) => `
${escapeHtml(item.kind === "skill" ? "技能证据" : "记忆证据")} · ${escapeHtml(item.title || "未命名")}
${escapeHtml(item.summary || "暂无摘要")}
`).join("")}
` : ""}
${safeArray(executionCard.next_steps).length ? `
${safeArray(executionCard.next_steps).slice(0, 3).map((item) => `${escapeHtml(item)}`).join("")}
` : ""}
${secondaryActions.length ? `
${secondaryActions.map((item) => actionTag(
item.label || item.key || "执行",
item.key || "",
buildOnelinerActionAttrs(item),
{ disabledReason: item.disabled_reason || "" }
)).join("")}
` : ""}
` : ""}
`;
}).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 = `
${escapeHtml(profile?.display_name || "OneLiner")}
${escapeHtml(getSelectedProject()?.name || "未选项目")}
${escapeHtml(profile?.default_platform ? platformLabel(profile.default_platform) : "未设默认平台")}
${profile?.current_version?.version_no ? `配置 v${escapeHtml(formatNumber(profile.current_version.version_no || 0))}` : ""}
${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent
${escapeHtml(profile?.long_term_goal || "当前还没有设长期目标。你可以先直接告诉主 Agent 你要推进的方向,它会按当前项目持续收敛执行策略。")}
${layers.map((layer) => `${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}`).join("") || `还没有策略层`}
${highlights.map((item) => `${escapeHtml(item)}`).join("")}
我的策略
${activeAdminOverrideNotice?.title ? `
管理员覆盖生效中
${escapeHtml(activeAdminOverrideNotice.summary || "当前主 Agent 会优先遵循管理员覆盖层。")}
${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}
查看我的策略
` : ""}
`;
}
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.body.classList.add("sheet-open", "oneliner-open");
document.querySelector(".oneliner-backdrop")?.classList.remove("hidden");
syncOneLinerRunPolling();
}
function closeOneLinerPanel() {
document.body.classList.remove("sheet-open", "oneliner-open");
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 = "") {
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) {
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 [profile, sessionsPayload, runsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, userPolicyAuditsPayload, adminSystemMainPolicy, adminSystemPlatformPolicies, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null),
storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })),
storyforgeFetch(`/v2/oneliner/runs?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })),
storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })),
storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })),
storyforgeFetch(`/v2/oneliner/governance/effective?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => null),
storyforgeFetch(`/v2/oneliner/governance/user/global?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null),
storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(governancePlatform)}?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null),
storyforgeFetch(`/v2/oneliner/governance/user/audits?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => ({ items: [] })),
isSuperAdmin()
? storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent").catch(() => null)
: Promise.resolve(null),
isSuperAdmin()
? Promise.all(ACTIVE_PLATFORMS.map((item) =>
storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(item.value)}`).catch(() => null)
))
: Promise.resolve([]),
isSuperAdmin()
? storyforgeFetch("/v2/admin/oneliner/governance/directory").catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null),
storyforgeFetch(`/v2/tenant/usage?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null),
isSuperAdmin()
? storyforgeFetch("/v2/admin/ops/overview").catch(() => null)
: Promise.resolve(null),
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() && 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 = 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) {
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 需要先绑定项目上下文。");
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) {
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 ``;
}
const recommendedAction = payload.recommended_action && typeof payload.recommended_action === "object"
? payload.recommended_action
: null;
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 configVersion = payload.execution_card?.oneliner_profile_version || payload.context?.oneliner_profile?.current_version || {};
const platformAgentProfile = payload.execution_card?.platform_agent_profile || {};
const latestOnelinerConfigVersion = appState.onelinerProfile?.current_version || {};
const latestPlatformAgentProfile = safeArray(appState.platformAgents).find((item) => item.platform === platformAgentProfile.platform) || {};
const currentRunOnelinerConfigStale = isConfigurationVersionStale(configVersion, latestOnelinerConfigVersion);
const currentRunPlatformAgentConfigStale = isConfigurationVersionStale(
platformAgentProfile,
latestPlatformAgentProfile.current_version || {}
);
const landingAttrs = buildRecommendedActionAttrs(payload.recommended_action, {
runId: landingRunId,
screen: landingScreen,
title: landingTitle,
summary: landingSummary
});
return `
${escapeHtml(payload.goal || "主 Agent 执行建议")}
${escapeHtml(payload.execution_summary || payload.summary_text || "已形成一版可继续执行的主 Agent 建议。")}
${payload.platform ? `${escapeHtml(platformLabel(payload.platform))}` : ""}
${escapeHtml(payload.platform_scope === "all_platforms" ? "全平台" : "单平台")}
${configVersion.version_no ? `配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))}` : ""}
${currentRunOnelinerConfigStale ? `主配置已更新` : ""}
${currentRunPlatformAgentConfigStale ? `平台 Agent 已更新` : ""}
已收口
${payload.recommended_action?.action ? `${escapeHtml(payload.recommended_action.label || "回到对应页面")}` : ""}
${configVersion.version_no ? `
本轮使用的主配置
${escapeHtml(configVersion.summary || configVersion.title || `OneLiner 主配置版本 v${formatNumber(configVersion.version_no || 0)}`)}
配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))}
${currentRunOnelinerConfigStale ? `主配置已更新` : ""}
查看配置历史
` : ""}
${platformAgentProfile.platform ? `
本轮平台 Agent
${escapeHtml(platformAgentProfile.mission || platformAgentProfile.name || `${platformAgentProfile.platform_label || platformLabel(platformAgentProfile.platform)} Agent 正在承接这轮执行。`)}
${escapeHtml(platformAgentProfile.platform_label || platformLabel(platformAgentProfile.platform))}
${platformAgentProfile.name ? `${escapeHtml(platformAgentProfile.name)}` : ""}
${platformAgentProfile.assistant_name ? `${escapeHtml(platformAgentProfile.assistant_name)}` : ""}
${platformAgentProfile.version_no ? `${escapeHtml(platformLabel(platformAgentProfile.platform || payload.platform || ""))} Agent v${escapeHtml(formatNumber(platformAgentProfile.version_no || 0))}` : ""}
${currentRunPlatformAgentConfigStale ? `平台 Agent 已更新` : ""}
${platformAgentProfile.platform && platformAgentProfile.version_no ? `看平台配置历史` : ""}
${platformAgentProfile.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(platformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(platformAgentProfile.readiness_score || 0))}` : ""}
` : ""}
${resultCards.length ? `
${escapeHtml(resultSections.workstream_label || "执行结果分组")}
${escapeHtml(payload.summary_text || "主 Agent 已将当前执行结果整理为可继续推进的结构化建议。")}
${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 `
${escapeHtml(card?.title || (index === 0 ? "当前焦点" : "执行结果"))}
${escapeHtml(card?.body || "主 Agent 已生成一条可继续执行的结果说明。")}
${escapeHtml(card?.title || (index === 0 ? "当前焦点" : "结果卡片"))}
${tags.map((tag) => `${escapeHtml(tag)}`).join("")}
`;
}).join("")}
` : ""}
${payload.recommended_action?.summary ? `
建议回跳
${escapeHtml(payload.recommended_action.summary)}
` : ""}
${safeArray(payload.next_steps).length ? `
${safeArray(payload.next_steps).slice(0, 4).map((step, index) => `
下一步 ${escapeHtml(formatNumber(index + 1))}
${escapeHtml(step)}
`).join("")}
` : ""}
`;
}
if (payload.job) {
const job = payload.job || {};
const sourceJob = payload.source_job || {};
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "production",
title: job.title || "任务已创建",
summary: recommendedAction?.summary || payload.brief || sourceJob.title || ""
});
return `
${escapeHtml(job.title || "任务已创建")}
${escapeHtml(payload.brief || sourceJob.title || `已创建 ${job.line_type || job.source_type || "任务"},你可以继续进入生产中心查看进度。`)}
${escapeHtml(job.line_type || job.source_type || "analysis")}
${escapeHtml(job.status || "queued")}
${recommendedAction?.action ? actionTag(recommendedAction.label || "看任务详情", recommendedAction.action, recommendedAttrs) : job.id ? `看任务详情` : ""}
去生产中心
`;
}
if (payload.saved || payload.started) {
const saved = payload.saved || {};
const item = saved.item || {};
const started = payload.started || {};
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "production",
title: item.binding_title || payload.source_url || "录制源已保存",
summary: recommendedAction?.summary || item.source_url || payload.source_url || ""
});
return `
${escapeHtml(item.binding_title || payload.source_url || "录制源已保存")}
${escapeHtml(item.source_url || payload.source_url || "直播源已经保存到当前租户的录制配置。")}
${escapeHtml(platformLabel(payload.platform || item.platform || "kuaishou"))}
${escapeHtml(item.quality || "原画")}
${escapeHtml(started && started.ok === false ? "启动待重试" : "已同步")}
${recommendedAction?.action ? actionTag(recommendedAction.label || "打开录制控制", recommendedAction.action, recommendedAttrs) : `打开录制控制`}
`;
}
if (payload.analyzed_count !== undefined && safeArray(payload.items).length) {
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "discovery",
title: "OneLiner 已分析高分作品",
summary: recommendedAction?.summary || ""
});
return `
平台${escapeHtml(platformLabel(payload.platform || payload.account?.platform || "douyin"))}
账号${escapeHtml(payload.account?.title || payload.account?.handle || payload.account?.source_url || "当前账号")}
拆解作品${escapeHtml(formatNumber(payload.analyzed_count || 0))}
已写记忆${escapeHtml(payload.memory?.title ? "是" : "否")}
${safeArray(payload.items).slice(0, 4).map((item) => `
${escapeHtml(item.video_title || "高分作品")}
${escapeHtml(item.summary_text || "已完成拆解。")}
得分 ${escapeHtml(formatNumber(item.performance_score || 0))}
${item.latest_job_id ? `看原分析` : ""}
`).join("")}
${recommendedAction?.action ? `
${actionTag(recommendedAction.label || "回到找对标", recommendedAction.action, recommendedAttrs)}
` : ""}
`;
}
if (payload.route_checks) {
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: recommendedAction?.screen || "playbook",
title: payload.platform_label || payload.platform || "平台自检",
summary: recommendedAction?.summary || ""
});
return `
平台${escapeHtml(payload.platform_label || payload.platform || "-")}
得分${escapeHtml(formatNumber(payload.score || 0))}
状态${escapeHtml(payload.readiness_label || payload.verdict || "-")}
账号源${escapeHtml(formatNumber(payload.source_count || 0))}
路由检查
${safeArray(payload.route_checks).map((item) => `
${escapeHtml(item.key || item.path || "route")}
${escapeHtml(item.path || "")}
${escapeHtml(item.ok ? "可用" : "缺失")}
`).join("")}
建议动作
${(safeArray(payload.suggestions).length ? safeArray(payload.suggestions) : ["当前已经达到可运行状态。"]).map((item) => `
`).join("")}
${recommendedAction?.action ? `
${actionTag(recommendedAction.label || "查看平台 Agent", recommendedAction.action, recommendedAttrs)}
` : ""}
`;
}
if (payload.strategy && payload.tenant_usage) {
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "automation",
title: "当前存储状态",
summary: recommendedAction?.summary || ""
});
return `
jobs${escapeHtml(payload.tenant_usage?.project_jobs?.human_size || "0B")}
downloads${escapeHtml(payload.tenant_usage?.project_downloads?.human_size || "0B")}
模型目录${escapeHtml(payload.strategy?.models?.mode || "-")}
录像${escapeHtml(payload.strategy?.live_recorder?.mode || "-")}
${recommendedAction?.action ? `
${actionTag(recommendedAction.label || "去自动流程", recommendedAction.action, recommendedAttrs)}
` : ""}
`;
}
if (payload.items || payload.files) {
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "production",
title: "直播录制状态",
summary: recommendedAction?.summary || ""
});
return `
录制源${escapeHtml(formatNumber(safeArray(payload.items).length))}
最近文件${escapeHtml(formatNumber(safeArray(payload.files).length))}
${recommendedAction?.action ? `
${actionTag(recommendedAction.label || "打开录制维护", recommendedAction.action, recommendedAttrs)}
` : ""}
`;
}
if (payload.content) {
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "playbook",
title: "生成文案",
summary: recommendedAction?.summary || ""
});
return `
生成文案
${escapeHtml(brief(payload.content, 1200))}
${payload.assistant_id ? `${escapeHtml(payload.assistant_id)}` : ""}
${escapeHtml(formatNumber(safeArray(payload.used_documents).length))} 个参考素材
${recommendedAction?.action ? actionTag(recommendedAction.label || "继续调文案", recommendedAction.action, recommendedAttrs) : ""}
`;
}
if (payload.verdict !== undefined || payload.next_actions !== undefined || payload.highlights !== undefined) {
const recommendedAttrs = buildRecommendedActionAttrs(recommendedAction, {
screen: "review",
title: payload.title || "复盘草稿",
summary: recommendedAction?.summary || ""
});
return `
${escapeHtml(payload.title || "复盘草稿")}
${escapeHtml(payload.highlights || payload.notes || "已生成一版待补充的复盘草稿。")}
${escapeHtml(platformLabel(payload.platform || "douyin"))}
${escapeHtml(payload.verdict || "待补充")}
${payload.source_job_id ? `看任务详情` : ""}
${recommendedAction?.action ? actionTag(recommendedAction.label || "打开复盘", recommendedAction.action, recommendedAttrs) : payload.id ? `打开复盘` : ""}
`;
}
return `
原始结果
${escapeHtml(brief(JSON.stringify(payload, null, 2), 1200))}
`;
}
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 = {}) {
const projectId = options.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);
}
if (options.showResultModal !== false) {
openActionModal({
title: payload.title || "OneLiner 执行结果",
description: payload.summary || "已完成一次对话内执行。",
hideSubmit: true,
fields: [
{
type: "html",
label: "执行结果",
html: `${renderOneLinerExecutionPayloadHtml(payload.payload || {})}
`
}
]
});
}
rememberAction("OneLiner 已执行", payload.summary || "当前动作已在对话内执行。", "green", payload);
renderAll();
return payload;
}
async function followRecommendedActionResult(payload, options = {}) {
const recommendedAction = payload?.recommended_action && typeof payload.recommended_action === "object"
? payload.recommended_action
: null;
if (!recommendedAction?.action) return false;
const actionName = String(recommendedAction.action || "").trim();
const platform = normalizePlatformValue(
recommendedAction.platform || payload?.payload?.platform || options.platform || getPreferredPlatform(),
getPreferredPlatform()
);
if (options.refreshWorkbench) {
await bootstrap();
}
if (actionName === "open-job-detail" && recommendedAction.job_id) {
closeActionModal();
openJobDetailAction(recommendedAction.job_id);
return true;
}
if (actionName === "select-account" && recommendedAction.account_id) {
closeActionModal();
await loadPlatformAccount(platform, recommendedAction.account_id);
if (options.discoveryFocus === "relations") {
focusDiscoveryRelations();
} else if (options.discoveryFocus === "snapshots") {
focusDiscoveryInsights();
} else if (options.discoveryFocus === "top-videos") {
focusDiscoveryTopVideoInsights();
} else {
focusDiscoveryDetailTab("overview");
}
return true;
}
if (actionName === "goto-tracking") {
closeActionModal();
focusTrackingWorkspace();
return true;
}
if (actionName === "open-edit-assistant" && recommendedAction.assistant_id) {
closeActionModal();
focusPlaybookWorkspace(recommendedAction.assistant_id);
return true;
}
if (actionName === "open-review-edit" && recommendedAction.review_id) {
closeActionModal();
focusReviewWorkspace(recommendedAction.review_id);
return true;
}
if (actionName === "goto-discovery") {
closeActionModal();
if (options.discoveryFocus === "relations") {
focusDiscoveryRelations();
} else {
focusDiscoveryDetailTab("overview");
}
return true;
}
return false;
}
async function runDirectDiscoveryAction(executorKey, payload, options = {}) {
const account = requireSelectedAccountRow();
const defaultPlatform = getAccountPlatform(account);
const platform = normalizePlatformValue(options.platform || defaultPlatform, defaultPlatform);
setBusy(true, options.busyLabel || "正在执行当前动作...");
try {
const result = await executeOneLinerAction(executorKey, {
projectId: options.projectId || getSelectedProject()?.id || "",
platform,
payload,
showResultModal: false
});
await followRecommendedActionResult(result, {
platform,
refreshWorkbench: true,
discoveryFocus: options.discoveryFocus || ""
});
return result;
} catch (error) {
presentActionFailure(error, options.errorTitle || "执行失败");
throw error;
} finally {
setBusy(false, "");
}
}
async function runDirectWorkbenchAction(executorKey, options = {}) {
const platform = normalizePlatformValue(options.platform || getPreferredPlatform(), getPreferredPlatform());
setBusy(true, options.busyLabel || "正在执行当前动作...");
try {
const result = await executeOneLinerAction(executorKey, {
platform,
payload: options.payload || {},
showResultModal: false
});
if (typeof options.afterResult === "function") {
await options.afterResult(result);
}
if (options.followRecommendedAction !== false) {
await followRecommendedActionResult(result, {
platform,
refreshWorkbench: options.refreshWorkbench !== false,
discoveryFocus: options.discoveryFocus || ""
});
}
return result;
} catch (error) {
presentActionFailure(error, options.errorTitle || "执行失败");
throw error;
} finally {
setBusy(false, "");
}
}
function openCurrentOneLinerRunResultAction(runId = "") {
openOneLinerRunContextAction(runId)
.then((currentRun) => {
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: `${renderOneLinerExecutionPayloadHtml(currentRun.result)}
`
}
]
});
})
.catch(() => {
rememberAction("还没有可查看的结果", "当前主 Agent 任务还没有返回可展示的执行结果。", "orange");
renderAll();
});
}
async function openOneLinerRunContextAction(runId = "") {
const targetRunId = runId || appState.selectedOnelinerRunId || "";
if (!targetRunId) {
rememberAction("还没有可查看的运行", "当前还没有主 Agent 运行可继续查看。", "orange");
renderAll();
return null;
}
const currentProjectId = appState.selectedProjectId || getOneLinerProjectId();
if (!safeArray(appState.onelinerRuns).some((item) => item.id === targetRunId) && currentProjectId) {
await loadAgentControlSurfaces(currentProjectId);
}
appState.selectedOnelinerRunId = targetRunId;
const hydrated = await hydrateSelectedOneLinerRun();
const currentRun = hydrated || safeArray(appState.onelinerRuns).find((item) => item.id === targetRunId) || null;
if (currentRun?.run_status === "done") {
appState.onelinerRunFilter = "done";
}
openOneLinerPanel();
renderAll();
return currentRun;
}
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 ? `${escapeHtml(run.platform_label)}` : "",
`${escapeHtml(run.platform_scope === "all_platforms" ? "全平台" : "单平台")}`,
`${escapeHtml(onelinerIntentLabel(run.intent_key))}`,
run.source_screen ? `${escapeHtml(run.source_screen)}` : ""
].filter(Boolean).join("");
const planHtml = `
当前计划
${escapeHtml(run.plan?.summary || run.summary || "主 Agent 会先按这张确认卡理解目标,再继续执行。")}
${tags}
${planSteps.length ? `
${planSteps.map((step, index) => `
步骤 ${escapeHtml(formatNumber(index + 1))}
${escapeHtml(step)}
`).join("")}
` : ""}
${previewAction ? `
预计落点
${escapeHtml(previewAction.summary || "执行后会回到更合适的业务页面继续推进。")}
${escapeHtml(previewAction.label || "回到对应页面")}
${previewAction.screen ? `${escapeHtml(previewAction.screen)}` : ""}
` : ""}
`;
openActionModal({
title: "确认主 Agent 执行计划",
description: "确认后,主 Agent 会按当前计划进入执行流;你也可以先补一句执行说明。",
submitLabel: "确认执行",
fields: [
{
name: "plan",
type: "html",
label: "执行计划",
html: `${planHtml}
`
},
{
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);
try {
const [workspace, videos, snapshotsPayload, analysisReportsPayload] = await Promise.all([
storyforgeFetch(workspacePath),
videosPath
? 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
}),
normalizedPlatform === "douyin"
? storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(accountId)}/snapshots`).catch(() => [])
: Promise.resolve([]),
normalizedPlatform === "douyin"
? 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 = 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 hydrateWorkbenchDataAfterBootstrap(dashboard, preferredPlatform, requestToken) {
const runtimePlatforms = getRuntimePlatformValues();
try {
appState.backendCapabilities = await loadBackendCapabilities(appState.session.backendUrl).catch(() => null);
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 accounts = accountListPath
? await storyforgeFetch(accountListPath).catch(() => [])
: [];
const trackingAccountsPayload = trackingAccountsPath
? await storyforgeFetch(trackingAccountsPath).catch(() => ({ items: [], cursor_last_seen_at: "" }))
: { items: [], cursor_last_seen_at: "" };
const trackingCursorLastSeenAt = trackingAccountsPayload?.cursor_last_seen_at || "";
const trackingDigest = trackingDigestPath
? 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
};
})),
storyforgeFetch("/v2/reviews").catch(() => []),
storyforgeFetch("/v2/integrations/health").catch(() => null),
storyforgeFetch("/v2/integrations/local-models").catch(() => null),
storyforgeFetch("/v2/live-recorder/sources").catch(() => ({ items: [] }))
]);
if (requestToken !== bootstrapHydrationToken) return;
const liveRecorderIntegration = integrationHealth?.live_recorder || null;
const canLoadLiveRecorderRuntime = Boolean(liveRecorderIntegration?.reachable);
const [liveRecorderStatus, liveRecorderFilesPayload, liveRecorderHealth] = await Promise.all([
canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/status").catch(() => null) : Promise.resolve(null),
canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/files?limit=16").catch(() => ({ items: [] })) : Promise.resolve({ items: [] }),
canLoadLiveRecorderRuntime ? storyforgeFetch("/v2/live-recorder/health").catch(() => null) : Promise.resolve(null)
]);
if (requestToken !== bootstrapHydrationToken) return;
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;
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) {
appState.selectedAccountId = "";
appState.selectedWorkspace = null;
appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 };
}
renderAll();
appState.workspaceHydrating = false;
renderAll();
void loadKnowledgeDocuments(dashboard.knowledge_bases)
.then((documents) => {
if (requestToken !== bootstrapHydrationToken) return;
appState.documents = documents;
renderAll();
})
.catch(() => {});
void Promise.all([
loadStorageStatus(appState.selectedProjectId || ""),
loadAgentControlSurfaces(appState.selectedProjectId || "")
]).then(async () => {
if (requestToken !== bootstrapHydrationToken) return;
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId).catch(() => {});
} else {
appState.onelinerMessages = [];
}
renderAll();
}).catch(() => {});
if (nextAccountId) {
const nextAccount = appState.accounts.find((item) => item.id === nextAccountId) || null;
void loadPlatformAccount(getAccountPlatform(nextAccount), nextAccountId).catch(() => {});
}
} catch (error) {
if (requestToken !== bootstrapHydrationToken) return;
appState.workspaceHydrating = false;
appState.message = error.message;
renderAll();
} finally {
if (requestToken === bootstrapHydrationToken) {
appState.recoveryRecords = getRecoveryRecords();
renderAll();
}
}
}
async function bootstrap() {
renderAll();
const requestToken = ++bootstrapHydrationToken;
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 {
const [me, dashboard] = await Promise.all([
storyforgeFetch("/v2/me"),
storyforgeFetch("/v2/me/dashboard")
]);
appState.me = me;
if (requestToken !== bootstrapHydrationToken) return;
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;
}
if (requestToken !== bootstrapHydrationToken) return;
appState.dashboard = dashboard;
const preferredPlatform = getCurrentPlatformValue();
setCurrentPlatform(preferredPlatform);
appState.selectedProjectId = appState.selectedProjectId || dashboard.projects?.[0]?.id || "";
const selectedAssistantExists = safeArray(dashboard.assistants).some((item) => item.id === appState.selectedAssistantId);
appState.selectedAssistantId = selectedAssistantExists ? appState.selectedAssistantId : (dashboard.assistants?.[0]?.id || "");
appState.workspaceHydrating = true;
setBusy(false, "");
renderAll();
void hydrateWorkbenchDataAfterBootstrap(dashboard, preferredPlatform, requestToken);
return;
} 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();
if (appState.busy) {
setBusy(false, "");
}
renderAll();
}
}
async function markTrackingDigestRead() {
const platform = getCurrentPlatformValue();
const trackingCursorPath = getWorkbenchRoute(platform, "trackingCursor");
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");
setBusy(true, "正在同步跟踪账号...");
try {
const payload = await storyforgeFetch(trackingRefreshPath, {
method: "POST"
});
const refreshNotice = summarizeTrackingRefreshPayload(payload, platform, "batch");
rememberTrackingRefreshNotice(refreshNotice);
rememberAction(
refreshNotice?.title || "跟踪已同步",
refreshNotice?.summary || `已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)} 个` : ""}。`,
refreshNotice?.tone || (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);
setBusy(true, "正在同步该跟踪账号...");
try {
const payload = await storyforgeFetch(trackingRefreshPath, {
method: "POST"
});
const refreshNotice = summarizeTrackingRefreshPayload(payload, platform, "single");
rememberTrackingRefreshNotice(refreshNotice);
const success = payload.success !== false;
rememberAction(
refreshNotice?.title || (success ? "单账号已同步" : "单账号刷新失败"),
refreshNotice?.summary || (success
? `已刷新「${payload.account?.nickname || trackedAccountId}」的最新作品。`
: `暂时无法刷新「${payload.account?.nickname || trackedAccountId}」:${payload.message || "请稍后重试"}`),
refreshNotice?.tone || (success ? (safeArray(payload.sync_errors).length ? "orange" : "green") : "orange"),
payload
);
await bootstrap();
if (payload.sync_job_id) {
openJobDetailAction(payload.sync_job_id);
}
} 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 getLatestCompletedProjectJob() {
const projectId = appState.selectedProjectId || "";
return safeArray(appState.dashboard?.recent_jobs).find((item) => {
if (item.status !== "completed") return false;
if (!projectId) return true;
return String(item.project_id || "") === projectId;
}) || null;
}
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 `
当前项目
${escapeHtml(project?.name || "还没有项目")}
${escapeHtml(project?.description || "先选定项目,首页动作才会真正收敛。")}
主流程进度
${escapeHtml(`${progress.completed} / ${progress.total}`)}
${escapeHtml(progress.nextStep)}
阶段风险
${escapeHtml(progress.risk)}
${escapeHtml(stats.jobs.length ? `当前项目任务 ${formatNumber(stats.jobs.length)} 条` : "还没有生产任务")}
下一步建议
${escapeHtml(project ? `当前项目建议先「${progress.nextStep}」,做完后再回到首页看下一条动作。` : "先创建项目或切换到已有项目,然后首页动作区会自动切到该项目。")}
${actionTag(project ? "切换项目" : "去建项目", project ? "open-dashboard-project-switcher" : "goto-intake")}
${actionTag("去生产中心", "goto-production")}
`;
}
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 `
还没有重点账号 / 对标
${escapeHtml(project ? "先导入主页或把一个重点账号加入跟踪,首页才会持续给出更聪明的动作推荐。" : "先创建项目,再导入第一个重点账号。")}
${actionTag("去找对标", "goto-discovery")}
${actionTag("去跟踪账号", "goto-tracking")}
`;
}
return `
${items.map((item) => {
const account = item.account || item;
const accountId = account?.id || item.tracked_account_id || "";
return `
${escapeHtml(initials(getAccountName(account)))}
${escapeHtml(getAccountName(account))}
${escapeHtml(platformLabel(item.platform || getAccountPlatform(account)))} · ${escapeHtml(item.assistant_name || account.signature || "重点关注对象")}
${escapeHtml(account.video_summary?.count ? `作品 ${formatNumber(account.video_summary.count)}` : "重点对象")}
${account.sync_status ? `${escapeHtml(account.sync_status)}` : ""}
${accountId ? actionTag("查看详情", "select-account", `data-account-id="${escapeHtml(accountId)}"`) : ""}
`;
}).join("")}
`;
}
function renderDashboardProductionJobsBody(stats) {
const jobs = sortItemsByIsoDesc(stats.jobs, "updated_at").slice(0, 4);
if (!jobs.length) {
return `
当前项目还没有生产任务
先补完项目和对标,再从首页动作区或生产中心发起第一条任务。
${actionTag("去生产中心", "goto-production")}
${actionTag("去找对标", "goto-discovery")}
`;
}
return `
${jobs.map((job) => `
${escapeHtml(job.title || job.id)}
${escapeHtml(brief(job.error || job.summary || `最近更新于 ${formatDateTime(job.updated_at || job.created_at)}`, 120))}
${escapeHtml(job.status || "-")}
${job.line_type ? `${escapeHtml(job.line_type)}` : ""}
${actionTag("查看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
`).join("")}
`;
}
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 })
};
}
async function applySelectedProject(projectId = "") {
appState.selectedProjectId = projectId || "";
setBusy(true, "正在切换项目视图...");
try {
await loadStorageStatus(appState.selectedProjectId || "");
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
}
} finally {
setBusy(false, "");
}
rememberAction("当前项目已切换", `已切换到「${getSelectedProject()?.name || "所选项目"}」,你现在看到的首页、Agent 和任务都会跟随更新。`, "green");
focusDashboardWorkspace("dashboard-workspace-anchor");
}
function openDashboardProjectSwitcher() {
const options = getProjectOptions();
if (!options.length) {
rememberAction("还没有项目", "先创建一个项目,再让首页跟着当前项目切换。", "orange");
setScreen("intake");
return;
}
const selectedProject = getSelectedProject();
const projects = safeArray(appState.dashboard?.projects);
const isMobileViewport = typeof window !== "undefined" && window.matchMedia?.("(max-width: 760px)")?.matches;
const projectCards = projects.map((project) => {
const stats = getProjectStats(project.id);
const reviewCount = getProjectReviews(project.id).length;
const isActive = project.id === selectedProject?.id;
return `
`;
}).join("");
openActionModal({
title: "切换当前项目",
description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。手机端会优先让你先扫一眼当前项目和最近任务。",
submitLabel: "切换项目",
fields: [
{
name: "projectSummary",
label: "项目速览",
type: "html",
html: `
${escapeHtml(selectedProject?.name || "当前还没有项目")}
${escapeHtml(selectedProject?.description || "切换项目后,首页、OneLiner 和工作台会一起同步到对应上下文。")}
当前项目
最近任务 ${escapeHtml(formatNumber(getProjectStats(selectedProject?.id || "").jobs.length))}
切换到这个项目后会同步总台、Agent 和生产中心
最近任务
优先切到你现在真正要推进的项目,再从首页和主 Agent 继续往下走。
${projectCards}
`
},
{ name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options, hidden: isMobileViewport }
],
onOpen: ({ fields, submit }) => {
const select = fields.querySelector('[data-action-field="projectId"]');
if (submit && isMobileViewport) {
submit.hidden = true;
}
fields.querySelectorAll("[data-project-choice]").forEach((button) => {
button.addEventListener("click", async () => {
const nextProjectId = button.dataset.projectChoice || "";
if (select) {
select.value = nextProjectId;
}
fields.querySelectorAll("[data-project-choice]").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
if (isMobileViewport && nextProjectId) {
closeActionModal();
await applySelectedProject(nextProjectId);
}
});
});
},
onSubmit: async (payload) => {
await applySelectedProject(payload.projectId || "");
}
});
}
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: `
${escapeHtml(appState.dashboardActionReason.title)}
${escapeHtml(appState.dashboardActionReason.reason)}
${escapeHtml(appState.dashboardActionReason.sourceLabel)}
${appState.dashboardActionReason.badges.map((item) => `${escapeHtml(item)}`).join("")}
`
}
]
});
}
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 getCurrentTrackingRefreshNotice(platform = getCurrentPlatformValue()) {
const current = appState.trackingRefreshNotice || null;
if (!current) return null;
return normalizePlatformValue(current.platform || "", "") === normalizePlatformValue(platform || "", "") ? current : null;
}
function rememberTrackingRefreshNotice(notice) {
appState.trackingRefreshNotice = notice ? { ...notice, created_at: notice.created_at || new Date().toISOString() } : null;
}
function summarizeTrackingRefreshPayload(payload, platform, mode = "batch") {
const normalizedPlatform = normalizePlatformValue(platform || getCurrentPlatformValue(), getCurrentPlatformValue());
if (!payload || typeof payload !== "object") {
return null;
}
if (mode === "single") {
if (payload.success === false) {
return {
platform: normalizedPlatform,
mode,
tone: "orange",
title: "单账号同步失败",
summary: `暂时无法刷新该账号:${payload.message || "请稍后重试"}`,
items: []
};
}
if (payload.sync_job_id) {
return {
platform: normalizedPlatform,
mode,
tone: "blue",
title: "单账号同步已排队",
summary: `已为「${payload.account?.nickname || payload.tracked_account_id || "当前账号"}」创建后台同步任务,稍后会把结果回流到日报和作品区。`,
items: [{
tracked_account_id: payload.tracked_account_id || "",
sync_job_id: payload.sync_job_id,
status: payload.status || "queued"
}]
};
}
return {
platform: normalizedPlatform,
mode,
tone: safeArray(payload.sync_errors).length ? "orange" : "green",
title: "单账号已刷新",
summary: `已刷新「${payload.account?.nickname || payload.tracked_account_id || "当前账号"}」的最新作品。`,
items: []
};
}
const queuedItems = safeArray(payload.items).filter((item) => item.sync_job_id);
if (queuedItems.length) {
return {
platform: normalizedPlatform,
mode,
tone: payload.failed ? "orange" : "blue",
title: "批量同步已排队",
summary: `已为 ${formatNumber(queuedItems.length)} 个跟踪账号创建后台同步任务${payload.failed ? `,另有 ${formatNumber(payload.failed)} 个失败` : ""}。`,
items: queuedItems
};
}
return {
platform: normalizedPlatform,
mode,
tone: payload.failed ? "orange" : "green",
title: "批量同步已完成",
summary: `已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)} 个` : ""}。`,
items: []
};
}
function getSelectedTopVideoAnalysisResult() {
const accountId = String(getSelectedAccount()?.id || "").trim();
if (!accountId) return null;
return appState.topVideoAnalysisResults?.[accountId] || null;
}
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 || ""),
deploymentScope: String(raw?.deployment_scope || ""),
deploymentLabel: String(raw?.deployment_label || ""),
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 || ""),
runtimeDeviceMode: String(raw?.runtime_device_mode || ""),
runtimeComputeTypeMode: String(raw?.runtime_compute_type_mode || ""),
activeDevice: String(raw?.active_device || ""),
activeComputeType: String(raw?.active_compute_type || ""),
languageMode: String(raw?.language_mode || ""),
modelName: String(raw?.model_name || "")
};
}
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 getAsrRuntimeBadge(detail) {
if (!detail || detail.key !== "asr") return "";
const activeDevice = String(detail.activeDevice || "").trim().toLowerCase();
if (activeDevice === "cuda") return "GPU";
if (activeDevice === "cpu") return "CPU";
const runtimeMode = String(detail.runtimeDeviceMode || "").trim().toLowerCase();
if (runtimeMode === "auto") return "自动";
return runtimeMode ? runtimeMode.toUpperCase() : "";
}
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) {
if (detail.key === "asr") {
const runtimeBadge = getAsrRuntimeBadge(detail);
return { tone: "green", summary: runtimeBadge ? `在线 · ${runtimeBadge}` : "在线" };
}
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
? `打开管理页`
: "",
`设主模型`
].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 = `录制控制`;
}
if (key === "cutvideo") {
extra = isFnosTunnelCutvideo(detail)
? `当前通过 fnOS NAS 隧道访问 ${detail.baseUrl || detail.url || "cutvideo"}`
: `当前直连 ${detail.baseUrl || detail.url || "cutvideo"}`;
}
if (key === "asr") {
const runtimeBadge = getAsrRuntimeBadge(detail) || "待热身";
const computeLabel = detail.activeComputeType || detail.runtimeComputeTypeMode || "auto";
const languageLabel = detail.languageMode || "auto";
extra = `部署:${detail.deploymentLabel || "待确认"} · 当前转写:${runtimeBadge} · ${computeLabel} · 语言 ${languageLabel}`;
if (detail.modelName) {
extra += ` · 当前模型:${detail.modelName}`;
}
} else if (detail.deploymentLabel) {
extra = `部署:${detail.deploymentLabel}`;
}
if (detail.available && !detail.configured && isSuperAdmin()) {
actions = [
actions,
`去管理员配置台`
].filter(Boolean).join("");
}
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) => `
${escapeHtml(item.title || item.remote_name || item.source_url || "录制源")}
${escapeHtml(platformLabel(item.platform))} · ${escapeHtml(item.quality || "原画")} · ${escapeHtml(item.enabled ? "启用中" : "已停用")}
`).join("");
const fileHtml = files.slice(0, 4).map((item) => `
${escapeHtml(item.title || item.name || "录像文件")}
${escapeHtml(item.mtime || "-")} · ${escapeHtml(item.name || item.relative_path || "-")}
打开录像
`).join("");
return `
租户隔离状态
当前只展示你自己名下的录制源、活动录制和录像文件。全局 NAS 配置不会直接暴露给前端。
${escapeHtml(`录制源 ${sources.length}`)}
${escapeHtml(`活动 ${activeItems.length}`)}
${escapeHtml(`文件 ${files.length}`)}
${sourceHtml || `还没有录制源
新增直播源后会自动挂到你的租户空间下。
`}
${fileHtml || `还没有录像文件
录制完成后的文件会只出现在你的当前租户视图里。
`}
`;
}
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) => `
${escapeHtml(item.title || item.name || item.job_id || "任务")}
${escapeHtml(brief(getStorageItemPath(item), 140))}
${escapeHtml(item.status || "-")}
${item.project_name ? `${escapeHtml(item.project_name)}` : ""}
${item.line_type || item.source_type ? `${escapeHtml(item.line_type || item.source_type)}` : ""}
${item.id ? `看详情` : ""}
`).join("") || `${escapeHtml(emptyTitle)}
${escapeHtml(emptyText)}
`;
}
function renderStorageFileCards(items, emptyTitle, emptyText) {
return safeArray(items).slice(0, 4).map((item) => `
${escapeHtml(item.title || item.name || item.relative_path || "文件")}
${escapeHtml(brief(item.relative_path || item.name || item.content_url || "-", 140))}
${(item.updated_at || item.mtime) ? `${escapeHtml(formatDateTime(item.updated_at || item.mtime))}` : ""}
${(item.size_bytes || item.size) ? `${escapeHtml(formatBytes(item.size_bytes || item.size))}` : ""}
${item.id ? `打开文件` : ""}
`).join("") || `${escapeHtml(emptyTitle)}
${escapeHtml(emptyText)}
`;
}
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 `
存储状态
当前实例没有返回存储策略时,先用任务和录像文件做本地观察
降级视图
未拉取到 NAS 策略
当 storage/status 暂时拿不到时,这里会自动退回到任务和录像文件的降级视图。
最近任务 ${escapeHtml(formatNumber(projectJobCount))}
录制源 ${escapeHtml(formatNumber(liveRecorderSources.length))}
录像文件 ${escapeHtml(formatNumber(liveRecorderFiles.length))}
当前项目
${escapeHtml(currentProject?.name || appState.selectedProjectId || "未选择")}
任务 ${escapeHtml(formatNumber(projectJobCount))}
当前账号
${escapeHtml(currentAccount ? getAccountName(currentAccount) : "未选择")}
任务 ${escapeHtml(formatNumber(accountJobCount))}
录像源
${escapeHtml(formatNumber(liveRecorderSources.length))}
仅当前租户可见
最近文件
${escapeHtml(formatNumber(liveRecorderFiles.length))}
可直接打开
最近任务
优先展示 dashboard.recent_jobs,方便在没有 storage/status 时也能继续追踪产物。
${renderStorageJobCards(
fallbackRecentJobs,
"还没有任务样本",
"等你完成一次分析、下载或剪辑后,这里就会出现最近的任务路径和详情入口。"
)}
最近录像文件
录像文件沿用 live-recorder 的当前租户视图,支持直接打开查看。
${renderStorageFileCards(
liveRecorderFiles,
"还没有录像文件",
"录制完成后,这里会直接暴露当前租户的最近文件入口。"
)}
`;
}
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 `
${escapeHtml(strategyMode)}
当前观察范围
${escapeHtml(`项目 ${projectName} · 账号 ${accountName} · 最近任务 ${recentJobs.length} 条 · 最近录像 ${liveRecorderFiles.length} 个`)}
${strategyTags.map((item) => `${escapeHtml(item)}`).join("")}
${usageCards.map((item) => `
${escapeHtml(item.label)}
${escapeHtml(item.value)}
${escapeHtml(item.sub)}
`).join("")}
目录策略
${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(" · "))}
项目层 ${escapeHtml(usage.project_jobs?.path || strategy.jobs?.path || "-")}
账号层 ${escapeHtml(usage.account_jobs?.path || "-")}
产物入口
最近任务、分析产物和录像文件都能直接点开,便于从 NAS 面板跳回详情或原文件。
任务 ${escapeHtml(formatNumber(recentJobs.length))}
产物 ${escapeHtml(formatNumber(recentFiles.length))}
录像 ${escapeHtml(formatNumber(liveRecorderFiles.length))}
最近任务样本
默认取 storage.status 里的 recent_jobs;如果后端没给,会退回到 dashboard.recent_jobs。
${renderStorageJobCards(
recentJobs,
"还没有缓存样本",
"上传视频、导入作品后,这里会显示最近写入 NAS 的缓存路径。"
)}
${recentFiles.length ? `
最近产物文件
后端如果提供产物文件索引,这里会优先直接露出最近写入的文件入口。
${renderStorageFileCards(
recentFiles,
"还没有产物文件",
"当前 storage/status 没有返回可直接打开的产物文件。"
)}
` : ""}
最近录像文件
当前租户一旦写入新录像文件,这里会直接露出最近可回看的录制结果。
${renderStorageFileCards(
liveRecorderFiles,
"还没有录像文件",
"录制完成后的文件会出现在当前租户的录像列表里。"
)}
`;
}
function renderOneLinerActionRegistryPanel() {
const items = safeArray(appState.onelinerActionRegistry);
if (!items.length) {
return `
OneLiner 动作注册表
当前项目还没有单独动作配置,OneLiner 会继续沿用系统默认动作。
当前沿用系统默认动作
你可以等真实使用场景稳定后,再回来给当前项目单独开关、改名或补充动作说明。
系统默认仍可执行
当前项目未单独覆盖
`;
}
const grouped = items.reduce((acc, item) => {
const category = item.category || "custom";
acc[category] = acc[category] || [];
acc[category].push(item);
return acc;
}, {});
return `
OneLiner 动作注册表
把 OneLiner 可执行动作做成租户级注册中心,便于商业化灰度和定制。
${escapeHtml(formatNumber(items.length))} 条
${Object.entries(grouped).map(([category, list]) => `
${escapeHtml(category)}
${escapeHtml(`当前分类下 ${list.length} 条动作。`)}
${list.map((item) => `
${escapeHtml(item.label || item.action_key || "action")}
`).join("")}
`).join("")}
`;
}
function renderTenantQuotaPanel() {
const quota = appState.tenantQuota;
const usage = appState.tenantUsage || quota?.usage || {};
const quotaConfig = quota?.config || {};
const quotaNotice = renderQuotaBlockingNotice();
if (!quota && !usage) {
return `
租户额度与审计
当前项目还没有额度配置,会先按默认不限额模式运行。
当前项目还没有额度配置
先给这个项目补预算、动作配额和存储上限,再逐步收紧风险控制。
创建额度策略
默认不限额
`;
}
const categories = usage?.categories || {};
const recentItems = safeArray(usage?.recent_items);
const categoryEntries = Object.values(categories || {}).sort((left, right) => (right?.cost_cents || 0) - (left?.cost_cents || 0));
const topCategory = categoryEntries[0] || null;
const hasHardLimit = Boolean(
(quota?.monthly_budget_cents || 0) > 0 ||
(quota?.storage_limit_bytes || 0) > 0 ||
(quota?.analysis_quota || 0) > 0 ||
(quota?.copy_quota || 0) > 0 ||
(quota?.ai_video_quota || 0) > 0 ||
(quota?.real_cut_quota || 0) > 0 ||
(quota?.recorder_quota || 0) > 0
);
const usageCount = recentItems.length;
const packageLabel = String(quotaConfig.package_title || quotaConfig.package_label || "").trim() || (hasHardLimit ? "自定义套餐" : "未设套餐");
const packageFocus = String(quotaConfig.package_focus || "").trim();
const warnThreshold = Number(quotaConfig.warn_threshold ?? 0.8);
const forecast = getTenantQuotaForecastValues(quota || {}, usage);
const packageRecommendation = getTenantQuotaPackageRecommendation({
...quota,
package_label: quotaConfig.package_label || "custom"
}, usage);
const quotaTaskTitle = quota?.storage_over_limit
? "先处理存储超限"
: quota?.enabled === false
? "先恢复额度保护"
: !hasHardLimit
? "先补项目额度策略"
: usageCount
? "先检查本周期消耗"
: "先跑出第一条计量";
const quotaTaskSummary = quota?.storage_over_limit
? "当前项目已经命中存储上限,先调整额度或清理产物,再继续高成本动作。"
: quota?.enabled === false
? "额度保护已关闭,当前项目会按无限制模式运行,建议尽快恢复预算与动作保护。"
: !hasHardLimit
? "当前项目虽然可继续运行,但还没有预算和动作配额,先把保护线补齐更稳。"
: usageCount
? "本周期已经开始消耗额度,先看最主要的消耗来源,再决定是否收紧策略。"
: "额度已经建好,但当前周期还没有实际计量,先触发一次真实动作更容易校准策略。";
const quotaTaskActionLabel = quota?.storage_over_limit || !hasHardLimit || quota?.enabled === false ? "调整额度" : usageCount ? "查看最近计量" : "去跑动作";
const quotaTaskAction = quotaTaskActionLabel === "去跑动作" ? "goto-production" : quotaTaskActionLabel === "查看最近计量" ? "" : "open-tenant-quota";
const usagePeriodLabel = usage?.cycle_start ? formatDateTime(usage.cycle_start).slice(0, 10) : "本周期";
const quotaHealthTags = [
quota?.enabled === false ? `额度保护关闭` : `额度保护开启`,
quota?.storage_over_limit ? `存储超限` : `存储正常`,
topCategory ? `${escapeHtml(`主要消耗 ${topCategory.category || "usage"}`)}` : `本周期未产生消耗`,
`${escapeHtml(`推荐 ${packageRecommendation.title}`)}`
];
const cards = [
{ label: "套餐档位", value: packageLabel, sub: packageFocus || `预警阈值 ${formatNumber((warnThreshold || 0.8) * 100)}%` },
{ label: "预算", value: `${formatNumber((quota?.monthly_budget_cents || 0) / 100)} 元`, sub: `已用 ${formatNumber((usage?.total_cost_cents || 0) / 100)} 元 · 剩余 ${formatNumber(forecast.remainingBudgetCents / 100)} 元` },
{ label: "分析配额", value: formatNumber(quota?.analysis_quota || 0), sub: `已用 ${formatNumber(categories.analysis?.quantity || 0)} · 剩余 ${formatNumber(forecast.remainingAnalysisQuota)}` },
{ label: "文案配额", value: formatNumber(quota?.copy_quota || 0), sub: `已用 ${formatNumber(categories.copy?.quantity || 0)} · 剩余 ${formatNumber(forecast.remainingCopyQuota)}` },
{ label: "AI 视频配额", value: formatNumber(quota?.ai_video_quota || 0), sub: `已用 ${formatNumber(categories.ai_video?.quantity || 0)} · 剩余 ${formatNumber(forecast.remainingAiVideoQuota)}` },
{ label: "实拍剪辑配额", value: formatNumber(quota?.real_cut_quota || 0), sub: `已用 ${formatNumber(categories.real_cut?.quantity || 0)} · 剩余 ${formatNumber(forecast.remainingRealCutQuota)}` },
{ label: "存储上限", value: formatBytes(quota?.storage_limit_bytes || 0), sub: `当前 ${formatBytes(usage?.storage_bytes || 0)} · 剩余 ${formatBytes(forecast.remainingStorageBytes)}` }
];
return `
租户额度与审计
${escapeHtml(packageFocus || "预算、动作配额和最近计量都按租户 + 项目隔离,首屏先看风险和下一步。")}
${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")}
${escapeHtml(packageLabel)}
${quota?.storage_over_limit ? `存储超限` : ""}
编辑额度
${escapeHtml(quotaTaskTitle)}
${escapeHtml(quotaTaskSummary)}
${quotaTaskAction ? `${escapeHtml(quotaTaskActionLabel)}` : `${escapeHtml(quotaTaskActionLabel)}`}
${quotaHealthTags.join("")}
${escapeHtml(`套餐 ${packageLabel}`)}
${escapeHtml(`周期 ${usagePeriodLabel}`)}
${escapeHtml(`计量 ${formatNumber(usageCount)} 条`)}
${escapeHtml(`成本 ${(usage?.total_cost_cents || 0) / 100} 元`)}
${escapeHtml(`预警 ${(warnThreshold || 0.8) * 100}%`)}
${renderTenantQuotaForecastTags(quota || {}, usage)}
${quotaNotice}
${cards.map((item) => `
${escapeHtml(item.label)}
${escapeHtml(item.value)}
${escapeHtml(item.sub)}
`).join("")}
最近计量记录
动作执行后会写入租户级 ledger,便于后面做商业化配额、成本和审计。
${recentItems.map((item) => `
${escapeHtml(item.category || "usage")}
${escapeHtml(formatDateTime(item.created_at))}
次数 ${escapeHtml(formatNumber(item.quantity || 0))}
成本 ${(item.cost_cents || 0) / 100} 元
${item.reference_type ? `${escapeHtml(item.reference_type)}` : ""}
${item.reference_id ? `${escapeHtml(brief(item.reference_id, 14))}` : ""}
`).join("") || `
还没有计量记录
等 OneLiner 或生产动作实际执行后,这里会累积本周期的 usage ledger。
`}
`;
}
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 `
${escapeHtml(title)}
${escapeHtml(subtitle || "当前还没有策略摘要。")}
${layers.map((layer) => `${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}`).join("") || `尚未发布`}
${highlights.map((item) => `${escapeHtml(item)}`).join("")}
${activeAdminOverrideNotice?.title ? `
管理员覆盖生效中
${escapeHtml(activeAdminOverrideNotice.summary || activeAdminOverrideNotice.title || "当前这层管理员覆盖会优先于你的个人策略生效。")}
${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")}
${activeAdminOverrideNotice.platform_label ? `${escapeHtml(activeAdminOverrideNotice.platform_label)}` : ""}
` : ""}
${resolvedActions.length ? `
${resolvedActions.map((item) => `
${escapeHtml(item.label || "查看")}
`).join("")}
` : ""}
`;
}
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 `
系统级主 Agent 治理
先管系统默认主 Agent,再按平台补默认策略,普通用户的个性化覆盖会叠加在这些底座之上。
系统主 Agent ${escapeHtml(systemMain?.current_version ? "已发布" : "未发布")}
${escapeHtml(formatNumber(configuredPlatforms.length))} 个平台默认策略
编辑系统主 Agent
系统主 Agent
${escapeHtml(systemMain?.current_version?.summary || "还没有发布系统默认主 Agent 策略。")}
${escapeHtml(systemMain?.current_version ? `版本 ${formatNumber(systemMain.current_version.version_no)}` : "未发布")}
历史 ${escapeHtml(formatNumber(systemMain?.versions?.count || 0))}
历史与回滚
管理员覆盖
${escapeHtml(overrideBundle?.current_version?.summary || `${targetSummary.accountLabel} / ${targetSummary.projectLabel} / ${targetSummary.platformLabel}`)}
${escapeHtml(targetSummary.accountLabel)}
${escapeHtml(targetSummary.projectLabel)}
${escapeHtml(targetSummary.platformLabel)}
${escapeHtml(overrideBundle?.current_version ? `版本 ${formatNumber(overrideBundle.current_version.version_no)}` : "未发布")}
切换目标
编辑覆盖
历史与回滚
${ACTIVE_PLATFORMS.map((platformItem) => {
const item = systemPlatforms.find((entry) => entry?.scope?.platform === platformItem.value) || null;
return `
${escapeHtml(platformItem.label)} 默认策略
${escapeHtml(item?.current_version?.summary || "还没有平台默认策略,当前会沿用系统主 Agent 默认。")}
${escapeHtml(item?.current_version ? `版本 ${formatNumber(item.current_version.version_no)}` : "沿用系统默认")}
历史 ${escapeHtml(formatNumber(item?.versions?.count || 0))}
编辑
历史与回滚
`;
}).join("")}
`;
}
function renderAdminGovernanceAuditPanel() {
const targetSummary = getAdminOverrideTargetSummary();
const audits = safeArray(appState.adminPolicyAudits);
return `
覆盖与审计
按当前目标查看管理员覆盖、系统默认和相关策略变更,避免治理动作只留在弹窗里。
${escapeHtml(targetSummary.accountLabel)}
${escapeHtml(targetSummary.projectLabel)}
${escapeHtml(targetSummary.platformLabel)}
切换目标
当前管理员覆盖
${escapeHtml(appState.adminOverridePolicy?.current_version?.summary || "当前目标还没有管理员覆盖版本。")}
${escapeHtml(appState.adminOverridePolicy?.current_version ? `版本 ${formatNumber(appState.adminOverridePolicy.current_version.version_no || 0)}` : "未发布")}
编辑覆盖
历史与回滚
治理说明
管理员对用户策略的代改不会抹掉用户自己的历史,只会形成更高优先级的覆盖层,并在这里留下审计记录。
最近策略审计
系统默认、用户策略和管理员覆盖都会在这里形成时间线。
${renderPolicyAuditFeed(audits, "当前目标还没有策略审计记录。")}
`;
}
function renderPlatformAgentPanel() {
const items = safeArray(appState.platformAgents);
if (!items.length) {
return `
平台 Agent
当前项目还没有平台 Agent 配置,先从最常用的平台补起。
${ACTIVE_PLATFORMS.map((platformItem) => `
${escapeHtml(platformItem.label)} Agent
当前还没有单独配置,主 Agent 会先沿用系统默认平台方法论。
未单独配置
开始配置
`).join("")}
`;
}
return `
平台 Agent
按用户 + 平台隔离,沉淀该平台的方法论、记忆和技能。
${escapeHtml(formatNumber(items.length))} 个
${items.map((item) => `
${(() => {
const recentExecutionOnelinerConfigStale = isConfigurationVersionStale(
item.recent_execution || {},
appState.onelinerProfile?.current_version || {}
);
const recentExecutionPlatformConfigStale = isConfigurationVersionStale(
{
version_id: item.recent_execution?.platform_agent_profile_version_id,
version_no: item.recent_execution?.platform_agent_profile_version_no,
},
item.current_version || {}
);
return `
${escapeHtml(item.name || item.platform_label)}
${escapeHtml(item.mission || item.notes || "先绑定执行 Agent,再完善任务目标和方法论。")}
${escapeHtml(item.status || "draft")}
${item.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(item.readiness_label)} ${escapeHtml(formatNumber(item.readiness_score || 0))}` : ""}
记忆 ${escapeHtml(formatNumber(item.memory_count))}
技能 ${escapeHtml(formatNumber(item.skill_count))}
${escapeHtml(item.assistant?.name || "未绑 Agent")}
${item.recent_memory || item.recent_skill ? `
${item.recent_memory ? `
最近记忆 · ${escapeHtml(item.recent_memory.title || item.recent_memory.memory_key || "未命名")}
${escapeHtml(brief(item.recent_memory.summary || "暂无摘要", 68))}
` : ""}
${item.recent_skill ? `
最近技能 · ${escapeHtml(item.recent_skill.name || item.recent_skill.skill_key || "未命名")}
${escapeHtml(brief(item.recent_skill.test_spec?.summary || item.recent_skill.method?.summary || "暂无方法摘要", 68))}
${escapeHtml(item.recent_skill.status || "draft")}
得分 ${escapeHtml(formatNumber(item.recent_skill.last_score || 0))}
` : ""}
` : ""}
${item.recent_execution?.run_id ? `
最近执行
${escapeHtml(item.recent_execution.title || item.recent_execution.goal || item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}
${escapeHtml(item.recent_execution.intent_label || "主 Agent 任务")}
${escapeHtml(item.recent_execution.run_status || "done")}
${item.recent_execution.platform_scope ? `${escapeHtml(item.recent_execution.platform_scope === "all_platforms" ? "全平台" : "单平台")}` : ""}
${item.recent_execution.delivery_mode ? `${escapeHtml(item.recent_execution.delivery_mode)}` : ""}
${item.recent_execution.workstream_label ? `${escapeHtml(item.recent_execution.workstream_label)}` : ""}
${item.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(item.recent_execution.oneliner_profile_version_no))}` : ""}
${recentExecutionOnelinerConfigStale ? `主配置已更新` : ""}
${item.recent_execution.platform_agent_profile_version_no ? `${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent v${escapeHtml(formatNumber(item.recent_execution.platform_agent_profile_version_no))}` : ""}
${recentExecutionPlatformConfigStale ? `${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent 已更新` : ""}
${item.recent_execution.source_screen ? `${escapeHtml(screenLabel(item.recent_execution.source_screen) || item.recent_execution.source_screen)}` : ""}
${item.recent_execution.recommended_action?.action ? `${escapeHtml(item.recent_execution.recommended_action.label || "回到业务页")}` : ""}
${item.recent_execution.oneliner_profile_version_no ? `主配置历史` : ""}
${item.recent_execution.platform_agent_profile_version_no ? `平台配置历史` : ""}
查看执行结果
回到主 Agent 查看
` : ""}
查看详情
配置
看配置历史
补记忆
补技能
`;
})()}
`).join("")}
`;
}
function renderAdminOpsPanel() {
if (!isSuperAdmin()) return "";
const overview = appState.adminOpsOverview;
if (!overview) {
return `
尚未拉到概览
刷新后会自动读取失败任务、集成健康和待审事件。
${renderAdminFixRunsPanel()}
${renderRecoveryHistoryPanel()}
`;
}
const incidents = safeArray(overview.incidents).slice(0, 6);
const audits = safeArray(overview.recent_audits).slice(0, 5);
return `
运维与审计 Agent
只给管理员开放,主要盯日志、失败任务和集成异常。
${escapeHtml(formatNumber(overview.incident_count))} 条事件
待处理 ${escapeHtml(formatNumber(overview.open_incident_count || 0))}
错误 ${escapeHtml(formatNumber(overview.severity_counts?.error || 0))}
${escapeHtml(formatNumber(overview.failed_job_count))} 个失败任务
修复计划 ${escapeHtml(formatNumber(overview.fix_run_count || 0))}
重新扫描
${incidents.map((item) => `
${escapeHtml(item.title)}
${escapeHtml(item.summary || "待补详情")}
${escapeHtml(item.severity || "warn")}
${escapeHtml(item.status || "open")}
${item.source_type ? `${escapeHtml(item.source_type)}` : ""}
${item.tenant_user_id ? `租户 ${escapeHtml(brief(item.tenant_user_id, 12))}` : ""}
${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") : ""}
生成修复计划
审计处理
`).join("") || `
当前没有待处理事件
最近主链比较稳定,继续观察即可。
`}
最近审计记录
保留管理员扫描、放行、驳回等动作,方便商业化量产时追责和复盘。
${audits.map((item) => `
${escapeHtml(item.summary || item.action_key || "审计记录")}
${escapeHtml(formatDateTime(item.created_at))}
${escapeHtml(item.action_key || "audit")}
${escapeHtml(item.status || "recorded")}
${item.incident_id ? `事件 ${escapeHtml(brief(item.incident_id, 10))}` : ""}
`).join("") || `
还没有审计记录
等管理员做一次扫描或审计处理后,这里会自动出现。
`}
${renderAdminFixRunsPanel()}
${renderRecoveryHistoryPanel()}
`;
}
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 platform = getAccountPlatform(account);
const benchmarkPath = getWorkbenchRoute(platform, "benchmarkLinks", account.id);
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);
focusDiscoveryRelations();
}
function screenShell(title, subtitle, actionsHtml, bodyHtml) {
const actionLayout = splitPrimaryAction(actionsHtml);
const body = String(bodyHtml || "");
const hasMobileFocusCard = body.includes("mobile-flow-focus-card");
const headClassName = ["screen-head"];
if (hasMobileFocusCard) headClassName.push("screen-head-has-mobile-focus");
return `
${escapeHtml(title)}
${escapeHtml(subtitle)}
${actionLayout.primary ? `
${actionLayout.primary}
` : ""}
${actionLayout.secondary ? `
${actionLayout.secondary}
` : ""}
${body}
`;
}
function splitPrimaryAction(actionsHtml) {
const source = String(actionsHtml || "").trim();
if (!source) return { primary: "", secondary: "" };
const actions = source
.split(/(?=