feat: align async workbench feedback and add ci
This commit is contained in:
63
.github/workflows/ci.yml
vendored
Normal file
63
.github/workflows/ci.yml
vendored
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()");
|
||||
|
||||
Reference in New Issue
Block a user