feat: align async workbench feedback and add ci
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-03-30 20:07:53 +08:00
parent c400c1af44
commit f492cb3f83
4 changed files with 219 additions and 10 deletions

63
.github/workflows/ci.yml vendored Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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 `
<div class="layout-grid grid-main">
<div class="side-stack">
@@ -5523,6 +5605,29 @@ function renderDiscoveryOverviewSection({ selected, selectedProject, importedSou
</ul>
</div>
</div>
${topVideoBatchResult ? `
<div class="panel pad" style="box-shadow:none; margin-top:16px;">
<div class="panel-head">
<div>
<h3>最近高分拆解</h3>
<div class="panel-subtitle">这批结果已经回流到当前账号页,不会只停留在顶部提醒里。</div>
</div>
<span class="tag blue">${escapeHtml(formatNumber(topVideoBatchResult.analyzed_count || safeArray(topVideoBatchResult.items).length))} 条</span>
</div>
<div class="list">
${safeArray(topVideoBatchResult.items).slice(0, 3).map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.video_title || "高分作品")}</h4>
<p>${escapeHtml(item.summary_text || "已完成拆解。")}</p>
<div class="task-meta">
<span class="tag blue">得分 ${escapeHtml(formatNumber(item.performance_score || 0))}</span>
${safeArray(item.parsed_json?.borrow_points).slice(0, 2).map((point) => `<span class="tag">${escapeHtml(point)}</span>`).join("")}
</div>
</div>
`).join("")}
</div>
</div>
` : ""}
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
@@ -5622,6 +5727,7 @@ function renderDiscoveryScreen() {
const selectedProject = getSelectedProject();
const importedSources = getCurrentProjectSourcesForAccount(selected, selectedProject?.id || "");
const tracked = selected?.id ? isTrackedAccount(selected.id) : false;
const topVideoBatchResult = getSelectedTopVideoAnalysisResult();
const isMobileUi = isMobileViewport();
const discoveryHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "discovery",
@@ -5676,7 +5782,8 @@ function renderDiscoveryScreen() {
topVideos,
reports,
latestVideos,
currentPlatformLabel
currentPlatformLabel,
topVideoBatchResult
});
} else if (activeTab === "snapshots") {
detailBodyHtml = renderDouyinInsightPanel();
@@ -5851,6 +5958,7 @@ function renderTrackingScreen() {
const digestItems = getTrackingDigestItems(12, { platform: currentPlatform });
const platformCursor = getTrackingCursorForPlatform(currentPlatform) || appState.lastSeenAt;
const cursorLabel = platformCursor ? formatDateTime(platformCursor) : "尚未记录";
const trackingNotice = getCurrentTrackingRefreshNotice(currentPlatform);
const trackingHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "tracking",
sourceActionKey: "tracking-handoff",
@@ -5880,6 +5988,17 @@ function renderTrackingScreen() {
<span class="chip">上次已读 ${escapeHtml(cursorLabel)}</span>
</div>
</div>
${trackingNotice ? `
<div class="task-item" style="margin-top:16px;">
<h4>${escapeHtml(trackingNotice.title || "后台同步状态")}</h4>
<p>${escapeHtml(trackingNotice.summary || "最近一次跟踪同步已经进入后台执行。")}</p>
<div class="task-meta">
<span class="tag ${trackingNotice.tone === "orange" ? "orange" : trackingNotice.tone === "green" ? "green" : "blue"}">${escapeHtml(trackingNotice.mode === "single" ? "单账号" : "批量同步")}</span>
${trackingNotice.items?.[0]?.sync_job_id ? `<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(trackingNotice.items[0].sync_job_id)}">看任务详情</span>` : ""}
${actionTag("去生产中心", "goto-production")}
</div>
</div>
` : ""}
<div class="mobile-only mobile-flow-focus-card">
<div class="mobile-flow-focus-head">
<strong>当前跟踪任务</strong>
@@ -9569,6 +9688,15 @@ function openAnalyzeTopVideosAction() {
temperature: 0.25
}
});
appState.topVideoAnalysisResults = {
...(appState.topVideoAnalysisResults || {}),
[account.id]: {
...result,
account_id: account.id,
platform,
created_at: new Date().toISOString()
}
};
rememberAction("高分作品分析完成", `已补分析 ${formatNumber(result.analyzed_count)} 条高分作品。`, "green", result);
await loadPlatformAccount(platform, account.id);
renderAll();

View File

@@ -285,6 +285,20 @@ test("quota and review screens foreground live next-step guidance", () => {
assert.match(review, /已发布/);
});
test("tracking refresh and top-video analysis flows expose async feedback inside the workbench", () => {
const tracking = extractBetween(APP, "function renderTrackingScreen()", "function renderAutomationScreen()");
const discovery = extractBetween(APP, "function renderDiscoveryOverviewSection(", "function renderDiscoveryRelationsSection(");
assert.match(APP, /function summarizeTrackingRefreshPayload\(/);
assert.match(APP, /function rememberTrackingRefreshNotice\(/);
assert.match(tracking, /批量同步已排队|单账号同步已排队|后台同步状态/);
assert.match(tracking, /去生产中心/);
assert.match(APP, /function getSelectedTopVideoAnalysisResult\(/);
assert.match(discovery, /最近高分拆解/);
assert.match(discovery, /这批结果已经回流到当前账号页/);
});
test("discovery and production screens expose compact mobile flow summaries", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");