From c43375734a0314b7669964b31ce0f8dfd8e2e79d Mon Sep 17 00:00:00 2001 From: Neo Date: Tue, 5 May 2026 19:16:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin-web,backend):=20F1-5b=20Wave=20B=20U?= =?UTF-8?q?I-3=20+=20UI-5=20admin-web=20sandbox=20=E9=80=8F=E5=87=BA?= =?UTF-8?q?=E8=A1=A5=E5=BC=BA=20(W1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI-3 AIDashboard sandbox 提示 + today_calls 分组: - 后端 schemas/admin_ai.py DashboardResponse 加 today_live_calls / today_sandbox_calls 字段(默认 0,向后兼容) - 后端 services/ai/admin_service.py _get_range_stats SELECT 加 2 个 FILTER COUNT 表达式 - 前端 api/adminAI.ts DashboardResponse 类型补 2 字段 - 前端 pages/AIDashboard.tsx - 顶部加 sandbox Alert 提示条,选中 site sandbox 模式下显示业务日 + 实例 ID - today_calls 卡片下方加分组 Tag(实时 X / 沙箱 Y),feature flag 控制 - import fetchRuntimeContext + useEffect 拉 RuntimeContext - apps/admin-web/.env.example 新建,加 VITE_AI_RUNTIME_GROUPING=false 默认值说明 UI-5 AITriggerJobs runtime 列: - 后端 schemas/admin_ai.py TriggerJobItem 加 runtime_mode / sandbox_instance_id 可选字段 - 后端 admin_service.py list_trigger_jobs / get_trigger_job 各加 SELECT 列 - 前端 adminAI.ts TriggerJobItem 类型补 2 字段 - 前端 pages/AITriggerJobs.tsx 列表 columns 加运行模式 + 沙箱实例(同 UI-1 模式),详情 Modal 加 2 项(同 UI-2 模式) 双口径验证(Playwright + DB 直查): - UI-3 4a live: 选中默认门店,无 Alert,today_card 仅显示总数(flag off) - UI-3 4b sandbox=4-20: Alert 显示"沙箱 + 业务日 + sbx_…",today_calls=93(sandbox 当日) - UI-5 4a/4b: SQL INSERT 注入 walkthrough 测试行(id=9 live, id=10 sandbox),列表正确渲染 Tag + 短哈希 trend_7d 双线 / app_distribution 堆叠分布等更深入分组改造延后到 Wave C(§8.3 风险:破坏图表)。 审计: - docs/audit/changes/2026-05-05__wave1_f1_5b_ui3_aidashboard_sandbox.md - docs/audit/changes/2026-05-05__wave1_f1_5b_ui5_aitriggerjobs_runtime.md Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/admin-web/.env.example | 8 ++ apps/admin-web/src/api/adminAI.ts | 6 + apps/admin-web/src/pages/AIDashboard.tsx | 56 ++++++++- apps/admin-web/src/pages/AITriggerJobs.tsx | 31 +++++ apps/backend/app/schemas/admin_ai.py | 7 ++ apps/backend/app/services/ai/admin_service.py | 19 ++- ...05__wave1_f1_5b_ui3_aidashboard_sandbox.md | 118 ++++++++++++++++++ ...__wave1_f1_5b_ui5_aitriggerjobs_runtime.md | 70 +++++++++++ 8 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 apps/admin-web/.env.example create mode 100644 docs/audit/changes/2026-05-05__wave1_f1_5b_ui3_aidashboard_sandbox.md create mode 100644 docs/audit/changes/2026-05-05__wave1_f1_5b_ui5_aitriggerjobs_runtime.md diff --git a/apps/admin-web/.env.example b/apps/admin-web/.env.example new file mode 100644 index 0000000..82123f2 --- /dev/null +++ b/apps/admin-web/.env.example @@ -0,0 +1,8 @@ +# admin-web Vite 环境变量示例 +# 复制为 .env.local 后按需调整(已加入 .gitignore) + +# F1-5b UI-3: AIDashboard 是否展示 today_calls 按 runtime_mode 分组数字 +# (实时/沙箱两个 Tag 显示在调用次数卡片下方) +# - true:启用分组数字,sandbox 走查时一目了然 +# - false(默认):仅显示总数,保持原 UI 不变(防止图表回归) +VITE_AI_RUNTIME_GROUPING=false diff --git a/apps/admin-web/src/api/adminAI.ts b/apps/admin-web/src/api/adminAI.ts index f1cf56f..30462bf 100644 --- a/apps/admin-web/src/api/adminAI.ts +++ b/apps/admin-web/src/api/adminAI.ts @@ -78,6 +78,9 @@ export interface DashboardResponse { today_success_rate: number; today_tokens: number; today_avg_latency_ms: number; + // F1-5b UI-3: today_calls 按 runtime_mode 分组(live + sandbox = today_calls) + today_live_calls: number; + today_sandbox_calls: number; trend_7d: DailyTrend[]; app_distribution: AppDistItem[]; budget: BudgetInfo; @@ -97,6 +100,9 @@ export interface TriggerJobItem { started_at: string | null; finished_at: string | null; created_at: string; + // F1-5b UI-5: runtime 透出 + runtime_mode?: string | null; + sandbox_instance_id?: string | null; } export interface TriggerJobDetailResponse extends TriggerJobItem { diff --git a/apps/admin-web/src/pages/AIDashboard.tsx b/apps/admin-web/src/pages/AIDashboard.tsx index 57d7d0e..b4e2015 100644 --- a/apps/admin-web/src/pages/AIDashboard.tsx +++ b/apps/admin-web/src/pages/AIDashboard.tsx @@ -11,10 +11,17 @@ import React, { useEffect, useRef, useState, useCallback } from "react"; import { Card, Row, Col, Statistic, Table, Tag, Badge, Progress, - Select, Button, message, Typography, Space, DatePicker, + Select, Button, message, Typography, Space, DatePicker, Alert, } from "antd"; import { ReloadOutlined, WifiOutlined } from "@ant-design/icons"; import type { Dayjs } from "dayjs"; +// F1-5b UI-3: sandbox 提示条复用 UI-4 同款 RuntimeContext fetch +import { fetchRuntimeContext, type RuntimeContext } from "../api/runtimeContext"; + +// F1-5b UI-3: feature flag — today_calls 分组数字是否展示。 +// 默认 false(防止图表回归);开启时 today_calls 卡片下方显示 "实时 X / 沙箱 Y"。 +const RUNTIME_GROUPING_ENABLED = + String(import.meta.env.VITE_AI_RUNTIME_GROUPING ?? "").toLowerCase() === "true"; const { RangePicker } = DatePicker; @@ -105,6 +112,8 @@ const AIDashboard: React.FC = () => { const [wsStatus, setWsStatus] = useState<"connecting" | "connected" | "disconnected">("disconnected"); const [realtimeAlerts, setRealtimeAlerts] = useState([]); const wsRef = useRef(null); + // F1-5b UI-3: 当前选中 site 的 RuntimeContext,用于顶部 sandbox 提示条 + const [runtimeCtx, setRuntimeCtx] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -128,6 +137,19 @@ const AIDashboard: React.FC = () => { useEffect(() => { load(); }, [load]); + // F1-5b UI-3: 拉取当前 site 的 RuntimeContext。siteId 未选时不显示提示条。 + useEffect(() => { + if (siteId == null) { + setRuntimeCtx(null); + return; + } + let cancelled = false; + fetchRuntimeContext(siteId) + .then((ctx) => { if (!cancelled) setRuntimeCtx(ctx); }) + .catch(() => { if (!cancelled) setRuntimeCtx(null); }); + return () => { cancelled = true; }; + }, [siteId]); + const statLabel = rangeDays === 0 ? (customRange ? `${customRange[0].format("MM-DD")} ~ ${customRange[1].format("MM-DD")}` : "指定日期") : (RANGE_LABEL[rangeDays] || "今日"); @@ -198,10 +220,40 @@ const AIDashboard: React.FC = () => { + {/* F1-5b UI-3: sandbox 提示条 — 仅当选中 site 处于 sandbox 模式时显示 */} + {runtimeCtx?.is_sandbox && ( + + 沙箱 + 当前门店处于沙箱模式,数据基于业务日 + {runtimeCtx.business_date} + {runtimeCtx.sandbox_instance_id && ( + + 实例 {runtimeCtx.sandbox_instance_id.slice(0, 12)}… + + )} + + } + /> + )} + {/* 第一行:4 个统计卡片 */} - + + + {/* F1-5b UI-3: feature flag 控制 — today_calls 按 runtime 分组数字 */} + {RUNTIME_GROUPING_ENABLED && data && ( +
+ 实时 {data.today_live_calls} + 沙箱 {data.today_sandbox_calls} +
+ )} +
diff --git a/apps/admin-web/src/pages/AITriggerJobs.tsx b/apps/admin-web/src/pages/AITriggerJobs.tsx index 9a60b46..8dd9284 100644 --- a/apps/admin-web/src/pages/AITriggerJobs.tsx +++ b/apps/admin-web/src/pages/AITriggerJobs.tsx @@ -121,6 +121,22 @@ const AITriggerJobs: React.FC = () => { title: "耗时", key: "duration", width: 90, render: (_: unknown, r: TriggerJobItem) => calcDuration(r.started_at, r.finished_at), }, + // F1-5b UI-5: 运行模式 + 沙箱实例(同 UI-1 模式) + { + title: "运行模式", dataIndex: "runtime_mode", key: "runtime_mode", width: 100, + render: (v: string | null | undefined) => { + if (!v) return "—"; + return {v}; + }, + }, + { + title: "沙箱实例", dataIndex: "sandbox_instance_id", key: "sandbox_instance_id", width: 130, + render: (v: string | null | undefined) => { + if (!v || v === "live") return "—"; + const short = v.length > 12 ? v.slice(0, 8) + "…" : v; + return {short}; + }, + }, { title: "创建时间", dataIndex: "created_at", key: "created_at", width: 170, render: fmtTime }, { title: "操作", key: "action", width: 160, fixed: "right" as const, @@ -222,6 +238,21 @@ const AITriggerJobs: React.FC = () => { {detail.connector_type} {detail.is_forced ? "是" : "否"} {calcDuration(detail.started_at, detail.finished_at)} + {/* F1-5b UI-5: runtime 详情(同 UI-2 模式) */} + + {detail.runtime_mode ? ( + + {detail.runtime_mode} + + ) : "—"} + + + {detail.sandbox_instance_id && detail.sandbox_instance_id !== "live" ? ( + + {detail.sandbox_instance_id} + + ) : "—"} + {fmtTime(detail.created_at)} {detail.error_message && ( diff --git a/apps/backend/app/schemas/admin_ai.py b/apps/backend/app/schemas/admin_ai.py index e1c6a13..209077a 100644 --- a/apps/backend/app/schemas/admin_ai.py +++ b/apps/backend/app/schemas/admin_ai.py @@ -62,6 +62,10 @@ class DashboardResponse(BaseModel): today_success_rate: float # 0.0 ~ 1.0 today_tokens: int today_avg_latency_ms: float + # F1-5b UI-3: today_calls 按 runtime_mode 分组 + # live + sandbox = today_calls(总计),供前端 feature flag 控制是否展示 + today_live_calls: int = 0 + today_sandbox_calls: int = 0 trend_7d: list[DailyTrend] app_distribution: list[AppDistItem] budget: BudgetInfo @@ -84,6 +88,9 @@ class TriggerJobItem(BaseModel): started_at: str | None finished_at: str | None created_at: str + # F1-5b UI-5: runtime 透出(同 RunLogItem) + runtime_mode: str | None = None + sandbox_instance_id: str | None = None class TriggerJobListResponse(BaseModel): diff --git a/apps/backend/app/services/ai/admin_service.py b/apps/backend/app/services/ai/admin_service.py index 9338545..96cac68 100644 --- a/apps/backend/app/services/ai/admin_service.py +++ b/apps/backend/app/services/ai/admin_service.py @@ -126,6 +126,8 @@ class AdminAIService: conn = get_connection() try: with conn.cursor() as cur: + # F1-5b UI-3: SELECT 加 live/sandbox 分组 COUNT, + # 总数 total_calls 与 today_live + today_sandbox 应一致。 cur.execute( f""" SELECT @@ -133,7 +135,10 @@ class AdminAIService: COUNT(*) FILTER (WHERE status = 'success') AS success_count, COALESCE(SUM(tokens_used), 0) AS total_tokens, COALESCE(AVG(latency_ms) FILTER (WHERE latency_ms IS NOT NULL), 0) - AS avg_latency + AS avg_latency, + COUNT(*) FILTER (WHERE COALESCE(runtime_mode, 'live') = 'live') + AS live_calls, + COUNT(*) FILTER (WHERE runtime_mode = 'sandbox') AS sandbox_calls FROM biz.ai_run_logs WHERE {time_clause} {site_clause} @@ -145,13 +150,15 @@ class AdminAIService: finally: conn.close() - total, success, tokens, avg_lat = row if row else (0, 0, 0, 0) + total, success, tokens, avg_lat, live_cnt, sandbox_cnt = row if row else (0, 0, 0, 0, 0, 0) rate = round(success / total, 4) if total > 0 else 0.0 return { "today_calls": total, "today_success_rate": rate, "today_tokens": int(tokens), "today_avg_latency_ms": round(float(avg_lat), 2), + "today_live_calls": int(live_cnt), + "today_sandbox_calls": int(sandbox_cnt), } async def _get_7d_trend(self, site_id: int | None) -> list[dict]: @@ -350,10 +357,12 @@ class AdminAIService: total = cur.fetchone()[0] # 分页数据 + # F1-5b UI-5: 列表 SELECT 加 runtime_mode + sandbox_instance_id 透出 cur.execute( f""" SELECT id, event_type, member_id, status, app_chain, - is_forced, site_id, started_at, finished_at, created_at + is_forced, site_id, started_at, finished_at, created_at, + runtime_mode, sandbox_instance_id FROM biz.ai_trigger_jobs {where_sql} ORDER BY created_at DESC @@ -392,11 +401,13 @@ class AdminAIService: conn = get_connection() try: with conn.cursor() as cur: + # F1-5b UI-5: 详情 SELECT 加 runtime_mode + sandbox_instance_id 透出 cur.execute( """ SELECT id, event_type, member_id, status, app_chain, is_forced, site_id, started_at, finished_at, - created_at, payload, error_message, connector_type + created_at, payload, error_message, connector_type, + runtime_mode, sandbox_instance_id FROM biz.ai_trigger_jobs WHERE id = %s """, diff --git a/docs/audit/changes/2026-05-05__wave1_f1_5b_ui3_aidashboard_sandbox.md b/docs/audit/changes/2026-05-05__wave1_f1_5b_ui3_aidashboard_sandbox.md new file mode 100644 index 0000000..e66cb3e --- /dev/null +++ b/docs/audit/changes/2026-05-05__wave1_f1_5b_ui3_aidashboard_sandbox.md @@ -0,0 +1,118 @@ +# 2026-05-05 · F1-5b UI-3 AIDashboard sandbox 提示 + today_calls 分组 + +> Wave 1 / F1-5b Wave B 第 1 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2 顺序 13) +> +> 工作量评估 M / 2-3h(实际 ~ 1.5h),按 §3 五步流程完成。 + +## 背景 + +admin-web AIDashboard(/ai/dashboard)在 sandbox 模式下展示数据时,操作员无法直观判断"这是 sandbox 数据还是 live 数据",容易误读。 + +UI-3 拆为两个子项: +- **UI-3a 提示条**:顶部加 sandbox Alert 警示带,显示"沙箱 + 业务日 + 实例 ID",sandbox 模式下显眼提示 +- **UI-3b today_calls 分组**:今日调用次数卡片下方加"实时 X / 沙箱 Y"分组标签,feature flag 控制启用 + +trend_7d 双线 / app_distribution 堆叠分布等更复杂的分组改造**延后到 Wave C**(§8.3 风险:可能破坏图表)。 + +## 改动清单 + +### Step 1 调研 + +调研发现: +- `AIDashboard.tsx` 当前无 sandbox 透出 +- 后端 `admin_service._get_range_stats` SELECT 无按 runtime_mode 分组 +- 项目无 vite feature flag 先例,需新建 `.env.example` +- UI-4 全局 Footer 徽章(App.tsx)是 sandbox 透出的现成模板 + +### Step 2 TDD + +后端无 unit test 覆盖该聚合 SQL,前端无 unit test 框架。以 curl + Playwright + DB 直查作为验证主线(§3.2 跳过条件 — XS 文档/UI 类任务可跳 TDD)。 + +### Step 3 实施 + +**后端**(2 文件): +- [apps/backend/app/schemas/admin_ai.py](apps/backend/app/schemas/admin_ai.py) + `DashboardResponse` 加 `today_live_calls: int = 0` + `today_sandbox_calls: int = 0`(可选字段,默认 0,向后兼容) +- [apps/backend/app/services/ai/admin_service.py](apps/backend/app/services/ai/admin_service.py) + `_get_range_stats` SELECT 加两个 FILTER COUNT 表达式: + ```sql + COUNT(*) FILTER (WHERE COALESCE(runtime_mode, 'live') = 'live') AS live_calls, + COUNT(*) FILTER (WHERE runtime_mode = 'sandbox') AS sandbox_calls + ``` + 返回 dict 加 `today_live_calls` / `today_sandbox_calls` 字段 + +**前端**(3 文件): +- [apps/admin-web/src/api/adminAI.ts](apps/admin-web/src/api/adminAI.ts) `DashboardResponse` 类型加两字段 +- [apps/admin-web/src/pages/AIDashboard.tsx](apps/admin-web/src/pages/AIDashboard.tsx) + - import `fetchRuntimeContext` + `RuntimeContext`(复用 UI-4 同款) + - import `Alert` from antd + - 加 `RUNTIME_GROUPING_ENABLED` const(`import.meta.env.VITE_AI_RUNTIME_GROUPING === 'true'`,默认 false) + - 加 `runtimeCtx` state + - 新 useEffect 依赖 `siteId`,siteId 变化时拉 RuntimeContext + - 第一行卡片之前插入 Alert(条件:`runtimeCtx?.is_sandbox`) + - today_calls 卡片下方插分组 Tag(条件:`RUNTIME_GROUPING_ENABLED && data`) +- [apps/admin-web/.env.example](apps/admin-web/.env.example) 新建,加 `VITE_AI_RUNTIME_GROUPING=false` 注释说明 + +### Step 4 双口径验证 + +**目标 site**: 2790685415443269 + +| 维度 | 4a live | 4b sandbox=2026-04-20 | +|------|---------|----------------------| +| API 返回 today_calls | 1171(近 30 天聚合,因 today=05-05 0 条) | 93(sandbox=4-20 当日) | +| API 返回 today_live_calls | 1171 | 93 | +| API 返回 today_sandbox_calls | 0 | 0(测试库无 sandbox 调用) | +| Alert 提示条 | **不显示** | **显示**:"沙箱 + 业务日 2026-04-20 + 实例 sbx_mp3_4c8e…" | +| today_calls 卡片下方分组 Tag | 无(flag off) | 无(flag off) | +| Footer 全局徽章 | 绿色"实时" + 2026-05-05 | 橙色"沙箱" + 2026-04-20 + 实例 ID | + +**关键证据**: +- live 模式 site 选中后,顶部 Alert **不**出现,与 UI-4 全局徽章状态一致 +- sandbox 模式 site 选中后,Alert 立即显示业务日 + 实例 ID 短哈希 +- feature flag 默认 false,today_calls 卡片视觉无变化(防图表回归 §8.3 ✓) + +**走查脚本**: +- `_DEL/walkthrough_f1_5b/step_ui3_4a_4b_dashboard.py` — DB 直查 live/sandbox 分组分布 + +### Step 5 审计 + +本文件。 + +## 影响范围 + +| 端 | 影响 | 验证 | +|----|------|------| +| 后端 admin_service | _get_range_stats SELECT 加 2 个 FILTER COUNT | curl 返回 200 + 字段正确 | +| 后端 schema | DashboardResponse 加 2 个可选字段(默认 0) | 向后兼容,旧前端无影响 | +| admin-web AIDashboard | Alert 提示条 + today_calls 卡片分组 Tag | Playwright 4a/4b PASS | +| admin-web 其他页面 | 无影响 | — | +| ETL / 小程序 / tenant-admin | 无影响 | — | + +## 测试 + +- Playwright 走查 + DB 直查 PASS +- 后端无 unit test 覆盖该 SQL(项目惯例) + +## 风险与未覆盖 + +- **trend_7d 双线 / app_distribution 堆叠分布**:Wave C 处理(本任务范围之外) +- **VITE_AI_RUNTIME_GROUPING=true 视觉验证**:本次只验证 false 状态(默认),true 状态需在 .env.local 设置后重启 vite 验证,留给后续走查 +- **测试库 sandbox 调用为 0**:无法实际验证"sandbox=Y > 0"分组数字,F1-5b T2 集成测试在批量执行时会触发 sandbox 写入(BE-3 任务覆盖) + +## 回滚策略 + +```bash +git revert +``` + +回滚后: +- 后端 `_get_range_stats` 恢复无 runtime 分组(旧前端不查这两字段,无影响) +- AIDashboard.tsx 恢复原 UI(不显示 Alert / 分组 Tag) +- DashboardResponse 字段移除(默认 0,旧前端无影响) +- `.env.example` 删除(项目本来就无) + +无 DB schema 改动,无副作用。 + +## Co-Authored-By + +Claude Opus 4.7 (1M context) diff --git a/docs/audit/changes/2026-05-05__wave1_f1_5b_ui5_aitriggerjobs_runtime.md b/docs/audit/changes/2026-05-05__wave1_f1_5b_ui5_aitriggerjobs_runtime.md new file mode 100644 index 0000000..5b7694e --- /dev/null +++ b/docs/audit/changes/2026-05-05__wave1_f1_5b_ui5_aitriggerjobs_runtime.md @@ -0,0 +1,70 @@ +# 2026-05-05 · F1-5b UI-5 AITriggerJobs runtime 列 + +> Wave 1 / F1-5b Wave B 第 2 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2 顺序 14) +> +> 工作量 XS / ~30min(实际 ~ 30min),按 §3 五步流程完成。 + +## 背景 + +admin-web AI 调度状态页(/ai/trigger-jobs)展示 `biz.ai_trigger_jobs` 列表,但未透出 `runtime_mode` / `sandbox_instance_id` 字段。F1-5a 已经把 retry_trigger_job 写入 runtime 字段(commit 1baa212),UI 层需要补显。模式与 UI-1 / UI-2 完全一致。 + +## 改动清单 + +### Step 3 实施 + +**后端**(2 文件): +- [apps/backend/app/services/ai/admin_service.py](apps/backend/app/services/ai/admin_service.py) + - `list_trigger_jobs` SELECT 加 runtime_mode + sandbox_instance_id 列 + - `get_trigger_job` 详情 SELECT 加同样两列 +- [apps/backend/app/schemas/admin_ai.py](apps/backend/app/schemas/admin_ai.py) + `TriggerJobItem` 加可选字段 `runtime_mode: str | None = None` + `sandbox_instance_id: str | None = None` + +**前端**(2 文件): +- [apps/admin-web/src/api/adminAI.ts](apps/admin-web/src/api/adminAI.ts) `TriggerJobItem` 类型加两可选字段 +- [apps/admin-web/src/pages/AITriggerJobs.tsx](apps/admin-web/src/pages/AITriggerJobs.tsx) + - 列表 columns 加"运行模式"(orange/blue Tag) + "沙箱实例"(短哈希)— 沿用 UI-1 模式 + - 详情 Modal Descriptions 加"运行模式" + "沙箱实例" — 沿用 UI-2 模式 + +### Step 4 双口径验证 + +由于测试库 ai_trigger_jobs 数据为 0(无历史调度任务),用 SQL INSERT 注入 walkthrough 测试行: + +| 测试行 ID | runtime_mode | sandbox_instance_id | UI 渲染 | +|----------|-------------|--------------------|----| +| 9 | live | live | 列表"运行模式"=live(蓝 Tag) / 沙箱实例="—" | +| 10 | sandbox | sbx_ui5_walkthrough_demo | 列表"运行模式"=sandbox(橙 Tag) / 沙箱实例="sbx_ui5_…"(短哈希) | + +**Playwright 实地验证**:列表第 2、3 行的 runtime_mode 列分别正确渲染 live(蓝) / sandbox(橙) Tag,沙箱实例列短哈希 monospace 字体显示。 + +**走查脚本**: +- `_DEL/walkthrough_f1_5b/step_ui5_seed_sandbox.py` — 注入 walkthrough 测试行 +- `_DEL/walkthrough_f1_5b/step_ui5_cleanup.py` — 清理测试行 + +测试数据**已清理**(DELETE FROM biz.ai_trigger_jobs WHERE event_type='walkthrough_ui5_test')。 + +## 影响范围 + +| 端 | 影响 | 验证 | +|----|------|------| +| 后端 admin_service | list_trigger_jobs / get_trigger_job 各加 2 列 SELECT | curl 200,字段返回正确 | +| 后端 schema | TriggerJobItem 加 2 个可选字段 | 向后兼容(默认 None) | +| admin-web AITriggerJobs | 列表 + 详情 Modal 加 runtime 透出 | Playwright PASS | + +## 风险与未覆盖 + +- 当前测试库 ai_trigger_jobs 永久 0 行,实际生产环境数据将填充正常表格内容 +- runtime_mode 索引已在 F1-5a (commit a045625) 同期复合索引补建,过滤性能不受影响 + +## 回滚策略 + +```bash +git revert +``` + +回滚后: +- 后端 SELECT 恢复原 10 列(旧前端不需要新字段,无影响) +- AITriggerJobs.tsx 恢复原 7 列 UI + +## Co-Authored-By + +Claude Opus 4.7 (1M context)