diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed3923..c7cb806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - `OneLiner 会话 / 运行详情 / 治理控制面 / integrations / live-recorder` 这些固定接口也已经切成 live-first,请求失败才降级,不再先被陈旧 capability 表拦住。 - 任务恢复链会优先真实调用 `/v2/explore/jobs/{job_id}/retry`,只有接口真的不存在时才回退到手动恢复模板。 - `找对标 / 跟踪账号` 里一批已经失效的 “当前平台待接入” 按钮禁用与入口分支已删除,当前 active 平台都直接走真实路由,失败时再给真实反馈。 +- 工作台前端已经清掉浏览器 `alert` 弹窗,缺对象、权限不足、刷新失败和加载失败都会回到站内反馈,不再把用户从当前流程里打断出去。 ### NAS 联调与回归 @@ -38,7 +39,7 @@ - Web: `http://192.168.31.188:19192/` - Collector: `http://192.168.31.188:19193/healthz` - 当前基线通过: - - 前端测试 `60/60` + - 前端测试 `61/61` - `bash scripts/check_repo_baseline.sh` - `bash scripts/smoke_fnos_storyforge_lan.sh` diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 8b3f66a..a18b543 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -9201,7 +9201,8 @@ async function openPlatformAgentDetailAction(platform) { const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform()); const profile = safeArray(appState.platformAgents).find((item) => item.platform === normalizedPlatform) || null; if (!profile) { - alert("没有找到这个平台 Agent。"); + rememberAction("平台 Agent 不存在", "当前没有找到这条平台 Agent,请先刷新当前页面。", "orange"); + renderAll(); return; } const [memoriesPayload, skillsPayload] = await Promise.all([ @@ -9379,7 +9380,8 @@ function openActionRegistryEditAction(actionKey) { const project = requireSelectedProject(); const actionDef = safeArray(appState.onelinerActionRegistry).find((item) => item.action_key === actionKey) || null; if (!actionDef) { - alert("没有找到这条动作定义。"); + rememberAction("动作定义不存在", "当前没有找到这条 OneLiner 动作定义,请先刷新当前页面。", "orange"); + renderAll(); return; } openActionModal({ @@ -9495,7 +9497,8 @@ function openCreateAssistantAction() { function openEditAssistantAction(assistantId = "") { const assistant = safeArray(appState.dashboard?.assistants).find((item) => item.id === assistantId) || getSelectedAssistant(); if (!assistant) { - alert("请先选择一个 Agent"); + rememberAction("请先选择 Agent", "当前还没有可编辑的 Agent,先在项目里选择或创建一个。", "orange"); + renderAll(); return; } const modelOptions = getModelOptions(); @@ -9718,12 +9721,14 @@ async function scanAdminOpsAction() { function openAdminIncidentReviewAction(incidentId) { if (!isSuperAdmin()) { - alert("只有平台管理者才能审计处理故障事件。"); + rememberAction("权限不足", "只有平台管理者才能审计处理故障事件。", "orange"); + renderAll(); return; } const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId); if (!incident) { - alert("没有找到这条故障事件。"); + rememberAction("故障事件不存在", "当前没有找到这条故障事件,请先重新扫描。", "orange"); + renderAll(); return; } openActionModal({ @@ -9776,12 +9781,14 @@ function openAdminIncidentReviewAction(incidentId) { function openAdminRepairPlanAction(incidentId) { if (!isSuperAdmin()) { - alert("只有平台管理者才能生成修复计划。"); + rememberAction("权限不足", "只有平台管理者才能生成修复计划。", "orange"); + renderAll(); return; } const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId); if (!incident) { - alert("没有找到这条故障事件。"); + rememberAction("故障事件不存在", "当前没有找到这条故障事件,请先重新扫描。", "orange"); + renderAll(); return; } openActionModal({ @@ -9810,12 +9817,14 @@ function openAdminRepairPlanAction(incidentId) { function openAdminFixRunDetailAction(runId) { if (!isSuperAdmin()) { - alert("只有平台管理者才能查看修复计划。"); + rememberAction("权限不足", "只有平台管理者才能查看修复计划。", "orange"); + renderAll(); return; } const run = safeArray(appState.adminFixRuns.length ? appState.adminFixRuns : appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId); if (!run) { - alert("没有找到这条修复计划。"); + rememberAction("修复计划不存在", "当前没有找到这条修复计划,请先刷新管理员配置台。", "orange"); + renderAll(); return; } openActionModal({ @@ -9857,12 +9866,14 @@ function openAdminFixRunDetailAction(runId) { function openAdminFixRunAuditAction(runId) { if (!isSuperAdmin()) { - alert("只有平台管理者才能审计修复计划。"); + rememberAction("权限不足", "只有平台管理者才能审计修复计划。", "orange"); + renderAll(); return; } const run = safeArray(appState.adminFixRuns.length ? appState.adminFixRuns : appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId); if (!run) { - alert("没有找到这条修复计划。"); + rememberAction("修复计划不存在", "当前没有找到这条修复计划,请先刷新管理员配置台。", "orange"); + renderAll(); return; } openActionModal({ @@ -10033,7 +10044,8 @@ function openRecoverJobAction(jobId) { const job = safeArray(appState.dashboard?.recent_jobs).find((item) => item.id === jobId) || (appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null); if (!job) { - alert("没有找到这条任务。"); + rememberAction("任务不存在", "当前没有找到这条任务,请先刷新生产中心。", "orange"); + renderAll(); return; } const recovery = getJobRecoverability(job); @@ -10350,7 +10362,8 @@ function openLiveRecorderCreateAction() { function openLiveRecorderSourceAction(sourceId) { const source = safeArray(appState.liveRecorderSources).find((item) => item.id === sourceId); if (!source) { - alert("没有找到这条录制源。"); + rememberAction("录制源不存在", "当前没有找到这条录制源,请先刷新录制维护。", "orange"); + renderAll(); return; } const currentProject = getSelectedProject() || safeArray(appState.dashboard?.projects).find((item) => item.id === source.project_id) || appState.dashboard?.projects?.[0] || null; @@ -10435,7 +10448,8 @@ function openLiveRecorderImportAction() { async function toggleLiveRecorderSourceAction(sourceId, nextEnabled) { const source = safeArray(appState.liveRecorderSources).find((item) => item.id === sourceId); if (!source) { - alert("没有找到这条录制源。"); + rememberAction("录制源不存在", "当前没有找到这条录制源,请先刷新录制维护。", "orange"); + renderAll(); return; } setBusy(true, nextEnabled ? "正在启用录制源..." : "正在停用录制源..."); @@ -10456,7 +10470,8 @@ async function toggleLiveRecorderSourceAction(sourceId, nextEnabled) { async function deleteLiveRecorderSourceAction(sourceId) { const source = safeArray(appState.liveRecorderSources).find((item) => item.id === sourceId); if (!source) { - alert("没有找到这条录制源。"); + rememberAction("录制源不存在", "当前没有找到这条录制源,请先刷新录制维护。", "orange"); + renderAll(); return; } if (!window.confirm(`确认删除「${source.title || source.source_url || "录制源"}」吗?删除后需要重新导入。`)) { @@ -10660,7 +10675,6 @@ document.addEventListener("click", async (event) => { const reason = action.dataset.disabledReason || action.title || "当前动作暂不可用"; rememberAction("动作已拦截", reason, "orange"); renderAll(); - alert(reason); return; } if (name === "auth-refresh" || name === "refresh-data") { @@ -10678,7 +10692,7 @@ document.addEventListener("click", async (event) => { if (name === "auth-refresh" && message) { message.textContent = formatActionErrorMessage(error, "自动连接失败"); } else { - alert("刷新数据失败: " + error.message); + presentActionFailure(error, "刷新数据失败"); } } finally { setBusy(false, ""); @@ -11064,7 +11078,8 @@ document.addEventListener("click", async (event) => { if (name === "open-review-edit") { const review = getReviewById(action.dataset.reviewId || ""); if (!review) { - alert("复盘记录不存在,请先刷新页面"); + rememberAction("复盘记录不存在", "当前没有找到这条复盘记录,请先刷新当前页面。", "orange"); + renderAll(); return; } openReviewAction({ review }); @@ -11196,7 +11211,7 @@ document.addEventListener("click", async (event) => { } } catch (error) { if (requestToken === appState.selectedAccountRequestToken) { - alert("加载对标详情失败: " + error.message); + presentActionFailure(error, "加载对标详情失败"); } } finally { if (requestToken === appState.selectedAccountRequestToken) { diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 46bfbe4..e94b118 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -782,3 +782,10 @@ test("oneliner panel auto-polls active runs while the floating panel stays open" assert.match(APP, /setTimeout\(async \(\) => \{/); assert.match(APP, /await hydrateSelectedOneLinerRun\(\);/); }); + +test("workbench interaction flow avoids browser alerts and keeps failures inside the product shell", () => { + const clicks = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); + assert.doesNotMatch(APP, /alert\(/); + assert.match(clicks, /name === "show-disabled-reason"[\s\S]*rememberAction\("动作已拦截"/); + assert.match(clicks, /name === "auth-refresh" \|\| name === "refresh-data"[\s\S]*presentActionFailure\(error, "刷新数据失败"\)/); +});