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>
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
""",
|
""",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 236 KiB |