chore: sync storyforge handoff state
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-05-02 17:50:21 +08:00
parent 6f0d944a75
commit 65db3cd336
20 changed files with 3780 additions and 250 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,12 @@
function makePlatformRoutes(platform) {
return {
accounts: `/v2/${platform}/accounts`,
syncAccount: `/v2/${platform}/accounts/sync`,
workspace: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/workspace`,
snapshots: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/snapshots`,
snapshotDetail: (accountId, snapshotId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/snapshots/${encodeURIComponent(snapshotId)}`,
creatorFields: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/creator-fields`,
analysisReports: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/analysis-reports`,
videos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos?limit=80`,
analyzeAccount: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/analysis`,
analyzeTopVideos: (accountId) => `/v2/${platform}/accounts/${encodeURIComponent(accountId)}/videos/analyze-top`,

View File

@@ -115,10 +115,18 @@ test("mobile action sheets and oneliner runtime behave like bottom sheets", () =
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-composer\s*\{[\s\S]*position:\s*sticky/);
});
test("opening OneLiner clears the transient loading state after the panel is hydrated", () => {
test("opening OneLiner keeps global loading clear and uses panel-local hydration status", () => {
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(actions, /name === "open-oneliner"[\s\S]*setBusy\(true,\s*"正在打开 OneLiner\.\.\."\)/);
assert.match(actions, /name === "open-oneliner"[\s\S]*finally \{[\s\S]*setBusy\(false,\s*""\);[\s\S]*renderAll\(\);[\s\S]*\}/);
const branch = extractBetween(actions, "if (name === \"open-oneliner\") {", "if (name === \"close-oneliner\") {");
assert.match(APP, /function setOneLinerHydrating\(next,\s*message = ""\)/);
assert.match(branch, /setOneLinerHydrating\(true,\s*"正在同步 OneLiner 上下文\.\.\."\)/);
assert.doesNotMatch(branch, /setBusy\(true,\s*"正在打开 OneLiner\.\.\."\)/);
assert.match(branch, /finally \{[\s\S]*setOneLinerHydrating\(false,\s*""\);[\s\S]*renderAll\(\);[\s\S]*\}/);
});
test("opening OneLiner opens the panel before waiting for control-surface hydration", () => {
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(actions, /name === "open-oneliner"[\s\S]*setOneLinerHydrating\(true,\s*"正在同步 OneLiner 上下文\.\.\."\)[\s\S]*openOneLinerPanel\(\);[\s\S]*renderAll\(\);[\s\S]*await loadAgentControlSurfaces/);
});
test("project creation and switching use in-app sheets instead of browser prompts", () => {
@@ -295,6 +303,57 @@ test("admin workbench exposes a dedicated model access workspace and actions", (
assert.match(clickActions, /name === "open-admin-huobao-ai-config"/);
});
test("owned account and admin model pages surface creator-center and model setup quick actions", () => {
const owned = extractBetween(APP, "function renderOwnedScreen()", "function renderPlaybookScreen()");
const modelOverview = extractBetween(APP, "function renderAdminModelCapabilityOverviewPanel()", "function renderAdminModelAccessPanel()");
assert.match(owned, /创作者中心账号分析/);
assert.match(owned, /登录抖音创作者中心/);
assert.match(owned, /登录快手创作者中心/);
assert.match(owned, /open-creator-center-sync/);
assert.match(owned, /data-platform="douyin" data-sync-origin="owned"/);
assert.match(owned, /data-platform="kuaishou" data-sync-origin="owned"/);
assert.match(owned, /去找对标看快照/);
assert.match(modelOverview, /语言模型缺口/);
assert.match(modelOverview, /ASR 缺口/);
assert.match(modelOverview, /文生图缺口/);
assert.match(modelOverview, /图生图缺口/);
assert.match(modelOverview, /生视频缺口/);
assert.match(modelOverview, /open-admin-system-model/);
assert.match(modelOverview, /open-admin-runtime-config/);
assert.match(modelOverview, /open-admin-huobao-ai-config/);
assert.match(modelOverview, /data-service-type="image"/);
assert.match(modelOverview, /data-service-type="video"/);
});
test("ai video and huobao admin config expose preflight and connection test flows", () => {
const guardSource = extractBetween(APP, "function getPipelineGuard(kind) {", "function getIntegrationCards()");
const preflightSource = extractBetween(APP, "function getAiVideoProviderPreflight(provider = \"doubao\", model = \"\") {", "function renderAiVideoProviderHintHtml(");
const aiVideoSource = extractBetween(APP, "function openCreateAiVideoAction(defaults = {})", "function openCreateRealCutAction(");
const huobaoSource = extractBetween(APP, "async function openAdminHuobaoConfigAction(serviceType = \"video\", configId = \"\") {", "function openAdminHuobaoConfigDeleteAction(");
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(guardSource, /detail\.videoConfigReady === false/);
assert.match(guardSource, /detail\.videoConfigCount <= 0/);
assert.match(preflightSource, /Huobao 视频配置未就绪,请先在管理后台完成视频配置/);
assert.match(preflightSource, /请先在 Huobao 启用至少一条视频配置/);
assert.match(preflightSource, /Huobao 启用中的视频配置未包含所选 Seedance 模型/);
assert.match(preflightSource, /所选 Seedance 模型/);
assert.match(preflightSource, /seedance-2\.0-pro/);
assert.match(aiVideoSource, /const videoPreflight = getAiVideoProviderPreflight\(normalizedProvider, normalizedVideoModel\)/);
assert.match(aiVideoSource, /if \(!videoPreflight\.ready\) throw new Error\(videoPreflight\.reason\);/);
assert.match(aiVideoSource, /renderAiVideoProviderHintHtml\(defaultVideoProvider, defaultVideoModel\)/);
assert.match(APP, /renderAiVideoProviderHintHtml\(provider, modelInput\?\.value \|\| seedanceModel\)/);
assert.match(aiVideoSource, /videoPreflight\.reason/);
assert.match(huobaoSource, /submitLabel: existing \? "保存配置" : "创建配置并测试"/);
assert.match(huobaoSource, /testLabel:\s*"测试配置"/);
assert.match(huobaoSource, /const testPayload = await storyforgeFetch\("\/v2\/admin\/model-access\/huobao-configs\/test"/);
assert.match(huobaoSource, /keepOpen: true/);
assert.match(huobaoSource, /测试通过/);
assert.match(clickActions, /name === "test-sheet"[\s\S]*submitActionModal\("onTest"/);
});
test("governance and quota panels use real empty-state language instead of backend-sync placeholders", () => {
const actionRegistry = extractBetween(APP, "function renderOneLinerActionRegistryPanel()", "function renderTenantQuotaPanel()");
const tenantQuota = extractBetween(APP, "function renderTenantQuotaPanel()", "function policyScopeTagLabel(");
@@ -448,10 +507,12 @@ test("job detail and follow-up flows use direct generate-copy execution and pers
});
test("ai video provider hint links super admins into the huobao video config workspace", () => {
const hintSource = extractBetween(APP, "function renderAiVideoProviderHintHtml(provider = \"doubao\") {", "function renderAiVideoProviderMemoryHtml(");
const hintSource = extractBetween(APP, "function renderAiVideoProviderHintHtml(provider = \"doubao\", model = \"\") {", "function renderAiVideoProviderMemoryHtml(");
const clickActions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(hintSource, /打开视频引擎配置/);
assert.match(hintSource, /focus-huobao-video-config/);
assert.match(hintSource, /当前模型/);
assert.match(hintSource, /配置中的模型/);
assert.match(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAdminModelAccessWorkspace\("admin-model-video-anchor"\)/);
});
@@ -552,6 +613,17 @@ test("production queue promotes intake entrypoints so import flows are reachable
assert.match(mobileDeck, /actionTag\("视频录制", "focus-live-recorder-maintenance"\)/);
});
test("discovery and production promote creator-center sync entrypoints for douyin and kuaishou", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
assert.match(discovery, /button\("登录抖音创作者中心", "open-creator-center-sync"/);
assert.match(discovery, /button\("登录快手创作者中心", "open-creator-center-sync"/);
assert.match(discovery, /actionTag\("接入抖音创作者中心", "open-creator-center-sync"/);
assert.match(production, /button\("接入抖音创作者中心", "open-creator-center-sync"/);
assert.match(production, /button\("接入快手创作者中心", "open-creator-center-sync"/);
assert.match(production, /actionTag\("接入抖音创作者中心", "open-creator-center-sync"/);
});
test("discovery page promotes selected-account actions into direct execute flows", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const discoveryOverview = extractBetween(APP, "function renderDiscoveryOverviewSection(", "function renderDiscoveryRelationsSection(");
@@ -577,6 +649,46 @@ test("direct discovery analysis actions gracefully fall back to forms when no ac
assert.match(APP, /if \(name === "direct-analyze-top-videos"\)[\s\S]*openAnalyzeTopVideosAction\(\);/);
});
test("creator center sync modal covers douyin and kuaishou creator-center login flows", () => {
const clickActions = extractBetween(APP, 'document.addEventListener("click", async (event) => {', 'navButtons.forEach((button) => {');
const creatorCenterSync = extractBetween(APP, "function openCreatorCenterSyncAction(defaults = {}) {", "function openImportVideoLinkAction()");
assert.match(APP, /function openCreatorCenterSyncAction\(defaults = \{\}\)/);
assert.match(APP, /title: "\$\{platformLabel\(platform\)\}创作者中心接入"/);
assert.match(APP, /name: "sessionCookie", label: "登录 Cookie", type: "password"/);
assert.match(APP, /name: "creatorCenterUrls", label: "创作者中心页面"/);
assert.match(APP, /name: "autoAnalyze", label: "同步后自动分析", type: "checkbox", value: true/);
assert.match(APP, /const syncPath = `\/v2\/\$\{platform\}\/accounts\/sync`/);
assert.match(APP, /const analyzePath = getWorkbenchRoute\(platform, "analyzeAccount", workspace\?\.account\?\.id \|\| ""\)/);
assert.match(creatorCenterSync, /auto_analyze_top_videos: true/);
assert.match(creatorCenterSync, /const topVideoAnalyses = safeArray\(analyzeResult\?\.top_video_analyses\)/);
assert.match(creatorCenterSync, /appState\.topVideoAnalysisResults = \{/);
assert.match(APP, /manual_creator_pages: \[\]/);
assert.match(APP, /await loadPlatformAccount\(platform, workspace\.account\.id\)/);
assert.match(creatorCenterSync, /focusDiscoveryTopVideoInsights\(\)/);
assert.match(APP, /focusDiscoveryInsights\(\)/);
assert.match(clickActions, /name === "open-creator-center-sync"[\s\S]*openCreatorCenterSyncAction\(\{/);
});
test("workbench account loading fetches snapshots and creator fields for all supported platforms", () => {
const loadPlatformAccountSource = extractBetween(APP, "async function loadPlatformAccount(", "async function hydrateWorkbenchDataAfterBootstrap(");
const snapshotDetailAction = extractBetween(APP, "async function openDouyinSnapshotDetailAction(", "function renderLiveRecorderManagementPanel()");
assert.match(loadPlatformAccountSource, /const snapshotsPath = getWorkbenchRoute\(normalizedPlatform, "snapshots", accountId\);/);
assert.match(loadPlatformAccountSource, /const analysisReportsPath = getWorkbenchRoute\(normalizedPlatform, "analysisReports", accountId\);/);
assert.match(loadPlatformAccountSource, /const creatorFieldsPath = getWorkbenchRoute\(normalizedPlatform, "creatorFields", accountId\);/);
assert.match(loadPlatformAccountSource, /const snapshotDetailPath = getWorkbenchRoute\(normalizedPlatform, "snapshotDetail", accountId, nextSnapshotId\);/);
assert.doesNotMatch(loadPlatformAccountSource, /normalizedPlatform === "douyin"/);
assert.match(APP, /function renderCreatorInsightPanel\(\)/);
assert.match(snapshotDetailAction, /const detailPath = getWorkbenchRoute\(platform, "snapshotDetail", selected\.id, snapshotId\);/);
});
test("creator insight panel surfaces report model and linked-account context", () => {
const insightPanel = extractBetween(APP, "function renderCreatorInsightPanel() {", "function renderDouyinInsightPanel() {");
assert.match(insightPanel, /report\.model_profile_ids\?\.length/);
assert.match(insightPanel, /report\.linked_account_ids\?\.length/);
assert.match(insightPanel, /已绑对标/);
assert.match(insightPanel, /模型 /);
});
test("mobile discovery prioritizes the selected-account task flow before the scrollable account list", () => {
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
assert.match(discovery, /mobile-discovery-priority/);
@@ -1179,7 +1291,7 @@ test("ai video form explains where Seedance 火山配置 lives", () => {
'document.addEventListener("click", async (event) => {',
'navButtons.forEach((button) => {'
);
assert.match(APP, /function renderAiVideoProviderHintHtml\(provider = "doubao"\)/);
assert.match(APP, /function renderAiVideoProviderHintHtml\(provider = "doubao", model = ""\)/);
assert.match(APP, /Seedance 2\.0 走火山视频配置/);
assert.match(APP, /\/settings\/ai-config/);
assert.match(APP, /视频 -> 火山引擎/);
@@ -1197,6 +1309,8 @@ test("ai video form explains where Seedance 火山配置 lives", () => {
assert.match(APP, /当前项目最近视频引擎/);
assert.match(APP, /上次创建 AI 视频时使用的是/);
assert.match(APP, /modelInput\.placeholder = provider === "seedance2" \? "例如seedance-2\.0-pro" : "留空则沿用当前默认视频模型"/);
assert.match(APP, /modelInput\.addEventListener\("input", \(\) => \{/);
assert.match(APP, /hintHtml\.innerHTML = renderAiVideoProviderHintHtml\(provider, modelInput\?\.value \|\| seedanceModel\)/);
assert.match(APP, /bindAiVideoProviderRecommendations\(fields, \{ seedanceModel: defaultVideoModel \|\| "seedance-2\.0-pro" \}\)/);
assert.match(APP, /saveAiVideoPreferences\(project\.id, \{/);
assert.match(clickActions, /name === "focus-huobao-video-config"[\s\S]*focusAutomationHealthWorkspace\("integration-huobao-anchor"\)/);
@@ -1424,11 +1538,57 @@ test("discovery analysis actions focus the most relevant detail tab after succes
assert.match(APP, /function focusDiscoveryInsights\(\)/);
assert.match(APP, /function focusDiscoveryTopVideoInsights\(\)/);
assert.match(APP, /function focusDiscoveryRelations\(\)/);
assert.match(analyzeAccount, /safeArray\(result\.top_video_analyses\)\.length/);
assert.match(analyzeAccount, /appState\.topVideoAnalysisResults = \{/);
assert.match(analyzeAccount, /focusDiscoveryTopVideoInsights\(\)/);
assert.match(analyzeAccount, /focusDiscoveryInsights\(\)/);
assert.match(analyzeTopVideos, /focusDiscoveryTopVideoInsights\(\)/);
assert.match(similaritySearch, /focusDiscoveryRelations\(\)/);
});
test("discovery relations section surfaces candidate provenance and overlap context", () => {
const relations = extractBetween(APP, "function renderDiscoveryRelationsSection(linkedAccounts, similarCandidates) {", "function renderDiscoveryScreen()");
assert.match(relations, /const searchContext = appState\.lastSimilaritySearch\?\.context \|\| \{\}/);
assert.match(relations, /const dimensions = candidate\.dimensions \|\| candidate\.dimensions_json \|\| \{\}/);
assert.match(relations, /dimensions\.source === "linked_account"/);
assert.match(relations, /dimensions\.source === "manual_url"/);
assert.match(relations, /source_overlap != null/);
assert.match(relations, /requirement_overlap != null/);
assert.match(relations, /最近这一轮会优先合并已绑关系、手动主页和本地账号池/);
});
test("similarity search state is isolated per selected account", () => {
const state = extractBetween(APP, "const appState = {", "};\n\nlet PLATFORM_RUNTIME");
const hydrateSource = extractBetween(APP, "async function hydrateWorkbenchDataAfterBootstrap(", "async function bootstrap()");
const loadPlatformAccountSource = extractBetween(APP, "async function loadPlatformAccount(", "async function hydrateWorkbenchDataAfterBootstrap(");
const markSavedCandidate = extractBetween(APP, "function markSavedCandidate(candidate, links) {", "async function saveCandidateAsBenchmark(candidateIndex, relationType = \"benchmark\") {");
const similaritySearch = extractBetween(APP, "function openSimilaritySearchAction()", "function openBenchmarkLinkAction(defaults = {})");
assert.match(state, /similaritySearchResultsByAccount: \{\}/);
assert.match(hydrateSource, /if \(!nextAccountId\) \{[\s\S]*appState\.lastSimilaritySearch = null;/);
assert.match(loadPlatformAccountSource, /appState\.lastSimilaritySearch = appState\.similaritySearchResultsByAccount\?\.\[accountId\] \|\| null;/);
assert.doesNotMatch(loadPlatformAccountSource, /appState\.similarSearchDetail/);
assert.match(markSavedCandidate, /appState\.similaritySearchResultsByAccount = \{/);
assert.match(similaritySearch, /appState\.similaritySearchResultsByAccount = \{/);
assert.match(similaritySearch, /\[account\.id\]: detail/);
});
test("logout clears account-scoped discovery caches", () => {
const logoutSource = extractBetween(APP, "async function logoutSession() {", "async function loadKnowledgeDocuments(");
assert.match(logoutSource, /appState\.lastSimilaritySearch = null;/);
assert.match(logoutSource, /appState\.similaritySearchResultsByAccount = \{\};/);
assert.match(logoutSource, /appState\.topVideoAnalysisResults = \{\};/);
});
test("account loading clears stale workbench detail before surfacing fetch failures", () => {
const loadPlatformAccountSource = extractBetween(APP, "async function loadPlatformAccount(", "async function hydrateWorkbenchDataAfterBootstrap(");
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.lastSimilaritySearch = null;/);
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.selectedWorkspace = null;/);
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.selectedVideos = \{ items: \[], meta: \{}, top_scored_video_ids: \[], latest_video_ids: \[], high_score_threshold: 60 \};/);
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.snapshots = \[];/);
assert.match(loadPlatformAccountSource, /catch \(error\) \{[\s\S]*appState\.analysisReports = \[];/);
assert.match(loadPlatformAccountSource, /throw error;/);
});
test("tracking and benchmark actions land on the most relevant workbench area after success", () => {
const trackSelected = extractBetween(APP, "function openTrackSelectedAccountAction()", "function openImportVideoLinkAction()");
const saveCandidate = extractBetween(APP, "async function saveCandidateAsBenchmark(candidateIndex, relationType = \"benchmark\")", "function screenShell(title, subtitle, actionsHtml, bodyHtml)");