diff --git a/scripts/check_repo_baseline.sh b/scripts/check_repo_baseline.sh
index 37f231c..9fdaf9e 100755
--- a/scripts/check_repo_baseline.sh
+++ b/scripts/check_repo_baseline.sh
@@ -16,13 +16,13 @@ need_cmd node
cd "$ROOT"
-echo "[1/4] compile collector-service"
+echo "[1/5] compile collector-service"
python3 -m compileall collector-service/app >/dev/null
-echo "[2/4] validate docker compose"
+echo "[2/5] validate docker compose"
docker compose config >/dev/null
-echo "[3/4] validate n8n workflows"
+echo "[3/5] validate n8n workflows"
python3 - <<'PY'
import json
import pathlib
@@ -33,10 +33,13 @@ for path in sorted(pathlib.Path("n8n/workflows").glob("*.json")):
print(f"workflow ok: {path.name}")
PY
-echo "[4/4] validate web scripts"
+echo "[4/5] validate web scripts"
for file in web/storyforge-web-v4/assets/app.js web/storyforge-web-v4/assets/storyforge-*.js; do
node --check "$file"
done
node --check scripts/douyin-browser-capture/control_panel.mjs
+echo "[5/5] validate homepage dashboard tests"
+node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs
+
echo "baseline checks passed"
diff --git a/tests/test_production_baseline.py b/tests/test_production_baseline.py
index 7df5565..41a3924 100644
--- a/tests/test_production_baseline.py
+++ b/tests/test_production_baseline.py
@@ -249,6 +249,10 @@ class ProductionBaselineTests(unittest.TestCase):
]:
self.assertIn(expected, content)
+ def test_baseline_script_covers_homepage_dashboard_node_test(self) -> None:
+ script = (ROOT / "scripts" / "check_repo_baseline.sh").read_text(encoding="utf-8")
+ self.assertIn("dashboard-home.test.mjs", script)
+
def test_healthz_exposes_lan_routing_summary(self) -> None:
response = self.client.get("/healthz")
self.assertEqual(response.status_code, 200, response.text)
diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md
index bb47533..191bb20 100644
--- a/web/storyforge-web-v4/README.md
+++ b/web/storyforge-web-v4/README.md
@@ -6,7 +6,7 @@
- 页面:`index.html`
- 样式:`assets/styles.css`
-- 页面交互:`assets/storyforge-session-store.js`、`assets/storyforge-api-client.js`、`assets/storyforge-platform-runtime.js`、`assets/app.js`
+- 页面交互:`assets/storyforge-session-store.js`、`assets/storyforge-api-client.js`、`assets/storyforge-platform-runtime.js`、`assets/storyforge-dashboard-home.js`、`assets/app.js`
## 当前定位
@@ -30,6 +30,10 @@
- 生产中心
- 发布与复盘
- 额度
+- 首页已切到“人类决策优先”结构:
+ - 先显示当前项目上下文与 `1 主 2 次` 今日动作
+ - 再显示 `项目进度 / 重点账号·对标 / 生产任务` tab 概览
+ - 管理员配置台通过独立导航进入,不再挤占首页主体
## 当前已接入的真实能力
diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index 9b2b484..52e3a61 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -27,6 +27,7 @@ const appState = {
discoveryQuery: "",
currentPlatform: localStorage.getItem(STORAGE_KEY + ":currentPlatform") || "",
selectedProjectId: "",
+ dashboardOverviewTab: "project_progress",
selectedAssistantId: "",
lastSeenAt: SESSION_STORE.getLastSeenAt(Date.now()),
trackingCursorMap: {},
@@ -55,6 +56,7 @@ const appState = {
autoConnectAttempted: false,
autoConnectSuppressed: false,
autoConnectError: "",
+ dashboardActionReason: null,
busy: false,
message: "",
lastAction: null,
@@ -2094,6 +2096,329 @@ function getTrackingAccounts() {
);
}
+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 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 })
+ };
+}
+
+function openDashboardProjectSwitcher() {
+ const options = getProjectOptions();
+ if (!options.length) {
+ rememberAction("还没有项目", "先创建一个项目,再让首页跟着当前项目切换。", "orange");
+ setScreen("intake");
+ return;
+ }
+ openActionModal({
+ title: "切换当前项目",
+ description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。",
+ submitLabel: "切换项目",
+ fields: [
+ { name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options }
+ ],
+ onSubmit: async (payload) => {
+ appState.selectedProjectId = payload.projectId || "";
+ setBusy(true, "正在切换项目视图...");
+ try {
+ if (backendSupports("/v2/storage/status")) {
+ await loadStorageStatus(appState.selectedProjectId || "");
+ } else {
+ appState.storageStatus = null;
+ }
+ await loadAgentControlSurfaces(appState.selectedProjectId || "");
+ if (appState.selectedOnelinerSessionId) {
+ await loadOneLinerMessages(appState.selectedOnelinerSessionId);
+ }
+ } finally {
+ setBusy(false, "");
+ }
+ renderAll();
+ }
+ });
+}
+
+function openDashboardActionReasonAction(index) {
+ const model = buildDashboardHomeModel();
+ const actions = [model.primaryAction, ...safeArray(model.secondaryActions)];
+ const action = actions[Number(index)] || actions[0];
+ if (!action) return;
+ appState.dashboardActionReason = {
+ title: action.title,
+ reason: action.reason,
+ sourceLabel: model.actionSourceLabel,
+ badges: safeArray(action.badges),
+ goLabel: action.goLabel || "去处理"
+ };
+ openActionModal({
+ title: "动作原因",
+ description: "首页只放最短判断,这里再把原因展开一层。",
+ hideSubmit: true,
+ fields: [
+ {
+ type: "html",
+ label: "动作详情",
+ html: `
+
+
+
${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();
@@ -3492,6 +3817,29 @@ function renderAdminFixRunsPanel() {
`;
}
+function renderAdminWorkbenchScreen() {
+ if (!isSuperAdmin()) {
+ return screenShell(
+ "管理员配置台",
+ "仅超级管理员可见。",
+ "",
+ renderEmptyState("无权限", "请使用超级管理员账号访问管理员配置台。")
+ );
+ }
+ return screenShell(
+ "管理员配置台",
+ "系统级依赖、存储、平台 Agent 与运维治理。",
+ "",
+ `
+ ${renderIntegrationOverviewPanel({ showActions: false })}
+ ${renderStorageStatusPanel()}
+ ${renderPlatformAgentPanel()}
+ ${renderOneLinerActionRegistryPanel()}
+ ${renderAdminOpsPanel()}
+ `
+ );
+}
+
function renderDashboardScreen() {
if (!appState.session) {
return screenShell(
@@ -3509,128 +3857,15 @@ function renderDashboardScreen() {
renderEmptyState("工作区暂未就绪", appState.message || "如果账号未审批通过,当前页会先停在待审批状态。")
);
}
- const dashboard = appState.dashboard;
- const projects = safeArray(dashboard.projects);
- const jobs = safeArray(dashboard.recent_jobs);
- const assistants = safeArray(dashboard.assistants);
- const accounts = safeArray(appState.accounts);
- const trackedAccounts = getTrackingAccounts();
- const digestItems = getTrackingDigestItems(3);
- const actions = [];
- if (!projects.length) actions.push("先新建一个项目");
- if (!assistants.length) actions.push("先创建第一个 Agent");
- if (!accounts.length) actions.push("先导入一个平台主页或作品");
- if (!trackedAccounts.length && accounts.length) actions.push("挑 1 个重点账号加入跟踪");
- if (jobs.some((item) => item.status !== "completed")) actions.push("处理进行中的生产任务");
- if (!actions.length) actions.push("继续补高分对标并安排生产");
+ const dashboardHomeRenderer = window.StoryForgeDashboardHome;
+ const homeModel = buildDashboardHomeModel();
return screenShell(
"项目总台",
- "先看项目状态、待办动作和高价值对标。",
- `${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("OneLiner", "open-oneliner")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
- `
-
-
活跃项目${escapeHtml(formatNumber(projects.length))}
-
导入内容${escapeHtml(formatNumber(appState.contentSources.length))}
-
跟踪账号${escapeHtml(formatNumber(trackedAccounts.length))}
-
Agent${escapeHtml(formatNumber(assistants.length))}
-
生产任务${escapeHtml(formatNumber(jobs.length))}
-
-
- ${renderIntegrationOverviewPanel({ compact: true })}
-
-
-
-
-
当前主流程
-
项目 → Agent → 调研 → 导入并绑定 → 生产 → 复盘
-
- 我的项目
- 找对标
- 跟踪账号
- Agent
- 生产中心
-
-
-
-
${escapeHtml(formatNumber(actions.length))} 项
-
- ${actions.map((item, index) => `
-
-
${index + 1}. ${escapeHtml(item)}
-
${escapeHtml(index === 0 ? "先把最影响主流程的动作做掉。" : "做完上一步再继续推进。")}
-
- `).join("")}
-
-
- ${renderLastActionCard()}
-
-
-
- ${accounts.slice(0, 3).map((account) => `
-
-
-
${escapeHtml(initials(getAccountName(account)))}
-
-
${escapeHtml(getAccountName(account))}
-
${escapeHtml(account.signature || getAccountProfileUrl(account) || `已同步${platformLabel(getAccountPlatform(account))}账号`)}
-
-
-
- 作品 ${escapeHtml(formatNumber(account.video_summary?.count))}
- 均播 ${escapeHtml(formatNumber(account.video_summary?.avg_play))}
- ${escapeHtml(account.sync_status || "synced")}
-
-
- `).join("") || `
先到“找对标”导入一个账号。
`}
-
-
-
-
-
-
当前项目
-
${escapeHtml(getSelectedProject()?.name || "还没有项目")}
-
-
知识库${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).knowledgeBases.length : 0))}
-
Agent${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).assistants.length : 0))}
-
任务${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).jobs.length : 0))}
-
来源${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).sources.length : 0))}
-
-
-
- ${renderPlatformAgentPanel()}
-
- ${renderStorageStatusPanel()}
-
-
${escapeHtml(daysSince(appState.lastSeenAt))} 天汇总
-
- ${digestItems.map((item) => `
-
-
${escapeHtml(item.account?.nickname || "未命名账号")} · ${escapeHtml(item.video?.title || item.video?.description || "最新作品")}
-
${escapeHtml(item.summary || `最近发布时间 ${formatDateTime(item.video?.published_at)},适合继续交给 Agent 做借鉴点标注。`)}
-
- ${escapeHtml(getPlatformShortLabel(item.platform || item.account?.platform || getCurrentPlatformValue()))}
- ${escapeHtml(item.is_high_value ? "高价值" : "可学习")}
- ${item.assistant_name ? `${escapeHtml(item.assistant_name)}` : ""}
-
-
- `).join("") || `
还没有日报
先把重点账号加入跟踪,日报才会开始累积。
`}
-
-
-
-
-
- ${jobs.filter((item) => item.status === "failed").slice(0, 3).map((job) => `
-
-
${escapeHtml(job.title)}
-
${escapeHtml(job.error || "任务失败,请重新进入生产中心查看。")}
-
失败${escapeHtml(job.line_type || "analysis")}
-
- `).join("") || `
当前无异常
最近任务运行正常,继续推进导入和生产即可。
`}
-
-
-
-
- `
+ "先做最能推进当前项目的一步,再按需看概览。",
+ `${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
+ dashboardHomeRenderer?.renderDashboardHome
+ ? dashboardHomeRenderer.renderDashboardHome(homeModel, { escapeHtml })
+ : renderEmptyState("首页模块未加载", "请刷新页面后重试。")
);
}
@@ -4501,9 +4736,23 @@ function renderTopbar() {
}
}
+function syncRoleGatedNav() {
+ const allowAdmin = isSuperAdmin();
+ document.querySelectorAll("[data-role-gate]").forEach((element) => {
+ const gate = element.getAttribute("data-role-gate");
+ const visible = gate === "super_admin" ? allowAdmin : true;
+ element.classList.toggle("hidden", !visible);
+ element.hidden = !visible;
+ });
+ if (!allowAdmin && appState.screen === "admin-workbench") {
+ appState.screen = "dashboard";
+ }
+}
+
function renderAll() {
renderTopbar();
renderAuthUi();
+ syncRoleGatedNav();
screenMap.dashboard.innerHTML = renderDashboardScreen();
screenMap.intake.innerHTML = renderProjectsScreen();
screenMap.discovery.innerHTML = renderDiscoveryScreen();
@@ -4514,6 +4763,9 @@ function renderAll() {
screenMap.production.innerHTML = renderProductionScreen();
screenMap.review.innerHTML = renderReviewScreen();
screenMap.credits.innerHTML = renderCreditsScreen();
+ if (screenMap["admin-workbench"]) {
+ screenMap["admin-workbench"].innerHTML = renderAdminWorkbenchScreen();
+ }
renderOneLinerUi();
setScreen(screenMap[appState.screen] ? appState.screen : "dashboard");
}
@@ -7232,6 +7484,14 @@ document.addEventListener("click", async (event) => {
setScreen("playbook");
return;
}
+ if (name === "goto-owned") {
+ setScreen("owned");
+ return;
+ }
+ if (name === "goto-tracking") {
+ setScreen("tracking");
+ return;
+ }
if (name === "goto-production") {
setScreen("production");
return;
@@ -7272,6 +7532,19 @@ document.addEventListener("click", async (event) => {
openCreateAssistantAction();
return;
}
+ if (name === "open-dashboard-project-switcher") {
+ openDashboardProjectSwitcher();
+ return;
+ }
+ if (name === "open-dashboard-action-reason") {
+ openDashboardActionReasonAction(action.dataset.dashboardActionIndex || "0");
+ return;
+ }
+ if (name === "select-dashboard-tab") {
+ appState.dashboardOverviewTab = action.dataset.dashboardTab || "project_progress";
+ renderAll();
+ return;
+ }
if (name === "open-oneliner-profile") {
openOneLinerProfileAction();
return;
diff --git a/web/storyforge-web-v4/assets/storyforge-dashboard-home.js b/web/storyforge-web-v4/assets/storyforge-dashboard-home.js
new file mode 100644
index 0000000..0d7673c
--- /dev/null
+++ b/web/storyforge-web-v4/assets/storyforge-dashboard-home.js
@@ -0,0 +1,259 @@
+(function () {
+ function defaultEscapeHtml(value) {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+ }
+
+ function safeArray(value) {
+ return Array.isArray(value) ? value : [];
+ }
+
+ function renderTags(items, escapeHtml) {
+ return safeArray(items)
+ .map((item) => `${escapeHtml(item)}`)
+ .join("");
+ }
+
+ function renderActionButton(config, escapeHtml) {
+ if (!config?.label || !config?.action) return "";
+ const tone = config.tone || "secondary";
+ const attrs = safeArray(config.attrs).join(" ");
+ return `
+
+ `;
+ }
+
+ function createDashboardHomeModel(raw) {
+ const trackedAccountsCount = Number(raw?.trackedAccountsCount || 0);
+ const assistantCount = Number(raw?.assistantCount || 0);
+ const jobCount = Number(raw?.jobCount || 0);
+ const hasProject = raw?.hasProject != null
+ ? Boolean(raw.hasProject)
+ : Boolean(String(raw?.currentProjectName || "").trim() && String(raw?.currentProjectName || "").trim() !== "还没有项目");
+
+ const actions = [];
+ if (!hasProject) {
+ actions.push({
+ title: "先创建或切换到一个项目",
+ reason: "首页动作和概览都跟随当前项目,先定项目再推进后续工作。",
+ badges: ["最优先", "项目入口"],
+ goAction: "goto-intake",
+ goLabel: "去项目",
+ agentLabel: "交给主 Agent"
+ });
+ } else if (trackedAccountsCount > 0) {
+ actions.push({
+ title: "先补抖音重点对标的高分作品分析",
+ reason: "最近有新作品,但还没形成高分样本。",
+ badges: ["最优先", "预计 10 分钟判断", "关联:重点账号"],
+ goAction: "goto-discovery",
+ goLabel: "去找对标",
+ agentLabel: "交给主 Agent"
+ });
+ } else if (assistantCount <= 0) {
+ actions.push({
+ title: "先为当前项目创建第一个 Agent",
+ reason: "项目已经存在,但还没有可承接分析和生产动作的 Agent。",
+ badges: ["最优先", "Agent 缺失"],
+ goAction: "goto-playbook",
+ goLabel: "去创建",
+ agentLabel: "交给主 Agent"
+ });
+ } else {
+ actions.push({
+ title: "继续补高分对标并安排生产",
+ reason: "当前项目没有更高优先阻塞,继续推动主流程即可。",
+ badges: ["默认推进", "主流程"],
+ goAction: "goto-production",
+ goLabel: "去处理",
+ agentLabel: "交给主 Agent"
+ });
+ }
+
+ if (jobCount > 0) {
+ actions.push({
+ title: "确认一个待执行的生产计划",
+ reason: "素材和结论都在,只差最后确认。",
+ goAction: "goto-production",
+ goLabel: "去处理"
+ });
+ }
+
+ actions.push({
+ title: "更新重点账号的跟踪摘要",
+ reason: "有新动态,但不值得占据大块首页空间。",
+ goAction: "goto-tracking",
+ goLabel: "去处理"
+ });
+
+ while (actions.length < 3) {
+ actions.push({
+ title: "继续补高分对标并安排生产",
+ reason: "当前项目没有更多高优先动作时,保持主流程推进。",
+ goAction: "goto-production",
+ goLabel: "去处理"
+ });
+ }
+
+ const summaryTabs = safeArray(raw?.summaryTabs).length
+ ? raw.summaryTabs
+ : [
+ { key: "project_progress", label: "项目进度", value: "0 / 5", hint: "等待项目建立", active: raw?.dashboardOverviewTab !== "focus_accounts" && raw?.dashboardOverviewTab !== "production_jobs" },
+ { key: "focus_accounts", label: "重点账号 / 对标", value: String(trackedAccountsCount), hint: "重点对象", active: raw?.dashboardOverviewTab === "focus_accounts" },
+ { key: "production_jobs", label: "生产任务", value: String(jobCount), hint: "当前项目任务", active: raw?.dashboardOverviewTab === "production_jobs" }
+ ];
+
+ return {
+ workspaceLabel: raw?.workspaceLabel || "当前工作区",
+ currentProjectName: raw?.currentProjectName || "还没有项目",
+ actionSourceLabel: raw?.actionSourceLabel || "规则推荐",
+ contextLinks: safeArray(raw?.contextLinks),
+ primaryAction: actions[0],
+ secondaryActions: actions.slice(1, 3),
+ summaryTabs,
+ activeTabLabel: raw?.activeTabLabel || (summaryTabs.find((item) => item.active)?.label || "项目进度"),
+ overviewBodyHtml: raw?.overviewBodyHtml || ""
+ };
+ }
+
+ function renderSecondaryAction(item, index, escapeHtml) {
+ return `
+
+
${index + 2}
+
+
${escapeHtml(item.title)}
+
${escapeHtml(item.reason)}
+
+
+ ${renderActionButton({
+ label: "原因",
+ action: "open-dashboard-action-reason",
+ tone: "secondary",
+ attrs: [`data-dashboard-action-index="${index + 1}"`]
+ }, escapeHtml)}
+ ${renderActionButton({
+ label: item.goLabel || "去处理",
+ action: item.goAction || "goto-production",
+ tone: "secondary"
+ }, escapeHtml)}
+
+
+ `;
+ }
+
+ function renderDashboardHome(model, helpers = {}) {
+ const escapeHtml = helpers.escapeHtml || defaultEscapeHtml;
+ const summaryTabs = safeArray(model?.summaryTabs);
+ const contextLinks = safeArray(model?.contextLinks);
+ const primaryAction = model?.primaryAction || {};
+ const secondaryActions = safeArray(model?.secondaryActions);
+
+ return `
+
+
+
+
+
项目总台
+
当前项目优先,先做决定再展开细节。
+
+
+
+
+
+ 当前工作区
+ ${escapeHtml(model?.workspaceLabel || "当前工作区")}
+
+
+
+
+ ${contextLinks.map((item) => `
+
+ `).join("")}
+
+
+
+
+
+
+
+
今天先做什么
+
首页只保留 1 主 2 次动作,避免同时看太多说明。
+
+
${escapeHtml(model?.actionSourceLabel || "规则推荐")}
+
+
+
+
${escapeHtml(primaryAction.title || "先建立当前项目的主流程")}
+
${escapeHtml(primaryAction.reason || "先完成当前项目最影响推进的一步。")}
+
+ ${renderTags(primaryAction.badges, escapeHtml)}
+
+
+
+ ${renderActionButton({
+ label: "查看原因",
+ action: "open-dashboard-action-reason",
+ tone: "secondary",
+ attrs: ['data-dashboard-action-index="0"']
+ }, escapeHtml)}
+ ${renderActionButton({
+ label: primaryAction.goLabel || "去处理",
+ action: primaryAction.goAction || "goto-production",
+ tone: "secondary"
+ }, escapeHtml)}
+ ${renderActionButton({
+ label: primaryAction.agentLabel || "交给主 Agent",
+ action: "open-oneliner",
+ tone: "primary"
+ }, escapeHtml)}
+
+
+
+ ${secondaryActions.map((item, index) => renderSecondaryAction(item, index, escapeHtml)).join("")}
+
+
+
+
+
+
+
项目概览
+
同一块内容区按 tab 切换,不再把所有细节同时摊开。
+
+
${escapeHtml(model?.activeTabLabel || "项目进度")}
+
+
+ ${summaryTabs.map((item) => `
+
+ `).join("")}
+
+
${model?.overviewBodyHtml || ""}
+
+
+ `;
+ }
+
+ window.StoryForgeDashboardHome = {
+ createDashboardHomeModel,
+ renderDashboardHome
+ };
+})();
diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css
index 11448b2..b925b47 100644
--- a/web/storyforge-web-v4/assets/styles.css
+++ b/web/storyforge-web-v4/assets/styles.css
@@ -187,6 +187,10 @@ select {
border-color: rgba(79, 143, 238, 0.22);
}
+.hidden {
+ display: none !important;
+}
+
.content {
padding: 18px 22px 26px;
min-width: 0;
@@ -576,6 +580,163 @@ select {
line-height: 1.4;
}
+.dashboard-home {
+ display: grid;
+ gap: 18px;
+}
+
+.dashboard-context-panel,
+.dashboard-priority-panel,
+.dashboard-overview-panel {
+ display: grid;
+ gap: 14px;
+}
+
+.dashboard-context-row {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.dashboard-context-left,
+.dashboard-context-right {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.dashboard-context-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ border-radius: 14px;
+ border: 1px solid var(--line);
+ background: var(--panel-soft);
+ padding: 10px 12px;
+ color: var(--muted);
+}
+
+.dashboard-context-chip strong {
+ color: var(--text);
+ font-size: 12px;
+}
+
+.dashboard-context-chip-button {
+ cursor: pointer;
+}
+
+.dashboard-action-primary {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 18px;
+ align-items: center;
+ padding: 18px;
+ border-radius: 18px;
+ border: 1px solid rgba(79, 143, 238, 0.16);
+ background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
+}
+
+.dashboard-action-copy h4,
+.dashboard-action-copy h5 {
+ margin: 0;
+}
+
+.dashboard-action-copy p {
+ margin: 8px 0 0;
+ color: var(--muted);
+ line-height: 1.6;
+}
+
+.dashboard-action-buttons {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.dashboard-action-secondary-list {
+ display: grid;
+ gap: 10px;
+}
+
+.dashboard-action-secondary {
+ display: grid;
+ grid-template-columns: 34px minmax(0, 1fr) auto;
+ gap: 14px;
+ align-items: center;
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ padding: 14px 16px;
+ background: var(--panel-soft);
+}
+
+.dashboard-action-index {
+ width: 34px;
+ height: 34px;
+ display: grid;
+ place-items: center;
+ border-radius: 12px;
+ background: var(--blue-50);
+ color: var(--blue-700);
+ font-weight: 700;
+}
+
+.dashboard-overview-tabs {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.dashboard-overview-tab {
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ background: var(--panel-soft);
+ padding: 14px;
+ text-align: left;
+ cursor: pointer;
+ display: grid;
+ gap: 6px;
+ color: var(--text);
+}
+
+.dashboard-overview-tab small,
+.dashboard-overview-tab span {
+ color: var(--muted);
+}
+
+.dashboard-overview-tab strong {
+ font-size: 20px;
+ line-height: 1.1;
+}
+
+.dashboard-overview-tab.is-active {
+ border-color: rgba(79, 143, 238, 0.28);
+ background: linear-gradient(180deg, #f7fbff 0%, #edf5ff 100%);
+ box-shadow: inset 0 0 0 1px rgba(79, 143, 238, 0.08);
+}
+
+.dashboard-overview-body {
+ border-top: 1px solid var(--line);
+ padding-top: 14px;
+}
+
+.dashboard-overview-detail,
+.dashboard-overview-list,
+.dashboard-overview-grid {
+ display: grid;
+ gap: 14px;
+}
+
+.dashboard-overview-note,
+.dashboard-overview-empty {
+ margin-top: 0;
+}
+
+.dashboard-overview-card {
+ min-height: 100%;
+}
+
.stat-card {
padding: 18px;
border-radius: 20px;
@@ -1877,6 +2038,19 @@ tbody tr:hover {
width: 100%;
}
+ .dashboard-action-primary,
+ .dashboard-action-secondary {
+ grid-template-columns: 1fr;
+ }
+
+ .dashboard-action-buttons {
+ justify-content: flex-start;
+ }
+
+ .dashboard-overview-tabs {
+ grid-template-columns: 1fr;
+ }
+
.workspace-switch {
padding: 10px 12px;
}
diff --git a/web/storyforge-web-v4/index.html b/web/storyforge-web-v4/index.html
index 6f78058..5927b72 100644
--- a/web/storyforge-web-v4/index.html
+++ b/web/storyforge-web-v4/index.html
@@ -64,6 +64,10 @@
¤
额度
+