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, /近期运行概况/);