diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..74077ac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: StoryForge CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + baseline: + name: Baseline checks + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Python dependencies + run: python -m pip install --upgrade pip && pip install -r collector-service/requirements.txt + + - name: Run repository baseline + run: ./scripts/check_repo_baseline.sh + + backend-tests: + name: Backend tests + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Python dependencies + run: python -m pip install --upgrade pip && pip install -r collector-service/requirements.txt + + - name: Run backend unittest suite + run: python -m unittest tests.test_main_agent_governance tests.test_platform_contracts tests.test_production_baseline + + web-tests: + name: Web tests + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Run web node tests + run: node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs web/storyforge-web-v4/tests/workbench-pages.test.mjs diff --git a/README.md b/README.md index 75a6273..eaefaf2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ StoryForge 现在拆成独立项目目录,和 `AI-glasses` 分开维护。 - `data/collector/`:SQLite、任务文件、下载产物 - `docs/`:审计、实施计划、联调说明、当前 MVP 状态 +## CI + +仓库里的最小 GitHub/Gitea Actions workflow 位于 [`.github/workflows/ci.yml`](/Users/kris/code/StoryForge-gitea/.github/workflows/ci.yml),会在 `push`、`pull_request` 和 `workflow_dispatch` 时运行基线检查、后端单元测试和 Web Node 测试。 + ## 产品手册 - [新媒体运营中台产品逻辑手册](./docs/PRODUCT_LOGIC_NEW_MEDIA_OPERATING_SYSTEM_2026-03-22.md) diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 278c52f..82231e8 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -59,6 +59,7 @@ const appState = { trackingCursorMap: {}, trackingAccounts: [], trackingDigest: null, + trackingRefreshNotice: null, reviews: [], liveRecorderSources: [], liveRecorderStatus: null, @@ -103,7 +104,8 @@ const appState = { mainAgentLanding: null, lastGeneratedCopy: null, lastSimilaritySearch: null, - lastJobDetail: null + lastJobDetail: null, + topVideoAnalysisResults: {} }; let PLATFORM_RUNTIME = null; @@ -2679,10 +2681,12 @@ async function refreshTrackingAccountsAction() { const payload = await storyforgeFetch(trackingRefreshPath, { method: "POST" }); + const refreshNotice = summarizeTrackingRefreshPayload(payload, platform, "batch"); + rememberTrackingRefreshNotice(refreshNotice); rememberAction( - "跟踪已同步", - `已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)} 个` : ""}。`, - payload.failed ? "orange" : "green", + refreshNotice?.title || "跟踪已同步", + refreshNotice?.summary || `已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)} 个` : ""}。`, + refreshNotice?.tone || (payload.failed ? "orange" : "green"), payload ); await bootstrap(); @@ -2708,13 +2712,15 @@ async function refreshTrackedAccountAction(trackedAccountId) { const payload = await storyforgeFetch(trackingRefreshPath, { method: "POST" }); + const refreshNotice = summarizeTrackingRefreshPayload(payload, platform, "single"); + rememberTrackingRefreshNotice(refreshNotice); const success = payload.success !== false; rememberAction( - success ? "单账号已同步" : "单账号刷新失败", - success + refreshNotice?.title || (success ? "单账号已同步" : "单账号刷新失败"), + refreshNotice?.summary || (success ? `已刷新「${payload.account?.nickname || trackedAccountId}」的最新作品。` - : `暂时无法刷新「${payload.account?.nickname || trackedAccountId}」:${payload.message || "请稍后重试"}`, - success ? (safeArray(payload.sync_errors).length ? "orange" : "green") : "orange", + : `暂时无法刷新「${payload.account?.nickname || trackedAccountId}」:${payload.message || "请稍后重试"}`), + refreshNotice?.tone || (success ? (safeArray(payload.sync_errors).length ? "orange" : "green") : "orange"), payload ); await bootstrap(); @@ -3371,6 +3377,82 @@ function getLatestVideos(limit = 3) { .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) @@ -5477,7 +5559,7 @@ function renderDetailTabs(stateKey, tabs) { `; } -function renderDiscoveryOverviewSection({ selected, selectedProject, importedSources, tracked, workbenchReason, topVideos, reports, latestVideos, currentPlatformLabel }) { +function renderDiscoveryOverviewSection({ selected, selectedProject, importedSources, tracked, workbenchReason, topVideos, reports, latestVideos, currentPlatformLabel, topVideoBatchResult }) { return `
${escapeHtml(item.summary_text || "已完成拆解。")}
+ +${escapeHtml(trackingNotice.summary || "最近一次跟踪同步已经进入后台执行。")}
+ +