diff --git a/CHANGELOG.md b/CHANGELOG.md index b43ef77..318ad12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,6 +146,18 @@ ## 2026-04-04 +### CI / smoke 护栏加固 + +- `scripts/check_repo_baseline.sh` 现在会在校验 Web 资产时显式检查 `storyforge-*.js` 是否真的存在,避免后续打包产物变化后只留下一个“看起来在跑、实际漏掉文件”的空洞通过。 +- `scripts/smoke_fnos_storyforge_lan.sh` 现在对 `StoryForge` 首页、runtime 配置和 `19181` 兼容入口都做固定字符串断言;其中 `19181` 会校验真实兼容业务台文案,而不是只要抓取成功就算通过。 +- 这轮护栏加固保持了现有基线语义不变,只把原来偏宽松的检查收紧成可追踪的真实断言。 + +### 主 Agent 配置与执行结果继续打通 + +- `跟踪账号 -> 立即同步` 现在在同步成功后会自动打开对应 `sync_job_id` 的任务详情,不再停留在一条“已同步”的提示上。 +- 主 Agent 的执行结果卡、OneLiner 助手消息卡,现在都能直接跳转到 `主配置历史` 和 `平台 Agent 配置历史`,把一次执行和当时生效的治理版本真正连起来。 +- `execution_card` 里新增了主配置与平台 Agent 配置的 `version_id`,后续继续做更深的版本对比和追溯时不需要再靠标题文本猜版本。 + ### 平台 Agent 执行回写闭环 - 平台 Agent 配置现在不只是“被主 Agent 带进执行链”,还会在主 Agent 完成态后反向记录最近一次执行信息。 diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 477b0d3..8d51f2f 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -3598,6 +3598,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "platform_label": str(plan.get("platform_label") or "").strip() or "待判断", "active_admin_override_notice": _parse_json(row.get("active_admin_override_notice_json"), {}), "oneliner_profile_version": { + "version_id": str(oneliner_profile_version.get("id") or "").strip(), "version_no": int(oneliner_profile_version.get("version_no") or 0), "title": str(oneliner_profile_version.get("title") or "").strip(), "summary": str(oneliner_profile_version.get("summary") or "").strip(), @@ -3607,6 +3608,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: "platform_label": str(platform_agent_profile.get("platform_label") or "").strip(), "name": str(platform_agent_profile.get("name") or "").strip(), "assistant_name": str(platform_agent_profile.get("assistant_name") or "").strip(), + "version_id": str(((platform_agent_profile.get("current_version") or {}).get("id") or "").strip()), "version_no": int(((platform_agent_profile.get("current_version") or {}).get("version_no") or 0)), "version_title": str(((platform_agent_profile.get("current_version") or {}).get("title") or "").strip()), "version_summary": str(((platform_agent_profile.get("current_version") or {}).get("summary") or "").strip()), diff --git a/scripts/check_repo_baseline.sh b/scripts/check_repo_baseline.sh index d42055e..1fa0bb8 100755 --- a/scripts/check_repo_baseline.sh +++ b/scripts/check_repo_baseline.sh @@ -40,9 +40,24 @@ for path in sorted(pathlib.Path("n8n/workflows").glob("*.json")): PY echo "[5/6] 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 +validate_node_script() { + for file in "$@"; do + if [ ! -f "$file" ]; then + echo "missing required script file: $file" >&2 + exit 1 + fi + node --check "$file" + done +} + +validate_node_script web/storyforge-web-v4/assets/app.js + +set -- web/storyforge-web-v4/assets/storyforge-*.js +if [ "$1" = 'web/storyforge-web-v4/assets/storyforge-*.js' ]; then + echo "missing required script bundle: web/storyforge-web-v4/assets/storyforge-*.js" >&2 + exit 1 +fi +validate_node_script "$@" node --check scripts/douyin-browser-capture/control_panel.mjs echo "[6/6] validate homepage and workbench tests" diff --git a/scripts/smoke_fnos_storyforge_lan.sh b/scripts/smoke_fnos_storyforge_lan.sh index 7553113..96e81d9 100755 --- a/scripts/smoke_fnos_storyforge_lan.sh +++ b/scripts/smoke_fnos_storyforge_lan.sh @@ -41,12 +41,12 @@ token_file="$tmp_dir/token.txt" echo "[1/6] check fnOS web" curl_fetch "$WEB_URL/" >"$index_file" -rg -q "StoryForge" "$index_file" +rg -Fq "StoryForge" "$index_file" echo "web ok" echo "[2/6] check runtime config" curl_fetch "$WEB_URL/assets/storyforge-runtime-config.js" >"$runtime_file" -rg -q "$BACKEND_URL" "$runtime_file" +rg -Fq "$BACKEND_URL" "$runtime_file" echo "runtime config ok" echo "[3/6] check collector healthz" @@ -115,6 +115,10 @@ if not payload: raise SystemExit("empty cutvideo bootstrap payload") print("cutvideo bootstrap ok") ' "$bootstrap_file" +if ! rg -Fq "数字人网页业务台" "$compat_file" && ! rg -Fq "BUSINESS CONSOLE" "$compat_file"; then + echo "compat page does not look like the expected business console" >&2 + exit 1 +fi echo "compat ok" echo "fnOS lan smoke passed:" diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index 3278ff3..d747e80 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -905,6 +905,12 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertEqual(detail_response.status_code, 200, detail_response.text) detail_payload = detail_response.json() self.assertEqual(detail_payload["run_status"], "done") + self.assertTrue( + (((detail_payload.get("result") or {}).get("execution_card") or {}).get("oneliner_profile_version") or {}).get("version_id") + ) + self.assertTrue( + (((detail_payload.get("result") or {}).get("execution_card") or {}).get("platform_agent_profile") or {}).get("version_id") + ) self.assertEqual( (((detail_payload.get("result") or {}).get("execution_card") or {}).get("platform_agent_profile") or {}).get("version_no"), rollback_profile_payload["current_version"]["version_no"], diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index afa12a8..9205985 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -913,6 +913,9 @@ function ensureActionUi() { function renderActionFields(fields) { return fields.map((field) => { + if (field.hidden) { + return ""; + } const common = `data-action-field="${escapeHtml(field.name)}"`; if (field.type === "html") { return ` @@ -1360,6 +1363,8 @@ function renderOneLinerMessagesHtml() { ${executionCard.platform_agent_name ? `${escapeHtml(executionCard.platform_agent_name)}` : ""} ${executionCard.assistant_name ? `${escapeHtml(executionCard.assistant_name)}` : ""} ${profileVersion.version_no ? `配置 v${escapeHtml(formatNumber(profileVersion.version_no || 0))}` : ""} + ${profileVersion.version_no ? `看主配置历史` : ""} + ${executionCard.platform && executionCard.platform_agent_profile?.version_no ? `看平台配置历史` : ""} ${executionCard.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(executionCard.readiness_label)} ${escapeHtml(formatNumber(executionCard.readiness_score || 0))}` : ""} ${executionCard.primary_action?.key ? `${escapeHtml(executionCard.primary_action.label || "执行下一步")}` : ""} @@ -2020,6 +2025,7 @@ function renderOneLinerExecutionPayloadHtml(payload) { ${platformAgentProfile.name ? `${escapeHtml(platformAgentProfile.name)}` : ""} ${platformAgentProfile.assistant_name ? `${escapeHtml(platformAgentProfile.assistant_name)}` : ""} ${platformAgentProfile.version_no ? `${escapeHtml(platformLabel(platformAgentProfile.platform || payload.platform || ""))} Agent v${escapeHtml(formatNumber(platformAgentProfile.version_no || 0))}` : ""} + ${platformAgentProfile.platform && platformAgentProfile.version_no ? `看平台配置历史` : ""} ${platformAgentProfile.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(platformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(platformAgentProfile.readiness_score || 0))}` : ""} @@ -2728,6 +2734,9 @@ async function refreshTrackedAccountAction(trackedAccountId) { payload ); await bootstrap(); + if (payload.sync_job_id) { + openJobDetailAction(payload.sync_job_id); + } } finally { setBusy(false, ""); } @@ -3209,6 +3218,7 @@ function openDashboardProjectSwitcher() { } const selectedProject = getSelectedProject(); const projects = safeArray(appState.dashboard?.projects); + const isMobileViewport = typeof window !== "undefined" && window.matchMedia?.("(max-width: 760px)")?.matches; const projectCards = projects.map((project) => { const stats = getProjectStats(project.id); const reviewCount = getProjectReviews(project.id).length; @@ -3259,11 +3269,10 @@ function openDashboardProjectSwitcher() { ` }, - { name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options } + { name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options, hidden: isMobileViewport } ], onOpen: ({ fields, submit }) => { const select = fields.querySelector('[data-action-field="projectId"]'); - const isMobileViewport = typeof window !== "undefined" && window.matchMedia?.("(max-width: 760px)")?.matches; if (submit && isMobileViewport) { submit.hidden = true; } diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 183f352..dca66bd 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -138,12 +138,14 @@ test("mobile project sheets support direct project picking and zoom-safe form co const createProject = extractBetween(APP, "async function createProject()", "function openPreferredModelAction()"); assert.match(APP, /async function applySelectedProject\(projectId = ""\)/); assert.match(projectSwitcher, /data-project-choice=/); + assert.match(projectSwitcher, /hidden:\s*isMobileViewport/); assert.match(projectSwitcher, /onOpen:\s*\(/); assert.match(projectSwitcher, /select\.value = nextProjectId/); assert.match(projectSwitcher, /window\.matchMedia\?\.\("\(max-width: 760px\)"\)\?\.matches/); assert.match(projectSwitcher, /submit\.hidden = true/); assert.match(projectSwitcher, /closeActionModal\(\);/); assert.match(projectSwitcher, /await applySelectedProject\(nextProjectId\);/); + assert.match(APP, /if \(field\.hidden\) \{\s*return "";/); assert.match(applySelectedProject, /loadStorageStatus\(appState\.selectedProjectId \|\| ""\)/); assert.match(applySelectedProject, /loadAgentControlSurfaces\(appState\.selectedProjectId \|\| ""\)/); assert.match(createProject, /onOpen:\s*\(/); @@ -832,6 +834,21 @@ test("key workbench screens expose contextual handoff-to-main-agent actions", () assert.match(review, /sourceScreen: "review"/); }); +test("tracked-account refresh opens the created sync task when the backend returns one", () => { + const refresh = extractBetween(APP, "async function refreshTrackedAccountAction(trackedAccountId)", "function getSelectedProject()"); + assert.match(refresh, /sync_job_id/); + assert.match(refresh, /openJobDetailAction\(payload\.sync_job_id\)/); +}); + +test("main agent execution cards can jump to oneliner and platform profile history", () => { + const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderAutoConnectingScreen(screenTitle, nextStepText)"); + assert.match(messages, /data-action="open-oneliner-profile-history"/); + assert.match(messages, /data-action="open-platform-agent-profile-history"/); + assert.match(APP, /function renderOneLinerExecutionPayloadHtml\(payload\)/); + assert.match(APP, /data-action="open-oneliner-profile-history"/); + assert.match(APP, /data-action="open-platform-agent-profile-history"/); +}); + test("oneliner runtime shows grouped run health summary above the current run card", () => { const runtime = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()"); assert.match(runtime, /近期运行概况/);