feat(ai): F1-5b Wave A admin-web sandbox 透出 UI-1/2/4 (W1)

完成 F1-5b Wave A admin-web 改造:

UI-1 AIRunLogs 列表加 runtime_mode + sandbox_instance_id 列
- 后端 schema RunLogItem 补 runtime_mode / sandbox_instance_id 字段
- 后端 SQL list_run_logs SELECT 加这两列
- 前端 columns 加"运行模式"(orange/blue Tag) + "沙箱实例"(短哈希 + tooltip)

UI-2 AIRunLogs 详情 Drawer 加 runtime 字段
- 后端 SQL get_run_log SELECT 加 runtime 列
- 前端 Descriptions 加"运行模式" + "沙箱实例"两项

UI-4 全局 sandbox 徽章(覆盖所有 admin-web 页面)
- App.tsx Footer 三段式: 左 sandbox 徽章 / 中 任务状态 / 右 占位
- 30s 轮询 fetchRuntimeContext(userSiteId)
- sandbox: 橙色"沙箱"+ 业务日 + 短哈希实例 ID(monospace)
- live: 绿色"实时"+ 真实今天

双口径 4a/4b 验证(MCP Playwright 实地走查):
- UI-1 4a live: 列表全行 live 蓝 Tag
- UI-1 4b sandbox: SQL INSERT walkthrough_ui12 → 列表显示 sandbox 橙 Tag + 短哈希
- UI-2 4b: Drawer 详情 runtime_mode='sandbox' 橙 Tag + sandbox_instance_id monospace 全 ID
- UI-4 4a: footer 左侧绿"实时"+ 2026-05-05
- UI-4 4b: 切 sandbox=2026-04-20 后 footer 显示橙"沙箱"+ 业务日 + sbx_e7a7e5c5...
- 截图归档 docs/audit/changes/screenshots/2026-05-05_f1_5b_wave_a/

剩余 Wave A: MP-3/5 小程序 sandbox / MP-1 board-finance 字段复核 / BE-1 task-list 403

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-05 15:14:29 +08:00
parent af02446740
commit 3c8d72edd4
12 changed files with 90 additions and 4 deletions

View File

@@ -32,6 +32,8 @@ import { useAuthStore } from "./store/authStore";
import { useBusinessDayStore } from "./store/businessDayStore"; import { useBusinessDayStore } from "./store/businessDayStore";
import { fetchQueue } from "./api/execution"; import { fetchQueue } from "./api/execution";
import type { QueuedTask } from "./types"; import type { QueuedTask } from "./types";
// F1-5b UI-4: 全局 sandbox 徽章
import { fetchRuntimeContext, type RuntimeContext } from "./api/runtimeContext";
import Login from "./pages/Login"; import Login from "./pages/Login";
import EnvConfig from "./pages/EnvConfig"; import EnvConfig from "./pages/EnvConfig";
import DBViewer from "./pages/DBViewer"; import DBViewer from "./pages/DBViewer";
@@ -150,8 +152,11 @@ const AppLayout: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const logout = useAuthStore((s) => s.logout); const logout = useAuthStore((s) => s.logout);
const userSiteId = useAuthStore((s) => s.user?.site_id);
const [runningTask, setRunningTask] = useState<QueuedTask | null>(null); const [runningTask, setRunningTask] = useState<QueuedTask | null>(null);
// F1-5b UI-4: 当前 site 的 runtime context(sandbox 徽章渲染依据)
const [runtimeCtx, setRuntimeCtx] = useState<RuntimeContext | null>(null);
const pollQueue = useCallback(async () => { const pollQueue = useCallback(async () => {
try { try {
@@ -163,12 +168,29 @@ const AppLayout: React.FC = () => {
} }
}, []); }, []);
// F1-5b UI-4: 拉 runtime context(30 秒轮询,sandbox 切换不需即时但 5 分钟内必须更新)
const pollRuntimeCtx = useCallback(async () => {
if (!userSiteId) return;
try {
const ctx = await fetchRuntimeContext(userSiteId);
setRuntimeCtx(ctx);
} catch {
// 失败不阻塞顶栏渲染
}
}, [userSiteId]);
useEffect(() => { useEffect(() => {
pollQueue(); pollQueue();
const timer = setInterval(pollQueue, 5_000); const timer = setInterval(pollQueue, 5_000);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [pollQueue]); }, [pollQueue]);
useEffect(() => {
pollRuntimeCtx();
const timer = setInterval(pollRuntimeCtx, 30_000);
return () => clearInterval(timer);
}, [pollRuntimeCtx]);
const onMenuClick: MenuProps["onClick"] = ({ key }) => { navigate(key); }; const onMenuClick: MenuProps["onClick"] = ({ key }) => { navigate(key); };
const handleLogout = () => { const handleLogout = () => {
@@ -266,12 +288,37 @@ const AppLayout: React.FC = () => {
</Content> </Content>
<Footer <Footer
style={{ style={{
textAlign: "center", display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px 16px", padding: "6px 16px",
background: "#fafafa", background: "#fafafa",
borderTop: "1px solid #f0f0f0", borderTop: "1px solid #f0f0f0",
}} }}
> >
{/* F1-5b UI-4: 左侧 sandbox 徽章 - sandbox 模式始终可见,提醒离开 AIOperations 后仍知道在虚拟时间 */}
<div style={{ minWidth: 180, textAlign: "left" }}>
{runtimeCtx?.is_sandbox ? (
<Space size={6}>
<Tag color="orange" style={{ margin: 0 }}></Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{runtimeCtx.business_date}
</Text>
<Text type="secondary" style={{ fontSize: 11, fontFamily: "monospace" }}>
{runtimeCtx.sandbox_instance_id?.slice(0, 12)}
</Text>
</Space>
) : runtimeCtx ? (
<Space size={6}>
<Tag color="green" style={{ margin: 0 }}></Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{runtimeCtx.business_date}
</Text>
</Space>
) : null}
</div>
<div style={{ flex: 1, textAlign: "center" }}>
{runningTask ? ( {runningTask ? (
<Space size={8}> <Space size={8}>
<Spin size="small" /> <Spin size="small" />
@@ -285,6 +332,9 @@ const AppLayout: React.FC = () => {
) : ( ) : (
<Text type="secondary" style={{ fontSize: 12 }}></Text> <Text type="secondary" style={{ fontSize: 12 }}></Text>
)} )}
</div>
{/* 右侧占位,保持 Footer 三段式平衡 */}
<div style={{ minWidth: 180 }} />
</Footer> </Footer>
</Layout> </Layout>
</Layout> </Layout>

View File

@@ -139,6 +139,9 @@ export interface RunLogItem {
status: string; status: string;
site_id: number; site_id: number;
created_at: string; created_at: string;
// F1-5b UI-1: sandbox 透出
runtime_mode?: string | null;
sandbox_instance_id?: string | null;
} }
export interface RunLogDetailResponse extends RunLogItem { export interface RunLogDetailResponse extends RunLogItem {

View File

@@ -101,6 +101,21 @@ const AIRunLogs: React.FC = () => {
title: "状态", dataIndex: "status", key: "status", width: 110, title: "状态", dataIndex: "status", key: "status", width: 110,
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? "default"}>{v}</Tag>, render: (v: string) => <Tag color={STATUS_COLOR[v] ?? "default"}>{v}</Tag>,
}, },
{
title: "运行模式", dataIndex: "runtime_mode", key: "runtime_mode", width: 100,
render: (v: string | null) => {
if (!v) return "—";
return <Tag color={v === "sandbox" ? "orange" : "blue"}>{v}</Tag>;
},
},
{
title: "沙箱实例", dataIndex: "sandbox_instance_id", key: "sandbox_instance_id", width: 130,
render: (v: string | null) => {
if (!v || v === "live") return "—";
const short = v.length > 12 ? v.slice(0, 8) + "…" : v;
return <span title={v} style={{ fontFamily: "monospace", fontSize: 12 }}>{short}</span>;
},
},
{ title: "时间", dataIndex: "created_at", key: "created_at", width: 170, render: fmtTime }, { title: "时间", dataIndex: "created_at", key: "created_at", width: 170, render: fmtTime },
]; ];
@@ -192,6 +207,20 @@ const AIRunLogs: React.FC = () => {
<Tag color={STATUS_COLOR[detail.status] ?? "default"}>{detail.status}</Tag> <Tag color={STATUS_COLOR[detail.status] ?? "default"}>{detail.status}</Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Session ID">{detail.session_id ?? "—"}</Descriptions.Item> <Descriptions.Item label="Session ID">{detail.session_id ?? "—"}</Descriptions.Item>
<Descriptions.Item label="运行模式">
{detail.runtime_mode ? (
<Tag color={detail.runtime_mode === "sandbox" ? "orange" : "blue"}>
{detail.runtime_mode}
</Tag>
) : "—"}
</Descriptions.Item>
<Descriptions.Item label="沙箱实例">
{detail.sandbox_instance_id && detail.sandbox_instance_id !== "live" ? (
<span style={{ fontFamily: "monospace", fontSize: 12 }}>
{detail.sandbox_instance_id}
</span>
) : "—"}
</Descriptions.Item>
<Descriptions.Item label="创建时间" span={2}>{fmtTime(detail.created_at)}</Descriptions.Item> <Descriptions.Item label="创建时间" span={2}>{fmtTime(detail.created_at)}</Descriptions.Item>
<Descriptions.Item label="完成时间" span={2}>{fmtTime(detail.finished_at)}</Descriptions.Item> <Descriptions.Item label="完成时间" span={2}>{fmtTime(detail.finished_at)}</Descriptions.Item>
</Descriptions> </Descriptions>

View File

@@ -112,7 +112,7 @@ class RetryResponse(BaseModel):
class RunLogItem(BaseModel): class RunLogItem(BaseModel):
"""调用记录列表项。""" """调用记录列表项。F1-5b UI-1: 加 runtime_mode + sandbox_instance_id 透出 sandbox 状态。"""
id: int id: int
app_type: str app_type: str
trigger_type: str trigger_type: str
@@ -122,6 +122,8 @@ class RunLogItem(BaseModel):
status: str status: str
site_id: int site_id: int
created_at: str created_at: str
runtime_mode: str | None = None
sandbox_instance_id: str | None = None
class RunLogListResponse(BaseModel): class RunLogListResponse(BaseModel):

View File

@@ -496,7 +496,8 @@ class AdminAIService:
cur.execute( cur.execute(
f""" f"""
SELECT id, app_type, trigger_type, member_id, SELECT id, app_type, trigger_type, member_id,
tokens_used, latency_ms, status, site_id, created_at tokens_used, latency_ms, status, site_id, created_at,
runtime_mode, sandbox_instance_id
FROM biz.ai_run_logs FROM biz.ai_run_logs
{where_sql} {where_sql}
ORDER BY created_at DESC ORDER BY created_at DESC
@@ -527,7 +528,8 @@ class AdminAIService:
SELECT id, app_type, trigger_type, member_id, SELECT id, app_type, trigger_type, member_id,
tokens_used, latency_ms, status, site_id, tokens_used, latency_ms, status, site_id,
created_at, request_prompt, response_text, created_at, request_prompt, response_text,
error_message, session_id, finished_at error_message, session_id, finished_at,
runtime_mode, sandbox_instance_id
FROM biz.ai_run_logs FROM biz.ai_run_logs
WHERE id = %s WHERE id = %s
""", """,

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB