Przeglądaj źródła

修改前端页面

xueyiming 1 tydzień temu
rodzic
commit
9078f647ba

+ 22 - 1
api/app.py

@@ -9,12 +9,13 @@ from fastapi.middleware.cors import CORSMiddleware
 from fastapi.staticfiles import StaticFiles
 from pydantic import BaseModel, Field
 
+from api.services.agent_catalog import get_agent_detail
 from api.services.category_tree import build_category_tree
 from api.services.demand_belong_category import list_demand_belong_categories
 from api.services.demand_grade import list_demand_grades
 from api.services.demand_grade_videos import list_videos_for_demand_grade
 from api.services.demand_videos import list_videos_for_demand_belong
-from api.services.oss_logs import list_demand_belong_oss_logs
+from api.services.oss_logs import list_agent_oss_logs, list_demand_belong_oss_logs
 from api.services.scheduler import (
     get_scheduler_job_run,
     list_triggerable_jobs,
@@ -234,6 +235,26 @@ def demand_belong_oss_logs() -> dict:
     return {"items": items}
 
 
+@app.get("/api/agent-oss-logs")
+def agent_oss_logs(
+    agent_name: str | None = Query(
+        default=None,
+        description="Agent 名称;省略则返回全部 Agent 日志",
+    ),
+) -> dict:
+    """Return Agent oss_logs and the available Agent names."""
+    return list_agent_oss_logs(agent_name=agent_name)
+
+
+@app.get("/api/agents/{agent_name}")
+def agent_detail(agent_name: str) -> dict:
+    """Return one business Agent's current system prompt and tools."""
+    result = get_agent_detail(agent_name)
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"agent not found: {agent_name}")
+    return result
+
+
 _web_dist = Path(__file__).resolve().parent.parent / "web" / "dist"
 if _web_dist.is_dir():
     app.mount("/", StaticFiles(directory=_web_dist, html=True), name="web")

+ 62 - 0
api/services/agent_catalog.py

@@ -0,0 +1,62 @@
+"""Read-only catalog for the business Agents shown in the web console."""
+from __future__ import annotations
+
+from importlib import import_module
+from pathlib import Path
+from typing import Any
+
+_PROJECT_ROOT = Path(__file__).resolve().parents[2]
+
+_AGENTS: dict[str, dict[str, str]] = {
+    "demand_belong_category_agent": {
+        "display_name": "分配 Agent",
+        "subtitle": "语义归属与分类挂载",
+        "prompt_path": "agents/demand_belong_category_agent/prompt/system_prompt.md",
+        "tools_module": "agents.demand_belong_category_agent.tools",
+    },
+    "demand_grade_agent": {
+        "display_name": "分等级 Agent",
+        "subtitle": "证据融合与优先级判断",
+        "prompt_path": "agents/demand_grade_agent/prompt/system_prompt.md",
+        "tools_module": "agents.demand_grade_agent.tools",
+    },
+    "demand_video_expand_agent": {
+        "display_name": "拓展 Agent",
+        "subtitle": "视频点位与需求拓展",
+        "prompt_path": "agents/demand_video_expand_agent/prompt/system_prompt.md",
+        "tools_module": "agents.demand_video_expand_agent.tools",
+    },
+}
+
+
+def get_agent_detail(agent_name: str) -> dict[str, Any] | None:
+    """Return the current system prompt and registered tool definitions."""
+    spec = _AGENTS.get(agent_name)
+    if spec is None:
+        return None
+
+    prompt_path = _PROJECT_ROOT / spec["prompt_path"]
+    prompt = prompt_path.read_text(encoding="utf-8")
+
+    tools_module = import_module(spec["tools_module"])
+    tools = []
+    for tool_fn in tools_module.ALL_TOOLS:
+        tools.append(
+            {
+                "name": getattr(tool_fn, "_tool_name", tool_fn.__name__),
+                "description": getattr(
+                    tool_fn,
+                    "_tool_description",
+                    (tool_fn.__doc__ or "").strip(),
+                ),
+                "parameters": getattr(tool_fn, "_tool_parameters", {}),
+            }
+        )
+
+    return {
+        "name": agent_name,
+        "display_name": spec["display_name"],
+        "subtitle": spec["subtitle"],
+        "system_prompt": prompt,
+        "tools": tools,
+    }

+ 24 - 12
api/services/oss_logs.py

@@ -9,20 +9,32 @@ from supply_infra.db.session import get_session
 _DEMAND_AGENT = "demand_belong_category_agent"
 
 
+def _serialize_log(row: Any) -> dict[str, Any]:
+    return {
+        "id": row.id,
+        "log_name": row.log_name,
+        "agent_name": row.agent_name,
+        "oss_path": row.oss_path,
+        "create_time": row.create_time.isoformat(sep=" ", timespec="seconds")
+        if row.create_time
+        else None,
+    }
+
+
+def list_agent_oss_logs(agent_name: str | None = None) -> dict[str, Any]:
+    """List all Agent OSS logs, optionally filtered by exact agent name."""
+    with get_session() as session:
+        repo = OssLogRepository(session)
+        rows = repo.list_by_agent_name(agent_name) if agent_name else repo.list_all()
+        return {
+            "items": [_serialize_log(row) for row in rows],
+            "agents": repo.list_agent_names(),
+        }
+
+
 def list_demand_belong_oss_logs() -> list[dict[str, Any]]:
     """List demand_belong_category_agent oss logs, newest first."""
     with get_session() as session:
         repo = OssLogRepository(session)
         rows = repo.list_by_agent_name(_DEMAND_AGENT)
-        return [
-            {
-                "id": row.id,
-                "log_name": row.log_name,
-                "agent_name": row.agent_name,
-                "oss_path": row.oss_path,
-                "create_time": row.create_time.isoformat(sep=" ", timespec="seconds")
-                if row.create_time
-                else None,
-            }
-            for row in rows
-        ]
+        return [_serialize_log(row) for row in rows]

+ 23 - 1
supply_infra/db/repositories/oss_log_repo.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from sqlalchemy import select
+from sqlalchemy import distinct, select
 
 from supply_infra.db.models.oss_log import OssLog
 from supply_infra.db.repositories.base import BaseRepository
@@ -35,3 +35,25 @@ class OssLogRepository(BaseRepository[OssLog]):
             .order_by(OssLog.create_time.desc(), OssLog.id.desc())
         )
         return list(self.session.scalars(stmt).all())
+
+    def list_all(self) -> list[OssLog]:
+        """查询全部未删除记录,create_time 倒序。"""
+        stmt = (
+            select(OssLog)
+            .where(OssLog.is_delete == 0)
+            .order_by(OssLog.create_time.desc(), OssLog.id.desc())
+        )
+        return list(self.session.scalars(stmt).all())
+
+    def list_agent_names(self) -> list[str]:
+        """返回有日志记录的 Agent 名称。"""
+        stmt = (
+            select(distinct(OssLog.agent_name))
+            .where(
+                OssLog.is_delete == 0,
+                OssLog.agent_name.is_not(None),
+                OssLog.agent_name != "",
+            )
+            .order_by(OssLog.agent_name)
+        )
+        return [name for name in self.session.scalars(stmt).all() if name]

+ 6 - 1
web/index.html

@@ -4,7 +4,12 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>全局分类树</title>
+    <meta
+      name="description"
+      content="SupplyAgent 供给智能工作台:需求归类、热度计算、分级、视频发现与 AIGC 分发。"
+    />
+    <meta name="theme-color" content="#f5f6f8" />
+    <title>SupplyAgent · 供给智能工作台</title>
   </head>
   <body>
     <div id="app"></div>

+ 423 - 54
web/src/App.vue

@@ -1,100 +1,469 @@
 <script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue'
 import { RouterLink, RouterView, useRoute } from 'vue-router'
-import { computed } from 'vue'
 
 const route = useRoute()
+const navOpen = ref(false)
+const sidebarCollapsed = ref(false)
 const currentTitle = computed(() => (route.meta.title as string) || 'SupplyAgent')
+const SIDEBAR_STORAGE_KEY = 'supply-agent-sidebar-collapsed'
+
+const navItems = [
+  { to: '/', label: '供给总览', icon: '◫' },
+  { to: '/demand-map', label: '全局需求地图', icon: '⌁' },
+  { to: '/video-discovery', label: '需求汇总', icon: '▷' },
+  { to: '/demand-process', label: 'Agent 审计', icon: '◎' },
+]
+
+onMounted(() => {
+  sidebarCollapsed.value = window.localStorage.getItem(SIDEBAR_STORAGE_KEY) === 'true'
+})
+
+watch(sidebarCollapsed, (collapsed) => {
+  window.localStorage.setItem(SIDEBAR_STORAGE_KEY, String(collapsed))
+})
 </script>
 
 <template>
   <div class="app-shell">
-    <nav class="nav">
-      <RouterLink class="brand" to="/">SupplyAgent</RouterLink>
-      <div class="links">
-        <RouterLink to="/" exact-active-class="active">平台全局需求地图</RouterLink>
-        <RouterLink to="/video-discovery" active-class="active">需求汇总</RouterLink>
+    <aside class="sidebar" :class="{ open: navOpen, collapsed: sidebarCollapsed }">
+      <RouterLink class="brand" to="/" @click="navOpen = false">
+        <span class="brand-mark">S</span>
+        <span class="brand-copy">
+          <strong>SupplyAgent</strong>
+          <small>CONTENT INTELLIGENCE</small>
+        </span>
+      </RouterLink>
+
+      <nav class="side-nav">
+        <span class="nav-section">工作台</span>
+        <RouterLink
+          v-for="item in navItems"
+          :key="item.to"
+          :to="item.to"
+          :class="{ active: route.path === item.to }"
+          active-class=""
+          exact-active-class=""
+          :title="sidebarCollapsed ? item.label : undefined"
+          @click="navOpen = false"
+        >
+          <span class="nav-icon">{{ item.icon }}</span>
+          <span class="nav-label">{{ item.label }}</span>
+          <span class="nav-arrow">›</span>
+        </RouterLink>
+      </nav>
+
+      <div class="sidebar-info">
+        <div class="system-badge">
+          <span class="pulse" />
+          <span>
+            <strong>系统服务</strong>
+            <small>FastAPI · 8080</small>
+          </span>
+        </div>
+        <p>将需求信号转化为可执行的内容供给。</p>
       </div>
-      <span class="current">{{ currentTitle }}</span>
-    </nav>
-    <main class="main">
-      <RouterView />
-    </main>
+
+      <button
+        class="collapse-button"
+        type="button"
+        :aria-label="sidebarCollapsed ? '展开导航栏' : '收起导航栏'"
+        :aria-expanded="!sidebarCollapsed"
+        :title="sidebarCollapsed ? '展开导航栏' : '收起导航栏'"
+        @click="sidebarCollapsed = !sidebarCollapsed"
+      >
+        {{ sidebarCollapsed ? '›' : '‹' }}
+      </button>
+    </aside>
+
+    <div class="shell-content">
+      <header class="topbar">
+        <button class="menu-button" type="button" aria-label="打开导航" @click="navOpen = !navOpen">
+          ☰
+        </button>
+        <div>
+          <span class="breadcrumb">SUPPLYAGENT /</span>
+          <strong>{{ currentTitle }}</strong>
+        </div>
+        <div class="topbar-meta">
+          <span class="environment"><i /> PRODUCTION</span>
+          <span class="clock">Asia / Shanghai</span>
+        </div>
+      </header>
+
+      <main class="main">
+        <RouterView />
+      </main>
+    </div>
+    <button
+      v-if="navOpen"
+      class="nav-backdrop"
+      type="button"
+      aria-label="关闭导航"
+      @click="navOpen = false"
+    />
   </div>
 </template>
 
 <style scoped>
 .app-shell {
   display: flex;
-  flex-direction: column;
   height: 100vh;
   overflow: hidden;
+  background: #f5f6f8;
 }
 
-.nav {
+.sidebar {
+  position: relative;
   display: flex;
-  align-items: center;
-  gap: 20px;
-  padding: 0 28px;
-  height: 48px;
+  width: 222px;
+  height: 100%;
+  padding: 20px 14px 18px;
+  flex-direction: column;
   flex-shrink: 0;
-  border-bottom: 1px solid #e2e8f0;
-  background: rgba(255, 255, 255, 0.82);
-  backdrop-filter: blur(8px);
+  border-right: 1px solid #e4e6eb;
+  background: rgba(250, 250, 251, 0.96);
+  backdrop-filter: blur(12px);
+  transition: width 0.22s ease, padding 0.22s ease;
 }
 
 .brand {
-  font-weight: 700;
-  font-size: 15px;
-  color: #0f172a;
+  display: flex;
+  align-items: center;
+  gap: 11px;
+  padding: 4px 8px 24px;
+  color: #202739;
   text-decoration: none;
+}
+
+.brand-mark {
+  display: grid;
+  width: 35px;
+  height: 35px;
+  place-items: center;
+  border-radius: 11px;
+  background: linear-gradient(145deg, #6a6adb, #4f4fbf);
+  color: #fff;
+  font-size: 17px;
+  font-weight: 800;
+  box-shadow: 0 9px 20px rgba(91, 91, 214, 0.25);
+}
+
+.brand-copy strong,
+.brand-copy small {
+  display: block;
+}
+
+.brand-copy strong {
+  font-size: 14px;
   letter-spacing: -0.02em;
 }
 
-.links {
+.brand-copy small {
+  margin-top: 2px;
+  color: #9a9fad;
+  font-size: 7px;
+  font-weight: 800;
+  letter-spacing: 0.14em;
+}
+
+.side-nav {
   display: flex;
-  gap: 6px;
+  flex-direction: column;
+  gap: 4px;
 }
 
-.links a {
-  display: inline-flex;
+.nav-section {
+  margin: 0 10px 8px;
+  color: #a1a6b1;
+  font-size: 9px;
+  font-weight: 700;
+  letter-spacing: 0.12em;
+}
+
+.side-nav a {
+  display: grid;
+  min-height: 42px;
+  grid-template-columns: 27px 1fr auto;
   align-items: center;
-  height: 32px;
-  padding: 0 14px;
-  border: 1px solid #cbd5e1;
-  border-radius: 6px;
-  background: #fff;
-  font-size: 13px;
-  font-weight: 500;
-  color: #334155;
+  padding: 0 10px;
+  border-radius: 9px;
+  color: #666e7e;
+  font-size: 11px;
+  font-weight: 600;
   text-decoration: none;
-  box-shadow: 0 1px 1px rgba(15, 23, 42, 0.04);
-  transition: background 0.15s, border-color 0.15s, color 0.15s;
+  transition: background 0.16s, color 0.16s;
 }
 
-.links a:hover {
-  background: #f8fafc;
-  border-color: #94a3b8;
-  color: #0f172a;
+.side-nav a:hover {
+  background: #f0f1f5;
+  color: #343b4b;
 }
 
-.links a.active {
-  background: #0f172a;
-  border-color: #0f172a;
-  color: #fff;
-  box-shadow: none;
+.side-nav a.active {
+  background: #ececff;
+  color: #5555cb;
 }
 
-.current {
-  margin-left: auto;
-  font-size: 12px;
-  color: #94a3b8;
+.nav-icon {
+  color: #8d94a3;
+  font-size: 16px;
 }
 
-.main {
+.side-nav a.active .nav-icon {
+  color: #5b5bd6;
+}
+
+.nav-arrow {
+  color: #abb0bb;
+  font-size: 17px;
+}
+
+.collapse-button {
+  position: absolute;
+  z-index: 2;
+  top: 72px;
+  right: -13px;
+  display: grid;
+  width: 26px;
+  height: 26px;
+  place-items: center;
+  padding: 0 0 2px;
+  border: 1px solid #dfe2e8;
+  border-radius: 8px;
+  background: #fff;
+  color: #7f8695;
+  font-size: 17px;
+  line-height: 1;
+  box-shadow: 0 5px 15px rgba(36, 43, 62, 0.09);
+  cursor: pointer;
+  transition: border-color 0.16s, color 0.16s, transform 0.16s;
+}
+
+.collapse-button:hover {
+  border-color: #7777d8;
+  color: #5b5bd6;
+  transform: scale(1.05);
+}
+
+.sidebar-info {
+  margin-top: auto;
+  padding: 14px 10px 4px;
+  border-top: 1px solid #e8eaee;
+}
+
+.system-badge {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.pulse {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #2ab282;
+  box-shadow: 0 0 0 4px rgba(42, 178, 130, 0.1);
+}
+
+.system-badge strong,
+.system-badge small {
+  display: block;
+}
+
+.system-badge strong {
+  color: #555c6c;
+  font-size: 10px;
+}
+
+.system-badge small {
+  margin-top: 2px;
+  color: #a0a6b1;
+  font-size: 8px;
+}
+
+.sidebar-info p {
+  margin: 12px 0 0;
+  color: #a2a7b2;
+  font-size: 9px;
+  line-height: 1.6;
+}
+
+.shell-content {
+  display: flex;
+  min-width: 0;
   flex: 1;
+  flex-direction: column;
+}
+
+.topbar {
+  display: flex;
+  height: 56px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 26px;
+  flex-shrink: 0;
+  border-bottom: 1px solid #e6e8ed;
+  background: rgba(255, 255, 255, 0.82);
+  backdrop-filter: blur(12px);
+}
+
+.topbar > div:first-of-type {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.breadcrumb {
+  color: #a1a6b2;
+  font-size: 9px;
+  font-weight: 800;
+  letter-spacing: 0.1em;
+}
+
+.topbar strong {
+  color: #535a69;
+  font-size: 10px;
+}
+
+.topbar-meta {
+  display: flex;
+  align-items: center;
+  gap: 17px;
+  color: #9ba1ad;
+  font-size: 8px;
+  font-weight: 700;
+  letter-spacing: 0.08em;
+}
+
+.environment {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.environment i {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #2ab282;
+}
+
+.menu-button {
+  display: none;
+  width: 34px;
+  height: 34px;
+  border: 1px solid #e0e3e9;
+  border-radius: 8px;
+  background: #fff;
+  color: #5f6675;
+  cursor: pointer;
+}
+
+.main {
   min-height: 0;
-  padding: 20px 28px 24px;
-  box-sizing: border-box;
-  overflow: hidden;
+  flex: 1;
+  padding: 22px 24px 28px;
+  overflow: auto;
+}
+
+.nav-backdrop {
+  display: none;
+}
+
+@media (min-width: 821px) {
+  .sidebar.collapsed {
+    width: 72px;
+    padding-right: 10px;
+    padding-left: 10px;
+  }
+
+  .sidebar.collapsed .brand {
+    justify-content: center;
+    padding-right: 0;
+    padding-left: 0;
+  }
+
+  .sidebar.collapsed .brand-copy,
+  .sidebar.collapsed .nav-section,
+  .sidebar.collapsed .nav-label,
+  .sidebar.collapsed .nav-arrow,
+  .sidebar.collapsed .system-badge > span:last-child,
+  .sidebar.collapsed .sidebar-info p {
+    display: none;
+  }
+
+  .sidebar.collapsed .side-nav a {
+    display: grid;
+    grid-template-columns: 1fr;
+    justify-items: center;
+    padding: 0;
+  }
+
+  .sidebar.collapsed .nav-icon {
+    font-size: 18px;
+  }
+
+  .sidebar.collapsed .sidebar-info {
+    display: flex;
+    justify-content: center;
+    padding-right: 0;
+    padding-left: 0;
+  }
+
+  .sidebar.collapsed .system-badge {
+    justify-content: center;
+  }
+}
+
+@media (max-width: 820px) {
+  .sidebar {
+    position: fixed;
+    z-index: 20;
+    top: 0;
+    left: 0;
+    transform: translateX(-100%);
+    transition: transform 0.2s ease;
+  }
+
+  .sidebar.open {
+    transform: translateX(0);
+  }
+
+  .menu-button {
+    display: block;
+  }
+
+  .collapse-button {
+    display: none;
+  }
+
+  .topbar {
+    justify-content: flex-start;
+    gap: 14px;
+    padding: 0 14px;
+  }
+
+  .topbar-meta {
+    margin-left: auto;
+  }
+
+  .clock {
+    display: none;
+  }
+
+  .main {
+    padding: 14px;
+  }
+
+  .nav-backdrop {
+    position: fixed;
+    z-index: 15;
+    inset: 0;
+    display: block;
+    border: 0;
+    background: rgba(21, 27, 40, 0.35);
+    backdrop-filter: blur(2px);
+  }
 }
 </style>

+ 25 - 0
web/src/api/agentCatalog.ts

@@ -0,0 +1,25 @@
+export interface AgentToolDetail {
+  name: string
+  description: string
+  parameters: {
+    type?: string
+    properties?: Record<string, { type?: string; default?: unknown; items?: unknown }>
+    required?: string[]
+  }
+}
+
+export interface AgentDetail {
+  name: string
+  display_name: string
+  subtitle: string
+  system_prompt: string
+  tools: AgentToolDetail[]
+}
+
+export async function fetchAgentDetail(agentName: string): Promise<AgentDetail> {
+  const response = await fetch(`/api/agents/${encodeURIComponent(agentName)}`)
+  if (!response.ok) {
+    throw new Error(`加载 Agent 详情失败: ${response.status} ${response.statusText}`)
+  }
+  return response.json()
+}

+ 70 - 0
web/src/api/dashboard.ts

@@ -0,0 +1,70 @@
+export interface SchedulerJobStatus {
+  id: string
+  name: string
+  next_run_time: string | null
+}
+
+export interface SchedulerStatus {
+  enabled: boolean
+  running: boolean
+  timezone: string
+  jobs: SchedulerJobStatus[]
+}
+
+export interface ManualJob {
+  id: string
+  name: string
+  description: string
+  accepts_biz_dt: boolean
+  accepts_partition_date: boolean
+}
+
+export interface ManualRun {
+  accepted?: boolean
+  run_id: string
+  job_id: string
+  job_name: string
+  status: 'running' | 'finished' | 'failed' | string
+  biz_dt: string | null
+  partition_date: string | null
+  started_at?: string
+  finished_at?: string
+  error?: string
+}
+
+async function readJson<T>(response: Response, message: string): Promise<T> {
+  if (!response.ok) {
+    throw new Error(`${message}: ${response.status} ${response.statusText}`)
+  }
+  return response.json() as Promise<T>
+}
+
+export async function fetchHealth(): Promise<{ status: string }> {
+  return readJson(await fetch('/health'), '服务健康检查失败')
+}
+
+export async function fetchSchedulerStatus(): Promise<SchedulerStatus> {
+  return readJson(await fetch('/api/scheduler/status'), '加载调度器状态失败')
+}
+
+export async function fetchManualJobs(): Promise<{ items: ManualJob[] }> {
+  return readJson(await fetch('/api/scheduler/jobs'), '加载任务列表失败')
+}
+
+export async function runManualJob(jobId: string, bizDt: string): Promise<ManualRun> {
+  return readJson(
+    await fetch(`/api/scheduler/jobs/${encodeURIComponent(jobId)}/run`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ biz_dt: bizDt || null, wait: false }),
+    }),
+    '任务启动失败',
+  )
+}
+
+export async function fetchManualRun(runId: string): Promise<ManualRun> {
+  return readJson(
+    await fetch(`/api/scheduler/runs/${encodeURIComponent(runId)}`),
+    '加载运行状态失败',
+  )
+}

+ 11 - 0
web/src/api/ossLog.ts

@@ -7,3 +7,14 @@ export async function fetchDemandBelongOssLogs(): Promise<OssLogListResponse> {
   }
   return res.json()
 }
+
+export async function fetchAgentOssLogs(
+  agentName?: string | null,
+): Promise<OssLogListResponse> {
+  const query = agentName ? `?agent_name=${encodeURIComponent(agentName)}` : ''
+  const res = await fetch(`/api/agent-oss-logs${query}`)
+  if (!res.ok) {
+    throw new Error(`加载 Agent 审计日志失败: ${res.status} ${res.statusText}`)
+  }
+  return res.json()
+}

+ 7 - 4
web/src/router.ts

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
 import CategoryTreeView from './views/CategoryTreeView.vue'
 import DemandProcessView from './views/DemandProcessView.vue'
 import GlobalDemandMapView from './views/GlobalDemandMapView.vue'
+import OverviewView from './views/OverviewView.vue'
 import VideoDiscoveryView from './views/VideoDiscoveryView.vue'
 
 export const router = createRouter({
@@ -9,6 +10,12 @@ export const router = createRouter({
   routes: [
     {
       path: '/',
+      name: 'overview',
+      component: OverviewView,
+      meta: { title: '供给智能总览' },
+    },
+    {
+      path: '/demand-map',
       name: 'demand-map',
       component: GlobalDemandMapView,
       meta: { title: '平台全局需求地图' },
@@ -31,9 +38,5 @@ export const router = createRouter({
       component: VideoDiscoveryView,
       meta: { title: '需求汇总' },
     },
-    {
-      path: '/demand-map',
-      redirect: '/',
-    },
   ],
 })

+ 5 - 7
web/src/style.css

@@ -1,9 +1,9 @@
 :root {
-  font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+  font-family: Inter, 'SF Pro Display', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
   line-height: 1.5;
   font-weight: 400;
   color: #0f172a;
-  background: #f1f5f9;
+  background: #f5f6f8;
   font-synthesis: none;
   text-rendering: optimizeLegibility;
   -webkit-font-smoothing: antialiased;
@@ -24,13 +24,11 @@ body,
 }
 
 body {
-  background:
-    radial-gradient(ellipse 80% 50% at 0% 0%, #e0e7ff 0%, transparent 55%),
-    radial-gradient(ellipse 60% 40% at 100% 0%, #dbeafe 0%, transparent 50%),
-    #f1f5f9;
+  background: #f5f6f8;
 }
 
 button,
-select {
+select,
+input {
   font-family: inherit;
 }

+ 1 - 0
web/src/types/ossLog.ts

@@ -8,4 +8,5 @@ export interface OssLogItem {
 
 export interface OssLogListResponse {
   items: OssLogItem[]
+  agents?: string[]
 }

+ 596 - 76
web/src/views/DemandProcessView.vue

@@ -1,151 +1,671 @@
 <script setup lang="ts">
-import { onMounted, ref } from 'vue'
-import { fetchDemandBelongOssLogs } from '../api/ossLog'
+import { computed, onMounted, ref, watch } from 'vue'
+import { fetchAgentOssLogs } from '../api/ossLog'
 import type { OssLogItem } from '../types/ossLog'
 
 const items = ref<OssLogItem[]>([])
+const agents = ref<string[]>([])
+const selectedAgent = ref('')
+const keyword = ref('')
 const loading = ref(true)
 const error = ref<string | null>(null)
+let requestId = 0
 
-onMounted(async () => {
+const filteredItems = computed(() => {
+  const query = keyword.value.trim().toLocaleLowerCase()
+  if (!query) return items.value
+  return items.value.filter((item) =>
+    [item.log_name, item.agent_name, item.oss_path]
+      .filter(Boolean)
+      .join(' ')
+      .toLocaleLowerCase()
+      .includes(query),
+  )
+})
+
+onMounted(() => loadLogs())
+
+watch(selectedAgent, () => loadLogs(selectedAgent.value))
+
+async function loadLogs(agentName?: string) {
+  const currentRequest = ++requestId
+  loading.value = true
+  error.value = null
   try {
-    const data = await fetchDemandBelongOssLogs()
+    const data = await fetchAgentOssLogs(agentName || null)
+    if (currentRequest !== requestId) return
     items.value = data.items ?? []
+    if (data.agents?.length) agents.value = data.agents
   } catch (e) {
+    if (currentRequest !== requestId) return
     error.value = e instanceof Error ? e.message : String(e)
   } finally {
-    loading.value = false
+    if (currentRequest === requestId) loading.value = false
   }
-})
+}
 
 function openHtml(item: OssLogItem) {
   const url = item.oss_path?.trim()
   if (!url) return
   window.open(url, '_blank', 'noopener,noreferrer')
 }
+
+function displayAgentName(value: string | null): string {
+  if (!value) return '未标记 Agent'
+  return value.replace(/_agent$/, '').replaceAll('_', ' ')
+}
+
+function formatTime(value: string | null): string {
+  if (!value) return '—'
+  const date = new Date(value)
+  if (Number.isNaN(date.getTime())) return value
+  return new Intl.DateTimeFormat('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit',
+    hour12: false,
+  }).format(date)
+}
+
+function agentTone(value: string | null): string {
+  if (!value) return 'tone-gray'
+  if (value.includes('find')) return 'tone-orange'
+  if (value.includes('grade')) return 'tone-violet'
+  if (value.includes('belong')) return 'tone-blue'
+  if (value.includes('expand')) return 'tone-green'
+  if (value.includes('generate')) return 'tone-cyan'
+  return 'tone-gray'
+}
 </script>
 
 <template>
-  <div class="process-page">
-    <header class="header">
-      <h1>需求归类过程</h1>
-      <p class="sub">demand_belong_category_agent · 按创建时间倒序 · 点击行打开可视化 HTML</p>
+  <div class="audit-page">
+    <header class="audit-header">
+      <div>
+        <div class="eyebrow"><span /> AGENT TRACEABILITY</div>
+        <h1>Agent 运行审计</h1>
+        <p>查看模型输入、思考过程、工具调用与完整运行结果。</p>
+      </div>
+      <div class="header-stat">
+        <strong>{{ filteredItems.length }}</strong>
+        <span>{{ selectedAgent ? '筛选结果' : '全部记录' }}</span>
+      </div>
     </header>
 
-    <div v-if="loading" class="state">正在加载…</div>
-    <div v-else-if="error" class="state error">{{ error }}</div>
-    <div v-else-if="!items.length" class="state">暂无日志记录</div>
-    <div v-else class="table-wrap">
-      <table>
-        <thead>
-          <tr>
-            <th class="col-time">创建时间</th>
-            <th class="col-name">日志名称</th>
-            <th class="col-path">oss_path</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr
-            v-for="item in items"
-            :key="item.id"
-            class="row"
-            @click="openHtml(item)"
-          >
-            <td>{{ item.create_time || '—' }}</td>
-            <td>{{ item.log_name || '—' }}</td>
-            <td class="path">{{ item.oss_path || '—' }}</td>
-          </tr>
-        </tbody>
-      </table>
+    <section class="filter-bar" aria-label="日志筛选">
+      <label class="agent-select">
+        <span class="filter-label">指定 Agent</span>
+        <span class="select-wrap">
+          <i class="agent-dot" :class="agentTone(selectedAgent)" />
+          <select v-model="selectedAgent" aria-label="选择 Agent">
+            <option value="">全部 Agent</option>
+            <option v-for="agent in agents" :key="agent" :value="agent">
+              {{ displayAgentName(agent) }}
+            </option>
+          </select>
+        </span>
+      </label>
+
+      <label class="search-control">
+        <span class="search-icon" aria-hidden="true" />
+        <input
+          v-model="keyword"
+          type="search"
+          placeholder="搜索日志名称或 OSS 路径"
+          aria-label="搜索日志"
+        />
+        <button v-if="keyword" type="button" aria-label="清空搜索" @click="keyword = ''">×</button>
+      </label>
+
+      <button class="refresh-button" type="button" :disabled="loading" @click="loadLogs(selectedAgent)">
+        <span :class="{ spinning: loading }">↻</span>
+        刷新
+      </button>
+    </section>
+
+    <div v-if="error" class="state error">
+      <strong>日志加载失败</strong>
+      <span>{{ error }}</span>
+      <button type="button" @click="loadLogs(selectedAgent)">重新加载</button>
     </div>
+
+    <section v-else class="table-card">
+      <div class="table-headline">
+        <span>
+          {{ selectedAgent ? displayAgentName(selectedAgent) : '全部 Agent' }}
+          <b>{{ filteredItems.length }}</b>
+        </span>
+        <small>按创建时间倒序 · 点击记录打开 HTML 审计详情</small>
+      </div>
+
+      <div v-if="loading" class="state">
+        <span class="loader" />
+        正在加载审计记录…
+      </div>
+
+      <div v-else-if="!filteredItems.length" class="state empty">
+        <span class="empty-icon">◎</span>
+        <strong>{{ keyword ? '没有匹配的日志' : '暂无审计记录' }}</strong>
+        <small>{{ keyword ? '尝试更换关键词或 Agent' : 'Agent 执行后,日志会出现在这里' }}</small>
+      </div>
+
+      <div v-else class="table-wrap">
+        <table>
+          <thead>
+            <tr>
+              <th class="col-time">创建时间</th>
+              <th class="col-agent">Agent</th>
+              <th class="col-name">日志名称</th>
+              <th class="col-path">OSS 审计文件</th>
+              <th class="col-open" aria-label="打开" />
+            </tr>
+          </thead>
+          <tbody>
+            <tr
+              v-for="item in filteredItems"
+              :key="item.id"
+              class="row"
+              tabindex="0"
+              @click="openHtml(item)"
+              @keydown.enter="openHtml(item)"
+            >
+              <td class="time">{{ formatTime(item.create_time) }}</td>
+              <td>
+                <span class="agent-badge" :class="agentTone(item.agent_name)">
+                  <i />
+                  {{ displayAgentName(item.agent_name) }}
+                </span>
+              </td>
+              <td class="name">{{ item.log_name || '未命名日志' }}</td>
+              <td class="path" :title="item.oss_path || ''">{{ item.oss_path || '—' }}</td>
+              <td class="open-arrow">↗</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </section>
   </div>
 </template>
 
 <style scoped>
-.process-page {
+.audit-page {
   display: flex;
+  min-height: 100%;
   flex-direction: column;
-  height: 100%;
-  min-height: 0;
-  gap: 16px;
+  gap: 18px;
+  color: #202738;
 }
 
-.header h1 {
+.audit-header {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  padding: 10px 4px 4px;
+}
+
+.eyebrow {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #71798b;
+  font-size: 9px;
+  font-weight: 800;
+  letter-spacing: 0.17em;
+}
+
+.eyebrow span {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  background: #6666d4;
+  box-shadow: 0 0 0 4px rgba(102, 102, 212, 0.11);
+}
+
+.audit-header h1 {
+  margin: 9px 0 5px;
+  font-size: 28px;
+  letter-spacing: -0.04em;
+}
+
+.audit-header p {
   margin: 0;
-  font-size: 22px;
+  color: #858c9b;
+  font-size: 12px;
+}
+
+.header-stat {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+  padding: 0 4px;
+}
+
+.header-stat strong {
+  font-size: 28px;
+  letter-spacing: -0.05em;
+}
+
+.header-stat span {
+  color: #959ba8;
+  font-size: 10px;
+}
+
+.filter-bar {
+  display: flex;
+  min-height: 68px;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 16px;
+  border: 1px solid #e5e7ec;
+  border-radius: 15px;
+  background: rgba(255, 255, 255, 0.88);
+}
+
+.agent-select {
+  display: flex;
+  min-width: 270px;
+  align-items: center;
+  gap: 11px;
+}
+
+.filter-label {
+  color: #7e8594;
+  font-size: 10px;
   font-weight: 700;
-  color: #0f172a;
+  white-space: nowrap;
 }
 
-.sub {
-  margin: 6px 0 0;
-  font-size: 13px;
-  color: #94a3b8;
+.select-wrap {
+  position: relative;
+  flex: 1;
 }
 
-.state {
-  padding: 48px;
-  text-align: center;
-  color: #64748b;
+.select-wrap select {
+  width: 100%;
+  height: 40px;
+  padding: 0 34px 0 31px;
+  border: 1px solid #dfe2e8;
+  border-radius: 9px;
+  appearance: none;
+  outline: none;
+  background:
+    linear-gradient(45deg, transparent 50%, #8d94a2 50%) calc(100% - 17px) 17px / 5px 5px no-repeat,
+    linear-gradient(135deg, #8d94a2 50%, transparent 50%) calc(100% - 12px) 17px / 5px 5px no-repeat,
+    #fff;
+  color: #3f4758;
+  font-size: 11px;
+  font-weight: 600;
 }
 
-.state.error {
-  color: #b91c1c;
+.select-wrap select:focus {
+  border-color: #7777d8;
+  box-shadow: 0 0 0 3px rgba(102, 102, 212, 0.09);
+}
+
+.agent-dot {
+  position: absolute;
+  z-index: 1;
+  top: 50%;
+  left: 13px;
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  transform: translateY(-50%);
+}
+
+.search-control {
+  position: relative;
+  flex: 1;
+}
+
+.search-control input {
+  width: 100%;
+  height: 40px;
+  padding: 0 36px 0 39px;
+  border: 1px solid #dfe2e8;
+  border-radius: 9px;
+  outline: none;
+  background: #fff;
+  color: #3f4758;
+  font-size: 11px;
+}
+
+.search-control input:focus {
+  border-color: #7777d8;
+  box-shadow: 0 0 0 3px rgba(102, 102, 212, 0.09);
+}
+
+.search-control input::placeholder {
+  color: #a6acb7;
+}
+
+.search-icon {
+  position: absolute;
+  z-index: 1;
+  top: 50%;
+  left: 15px;
+  width: 12px;
+  height: 12px;
+  border: 1.5px solid #8f96a5;
+  border-radius: 50%;
+  transform: translateY(-58%);
+}
+
+.search-icon::after {
+  position: absolute;
+  right: -4px;
+  bottom: -3px;
+  width: 5px;
+  height: 1.5px;
+  background: #8f96a5;
+  content: '';
+  transform: rotate(45deg);
+}
+
+.search-control button {
+  position: absolute;
+  top: 50%;
+  right: 10px;
+  width: 24px;
+  height: 24px;
+  border: 0;
+  border-radius: 6px;
+  background: transparent;
+  color: #949aa7;
+  cursor: pointer;
+  transform: translateY(-50%);
+}
+
+.refresh-button {
+  display: inline-flex;
+  height: 40px;
+  align-items: center;
+  gap: 7px;
+  padding: 0 14px;
+  border: 1px solid #dfe2e8;
+  border-radius: 9px;
+  background: #fff;
+  color: #687082;
+  font-size: 10px;
+  font-weight: 700;
+  cursor: pointer;
+}
+
+.refresh-button:disabled {
+  cursor: wait;
+  opacity: 0.58;
+}
+
+.spinning {
+  animation: spin 0.75s linear infinite;
+}
+
+.table-card {
+  display: flex;
+  min-height: 360px;
+  flex: 1;
+  flex-direction: column;
+  overflow: hidden;
+  border: 1px solid #e4e7ed;
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.94);
+  box-shadow: 0 12px 35px rgba(35, 42, 60, 0.035);
+}
+
+.table-headline {
+  display: flex;
+  min-height: 55px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 18px;
+  border-bottom: 1px solid #eceef2;
+}
+
+.table-headline span {
+  font-size: 11px;
+  font-weight: 700;
+}
+
+.table-headline b {
+  display: inline-flex;
+  min-width: 23px;
+  height: 19px;
+  align-items: center;
+  justify-content: center;
+  margin-left: 7px;
+  padding: 0 6px;
+  border-radius: 999px;
+  background: #eeeeff;
+  color: #6262ce;
+  font-size: 9px;
+}
+
+.table-headline small {
+  color: #9ba1ad;
+  font-size: 9px;
 }
 
 .table-wrap {
   flex: 1;
-  min-height: 0;
   overflow: auto;
-  border: 1px solid #e2e8f0;
-  border-radius: 10px;
-  background: rgba(255, 255, 255, 0.7);
 }
 
 table {
   width: 100%;
   border-collapse: collapse;
-  font-size: 14px;
+  table-layout: fixed;
+  font-size: 11px;
 }
 
 th,
 td {
-  padding: 12px 16px;
+  height: 52px;
+  padding: 0 15px;
+  border-bottom: 1px solid #f0f1f4;
   text-align: left;
-  border-bottom: 1px solid #eef2f7;
-  vertical-align: top;
+  vertical-align: middle;
 }
 
 th {
   position: sticky;
-  top: 0;
-  background: #f8fafc;
-  font-size: 12px;
-  font-weight: 600;
-  color: #64748b;
   z-index: 1;
+  top: 0;
+  height: 40px;
+  background: #fafafd;
+  color: #8d94a3;
+  font-size: 9px;
+  font-weight: 700;
+  letter-spacing: 0.03em;
 }
 
-.col-time {
-  width: 180px;
+.col-time { width: 174px; }
+.col-agent { width: 210px; }
+.col-name { width: 260px; }
+.col-open { width: 44px; }
+
+.row {
+  outline: none;
+  cursor: pointer;
+  transition: background 0.14s;
 }
 
-.col-name {
-  width: 280px;
+.row:hover,
+.row:focus {
+  background: #f8f8fc;
 }
 
-.row {
-  cursor: pointer;
+.time {
+  color: #727a89;
+  font-variant-numeric: tabular-nums;
+}
+
+.agent-badge {
+  display: inline-flex;
+  max-width: 100%;
+  height: 25px;
+  align-items: center;
+  gap: 7px;
+  overflow: hidden;
+  padding: 0 9px;
+  border-radius: 7px;
+  background: var(--tone-soft);
+  color: var(--tone);
+  font-size: 9px;
+  font-weight: 700;
+  text-overflow: ellipsis;
+  text-transform: capitalize;
+  white-space: nowrap;
+}
+
+.agent-badge i {
+  width: 6px;
+  height: 6px;
+  flex-shrink: 0;
+  border-radius: 50%;
+  background: var(--tone);
 }
 
-.row:hover td {
-  background: #f1f5f9;
+.tone-blue { --tone: #4d75c8; --tone-soft: #eaf1ff; background: var(--tone, #4d75c8); }
+.tone-violet { --tone: #7560c8; --tone-soft: #f0ecff; background: var(--tone, #7560c8); }
+.tone-orange { --tone: #c5793d; --tone-soft: #fff0e2; background: var(--tone, #c5793d); }
+.tone-green { --tone: #2b9272; --tone-soft: #e5f7f0; background: var(--tone, #2b9272); }
+.tone-cyan { --tone: #308ba1; --tone-soft: #e4f5f8; background: var(--tone, #308ba1); }
+.tone-gray { --tone: #7d8492; --tone-soft: #eef0f3; background: var(--tone, #7d8492); }
+
+.agent-badge.tone-blue,
+.agent-badge.tone-violet,
+.agent-badge.tone-orange,
+.agent-badge.tone-green,
+.agent-badge.tone-cyan,
+.agent-badge.tone-gray {
+  background: var(--tone-soft);
+}
+
+.name {
+  overflow: hidden;
+  color: #333b4c;
+  font-weight: 600;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .path {
-  color: #334155;
-  word-break: break-all;
+  overflow: hidden;
+  color: #89909e;
   font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
-  font-size: 12px;
+  font-size: 9px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.open-arrow {
+  color: #a4aab5;
+  font-size: 14px;
+}
+
+.state {
+  display: flex;
+  min-height: 280px;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  flex: 1;
+  color: #838a99;
+  font-size: 11px;
+}
+
+.state.error {
+  min-height: 260px;
+  flex-direction: column;
+  border: 1px solid #f0d9d7;
+  border-radius: 16px;
+  background: #fff7f6;
+  color: #a95752;
+}
+
+.state.error strong {
+  font-size: 13px;
+}
+
+.state.error button {
+  margin-top: 6px;
+  padding: 7px 12px;
+  border: 1px solid #e3b9b6;
+  border-radius: 7px;
+  background: #fff;
+  color: #a95752;
+  cursor: pointer;
+}
+
+.state.empty {
+  flex-direction: column;
+}
+
+.state.empty .empty-icon {
+  color: #7373d5;
+  font-size: 28px;
+}
+
+.state.empty strong {
+  color: #636b7b;
+}
+
+.state.empty small {
+  color: #a1a7b2;
+  font-size: 9px;
+}
+
+.loader {
+  width: 15px;
+  height: 15px;
+  border: 2px solid #dddff0;
+  border-top-color: #6666d4;
+  border-radius: 50%;
+  animation: spin 0.75s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+@media (max-width: 760px) {
+  .audit-header {
+    align-items: flex-start;
+    flex-direction: column;
+    gap: 14px;
+  }
+
+  .filter-bar {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .agent-select {
+    min-width: 0;
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .select-wrap {
+    width: 100%;
+  }
+
+  .table-headline {
+    align-items: flex-start;
+    flex-direction: column;
+    justify-content: center;
+    gap: 4px;
+  }
+
+  .table-wrap {
+    overflow-x: auto;
+  }
+
+  table {
+    min-width: 930px;
+  }
 }
 </style>

+ 2754 - 0
web/src/views/OverviewView.vue

@@ -0,0 +1,2754 @@
+<script setup lang="ts">
+import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
+import { RouterLink } from 'vue-router'
+import {
+  fetchAgentDetail,
+  type AgentDetail,
+  type AgentToolDetail,
+} from '../api/agentCatalog'
+import {
+  fetchHealth,
+  fetchManualJobs,
+  fetchManualRun,
+  fetchSchedulerStatus,
+  runManualJob,
+  type ManualJob,
+  type ManualRun,
+  type SchedulerStatus,
+} from '../api/dashboard'
+import { fetchDemandGrade } from '../api/demand'
+import { fetchAgentOssLogs } from '../api/ossLog'
+import { fetchVideoDiscoveryDemands } from '../api/videoDiscovery'
+import type { DemandGradeItem } from '../types/demand'
+import type { OssLogItem } from '../types/ossLog'
+import type { VideoDiscoveryDemand } from '../types/videoDiscovery'
+
+const health = ref<'loading' | 'online' | 'offline'>('loading')
+const scheduler = ref<SchedulerStatus | null>(null)
+const jobs = ref<ManualJob[]>([])
+const demands = ref<DemandGradeItem[]>([])
+const discoveryDemands = ref<VideoDiscoveryDemand[]>([])
+const logs = ref<OssLogItem[]>([])
+const loading = ref(true)
+const notice = ref('')
+const noticeType = ref<'success' | 'error'>('success')
+const bizDt = ref(todayBizDt())
+const runningJobId = ref<string | null>(null)
+const activeRun = ref<ManualRun | null>(null)
+let pollTimer = 0
+let copiedTimer = 0
+
+const pipeline = [
+  {
+    index: '01',
+    title: '数据同步',
+    description: '从 ODPS 同步全局分类树、策略需求池与视频点位。',
+    color: 'blue',
+  },
+  {
+    index: '02',
+    title: '智能归类',
+    description: 'Agent 将新需求词挂靠到稳定的全局内容分类骨架。',
+    color: 'cyan',
+  },
+  {
+    index: '03',
+    title: '热度计算',
+    description: '融合四项先验与 ROV / VOV 后验,形成树级热度。',
+    color: 'violet',
+  },
+  {
+    index: '04',
+    title: '需求分级',
+    description: '按业务价值与供给机会自动评估 S / A / B / C / D。',
+    color: 'amber',
+  },
+  {
+    index: '05',
+    title: '寻找视频',
+    description: '从目的点、关键点、灵感点拓展搜索意图并发现视频。',
+    color: 'orange',
+  },
+  {
+    index: '06',
+    title: 'AIGC 分发',
+    description: '把合格候选幂等交付给视频爬取与生产计划。',
+    color: 'green',
+  },
+]
+
+const featureCards = [
+  {
+    to: '/demand-map',
+    eyebrow: 'INTELLIGENCE MAP',
+    title: '平台全局需求地图',
+    description: '用矩形树图观察分类热度、优先级与需求分布,快速定位供给机会。',
+    meta: '热度树 · 多维评分 · 需求下钻',
+    symbol: '⌁',
+    tone: 'indigo',
+  },
+  {
+    to: '/demand-tree',
+    eyebrow: 'CATEGORY SYSTEM',
+    title: '全局分类树',
+    description: '逐层展开内容分类骨架,核对节点路径、权重维度及关联需求。',
+    meta: '层级浏览 · 自由展开 · HTML 导出',
+    symbol: '⌘',
+    tone: 'sky',
+  },
+  {
+    to: '/video-discovery',
+    eyebrow: 'CONTENT DISCOVERY',
+    title: '需求汇总',
+    description: '从需求等级进入命中视频,查看目的点、关键点和灵感点证据。',
+    meta: '需求筛选 · 视频命中 · 点位追踪',
+    symbol: '▷',
+    tone: 'orange',
+  },
+  {
+    to: '/demand-process',
+    eyebrow: 'AGENT AUDIT',
+    title: 'Agent 运行审计',
+    description: '追溯模型输入、思考过程、工具调用和结构化运行结果。',
+    meta: 'JSONL · HTML 可视化 · OSS 日志',
+    symbol: '◎',
+    tone: 'green',
+  },
+]
+
+const agentSystem = [
+  {
+    step: '01',
+    name: '分配 Agent',
+    code: 'demand_belong_category_agent',
+    subtitle: '语义归属与分类挂载',
+    description: '理解需求词的真实语义,在全局内容分类树中逐层判断,找到最可信的归属位置。',
+    input: '原始需求词',
+    output: '分类节点',
+    tone: 'blue',
+    principles: ['真实分类树约束', '置信度优先', '语义路径下钻'],
+    tools: ['分类树浏览', '归属结果写入'],
+  },
+  {
+    step: '02',
+    name: '分等级 Agent',
+    code: 'demand_grade_agent',
+    subtitle: '证据融合与优先级判断',
+    description: '融合来源热度、树级环境与真实效果,为每个需求形成稳定、可解释的 S–D 级判断。',
+    input: '分类需求',
+    output: 'S / A / B / C / D',
+    tone: 'violet',
+    principles: ['后验效果优先', '全局与局部校正', '五档优先级决策'],
+    tools: [
+      '业务日期',
+      '相关需求检索',
+      '分类与权重',
+      '类目路径',
+      '局部热度',
+      '词级后验',
+      '分数分布',
+      '分级结果写入',
+    ],
+  },
+  {
+    step: '03',
+    name: '拓展 Agent',
+    code: 'demand_video_expand_agent',
+    subtitle: '视频点位与需求拓展',
+    description: '从目的点、关键点和灵感点中识别相近意图,将高价值点位转化为可执行供给方向。',
+    input: '高优需求 + 视频点位',
+    output: '拓展需求',
+    tone: 'orange',
+    principles: ['目的点优先', '相近意图筛选', '来源视频可追溯'],
+    tools: ['拓展结果批量写入'],
+  },
+]
+
+type AgentSummary = (typeof agentSystem)[number]
+
+const selectedAgentSummary = ref<AgentSummary | null>(null)
+const agentDetail = ref<AgentDetail | null>(null)
+const agentDetailLoading = ref(false)
+const agentDetailError = ref<string | null>(null)
+const agentDetailTab = ref<'prompt' | 'tools'>('prompt')
+const promptCopied = ref(false)
+
+const latestBizDt = computed(() => {
+  const values = [...demands.value.map((item) => item.biz_dt), ...discoveryDemands.value.map((item) => item.biz_dt)]
+    .filter(Boolean)
+    .sort()
+  return values.at(-1) ?? null
+})
+
+const gradeSummary = computed(() => {
+  const counts: Record<string, number> = { S: 0, A: 0, B: 0, C: 0, D: 0 }
+  for (const item of demands.value) {
+    const grade = item.grade?.toUpperCase()
+    if (grade in counts) counts[grade] += 1
+  }
+  return counts
+})
+
+const videoTotal = computed(() =>
+  discoveryDemands.value.reduce((sum, item) => sum + item.video_count, 0),
+)
+
+const coveredDemandCount = computed(() =>
+  discoveryDemands.value.filter((item) => item.video_count > 0).length,
+)
+
+const nextRun = computed(() => scheduler.value?.jobs[0]?.next_run_time ?? null)
+
+const nextRunLabel = computed(() => {
+  if (!nextRun.value) return '未计划'
+  const date = new Date(nextRun.value)
+  if (Number.isNaN(date.getTime())) return nextRun.value
+  return new Intl.DateTimeFormat('zh-CN', {
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    hour12: false,
+  }).format(date)
+})
+
+const displayJobs = computed(() => jobs.value.filter((job) => job.id !== 'run_supply_pipeline'))
+
+const promptLines = computed(() => agentDetail.value?.system_prompt.split('\n') ?? [])
+
+onMounted(() => {
+  loadDashboard()
+  window.addEventListener('keydown', handleDetailKeydown)
+})
+onBeforeUnmount(() => {
+  window.clearTimeout(pollTimer)
+  window.clearTimeout(copiedTimer)
+  window.removeEventListener('keydown', handleDetailKeydown)
+  document.body.style.overflow = ''
+})
+
+async function loadDashboard() {
+  const [healthResult, schedulerResult, jobsResult, demandResult, discoveryResult, logsResult] =
+    await Promise.allSettled([
+      fetchHealth(),
+      fetchSchedulerStatus(),
+      fetchManualJobs(),
+      fetchDemandGrade(),
+      fetchVideoDiscoveryDemands(),
+      fetchAgentOssLogs(),
+    ])
+
+  health.value =
+    healthResult.status === 'fulfilled' && healthResult.value.status === 'ok'
+      ? 'online'
+      : 'offline'
+  if (schedulerResult.status === 'fulfilled') scheduler.value = schedulerResult.value
+  if (jobsResult.status === 'fulfilled') jobs.value = jobsResult.value.items ?? []
+  if (demandResult.status === 'fulfilled') demands.value = demandResult.value.items ?? []
+  if (discoveryResult.status === 'fulfilled') {
+    discoveryDemands.value = discoveryResult.value.items ?? []
+  }
+  if (logsResult.status === 'fulfilled') logs.value = logsResult.value.items ?? []
+  loading.value = false
+}
+
+function todayBizDt(): string {
+  const parts = new Intl.DateTimeFormat('zh-CN', {
+    timeZone: 'Asia/Shanghai',
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+  }).formatToParts(new Date())
+  const value = Object.fromEntries(parts.map((part) => [part.type, part.value]))
+  return `${value.year}${value.month}${value.day}`
+}
+
+function readableBizDt(value: string | null): string {
+  if (!value || value.length !== 8) return value || '暂无数据'
+  return `${value.slice(0, 4)}.${value.slice(4, 6)}.${value.slice(6)}`
+}
+
+function readableTime(value: string | null | undefined): string {
+  if (!value) return '—'
+  const date = new Date(value)
+  if (Number.isNaN(date.getTime())) return value
+  return new Intl.DateTimeFormat('zh-CN', {
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    hour12: false,
+  }).format(date)
+}
+
+function openLog(item: OssLogItem) {
+  const url = item.oss_path?.trim()
+  if (url) window.open(url, '_blank', 'noopener,noreferrer')
+}
+
+async function openAgentDetail(agent: AgentSummary) {
+  selectedAgentSummary.value = agent
+  agentDetail.value = null
+  agentDetailError.value = null
+  agentDetailLoading.value = true
+  agentDetailTab.value = 'prompt'
+  promptCopied.value = false
+  document.body.style.overflow = 'hidden'
+  try {
+    agentDetail.value = await fetchAgentDetail(agent.code)
+  } catch (error) {
+    agentDetailError.value = error instanceof Error ? error.message : String(error)
+  } finally {
+    agentDetailLoading.value = false
+  }
+}
+
+function closeAgentDetail() {
+  selectedAgentSummary.value = null
+  agentDetail.value = null
+  agentDetailError.value = null
+  document.body.style.overflow = ''
+}
+
+function handleDetailKeydown(event: KeyboardEvent) {
+  if (event.key === 'Escape' && selectedAgentSummary.value) closeAgentDetail()
+}
+
+async function copySystemPrompt() {
+  const prompt = agentDetail.value?.system_prompt
+  if (!prompt) return
+  await navigator.clipboard.writeText(prompt)
+  promptCopied.value = true
+  window.clearTimeout(copiedTimer)
+  copiedTimer = window.setTimeout(() => {
+    promptCopied.value = false
+  }, 1600)
+}
+
+function promptLineType(line: string): 'heading' | 'bullet' | 'step' | 'text' | 'space' {
+  if (!line.trim()) return 'space'
+  if (line.startsWith('## ')) return 'heading'
+  if (/^\s*-\s/.test(line)) return 'bullet'
+  if (/^\s*\d+\.\s/.test(line)) return 'step'
+  return 'text'
+}
+
+function cleanPromptLine(line: string): string {
+  return line
+    .replace(/^##\s+/, '')
+    .replace(/^\s*-\s+/, '')
+    .replace(/^\s*\d+\.\s+/, '')
+    .replaceAll('**', '')
+}
+
+function toolParameters(tool: AgentToolDetail) {
+  return Object.entries(tool.parameters?.properties ?? {})
+}
+
+async function startJob(job: ManualJob) {
+  if (runningJobId.value) return
+  runningJobId.value = job.id
+  notice.value = ''
+  try {
+    activeRun.value = await runManualJob(job.id, bizDt.value)
+    noticeType.value = 'success'
+    notice.value = `${job.name}已开始,运行编号 ${activeRun.value.run_id.slice(0, 8)}`
+    pollRun(activeRun.value.run_id)
+  } catch (error) {
+    noticeType.value = 'error'
+    notice.value = error instanceof Error ? error.message : String(error)
+    runningJobId.value = null
+  }
+}
+
+async function startPipeline() {
+  const job = jobs.value.find((item) => item.id === 'run_supply_pipeline')
+  if (job) await startJob(job)
+}
+
+function pollRun(runId: string) {
+  window.clearTimeout(pollTimer)
+  pollTimer = window.setTimeout(async () => {
+    try {
+      const run = await fetchManualRun(runId)
+      activeRun.value = run
+      if (run.status === 'running') {
+        pollRun(runId)
+        return
+      }
+      runningJobId.value = null
+      noticeType.value = run.status === 'finished' ? 'success' : 'error'
+      notice.value =
+        run.status === 'finished'
+          ? `${run.job_name}执行完成`
+          : `${run.job_name}执行失败:${run.error || '请查看运行日志'}`
+      await loadDashboard()
+    } catch {
+      runningJobId.value = null
+    }
+  }, 2500)
+}
+</script>
+
+<template>
+  <div class="overview">
+    <section class="hero">
+      <div class="hero-copy">
+        <div class="hero-kicker">
+          <span class="signal" :class="health" />
+          SUPPLY INTELLIGENCE SYSTEM
+          <span class="version">v0.1</span>
+        </div>
+        <h1>从需求信号到<br /><em>可执行内容供给</em></h1>
+        <p>
+          SupplyAgent 把分类、热度、分级、视频拓展、内容发现与 AIGC 分发连接成一条
+          可追溯的智能流水线。
+        </p>
+        <div class="hero-actions">
+          <RouterLink class="primary-action" to="/demand-map">
+            打开需求地图 <span>↗</span>
+          </RouterLink>
+          <button
+            class="secondary-action"
+            type="button"
+            :disabled="!jobs.length || Boolean(runningJobId)"
+            @click="startPipeline"
+          >
+            <span v-if="runningJobId === 'run_supply_pipeline'" class="mini-spinner" />
+            {{ runningJobId === 'run_supply_pipeline' ? '流水线运行中' : '运行今日流水线' }}
+          </button>
+        </div>
+      </div>
+
+      <div class="hero-visual" aria-label="供给流水线实时概览">
+        <div class="orbit orbit-one" />
+        <div class="orbit orbit-two" />
+        <div class="core">
+          <span class="core-ring" />
+          <strong>SA</strong>
+          <small>AGENT CORE</small>
+        </div>
+        <div class="float-card card-demand">
+          <span class="float-icon indigo">D</span>
+          <span><small>今日需求</small><strong>{{ demands.length || '—' }}</strong></span>
+        </div>
+        <div class="float-card card-grade">
+          <span class="float-icon amber">S</span>
+          <span><small>高优需求</small><strong>{{ gradeSummary.S + gradeSummary.A || '—' }}</strong></span>
+        </div>
+        <div class="float-card card-video">
+          <span class="float-icon green">V</span>
+          <span><small>命中视频</small><strong>{{ videoTotal || '—' }}</strong></span>
+        </div>
+        <span class="agent-label label-llm">LLM</span>
+        <span class="agent-label label-tools">TOOLS</span>
+        <span class="agent-label label-skills">SKILLS</span>
+      </div>
+    </section>
+
+    <section class="metric-grid" aria-label="业务数据概览">
+      <article class="metric-card">
+        <span class="metric-label">数据业务日</span>
+        <strong>{{ readableBizDt(latestBizDt) }}</strong>
+        <small>{{ loading ? '正在读取数据' : '最新可用快照' }}</small>
+      </article>
+      <article class="metric-card">
+        <span class="metric-label">需求分级</span>
+        <strong>{{ demands.length || '—' }}</strong>
+        <small><b class="grade-s">S {{ gradeSummary.S }}</b> · A {{ gradeSummary.A }} · B {{ gradeSummary.B }}</small>
+      </article>
+      <article class="metric-card">
+        <span class="metric-label">视频供给覆盖</span>
+        <strong>{{ coveredDemandCount || '—' }}</strong>
+        <small>{{ videoTotal }} 个视频证据</small>
+      </article>
+      <article class="metric-card">
+        <span class="metric-label">调度状态</span>
+        <strong class="status-value">
+          <span class="status-dot" :class="{ active: scheduler?.running }" />
+          {{ scheduler?.running ? '运行中' : '未运行' }}
+        </strong>
+        <small>下次 {{ nextRunLabel }} · {{ scheduler?.timezone || 'Asia/Shanghai' }}</small>
+      </article>
+    </section>
+
+    <section class="section-block">
+      <div class="section-heading">
+        <div>
+          <span class="section-index">01 / CAPABILITIES</span>
+          <h2>一个工作台,覆盖完整供给链路</h2>
+        </div>
+        <p>每个结果都能回到数据、模型判断和工具调用证据。</p>
+      </div>
+      <div class="feature-grid">
+        <RouterLink
+          v-for="feature in featureCards"
+          :key="feature.to"
+          :to="feature.to"
+          class="feature-card"
+          :class="feature.tone"
+        >
+          <div class="feature-top">
+            <span class="feature-symbol">{{ feature.symbol }}</span>
+            <span class="feature-arrow">↗</span>
+          </div>
+          <span class="feature-eyebrow">{{ feature.eyebrow }}</span>
+          <h3>{{ feature.title }}</h3>
+          <p>{{ feature.description }}</p>
+          <small>{{ feature.meta }}</small>
+        </RouterLink>
+      </div>
+    </section>
+
+    <section class="section-block agent-system-section">
+      <div class="section-heading agent-system-heading">
+        <div>
+          <span class="section-index">02 / MULTI-AGENT SYSTEM</span>
+          <h2>三个 Agent,共同完成一次需求决策</h2>
+        </div>
+        <p>系统角色、判断原则与工具能力协同工作,让需求从语义信号逐步变成可执行方向。</p>
+      </div>
+
+      <div class="agent-system-canvas">
+        <div class="system-signal system-signal-one" />
+        <div class="system-signal system-signal-two" />
+        <div class="system-entry">
+          <span>INPUT</span>
+          <strong>需求信号</strong>
+        </div>
+
+        <div class="agent-chain">
+          <article
+            v-for="(agent, index) in agentSystem"
+            :key="agent.code"
+            class="agent-card"
+            :class="agent.tone"
+          >
+            <header class="agent-card-head">
+              <div class="agent-identity">
+                <span class="agent-index">{{ agent.step }}</span>
+                <div>
+                  <h3>{{ agent.name }}</h3>
+                  <small>{{ agent.subtitle }}</small>
+                </div>
+              </div>
+              <span class="agent-status"><i /> ACTIVE</span>
+            </header>
+
+            <div class="agent-core-visual">
+              <span class="agent-core-orbit orbit-a" />
+              <span class="agent-core-orbit orbit-b" />
+              <strong>{{ agent.step }}</strong>
+              <small>INTELLIGENCE</small>
+            </div>
+
+            <p class="agent-description">{{ agent.description }}</p>
+
+            <div class="system-charter">
+              <div class="mini-title">
+                <span>SYSTEM</span>
+                <small>核心判断原则</small>
+              </div>
+              <div class="principle-list">
+                <span v-for="principle in agent.principles" :key="principle">
+                  <i /> {{ principle }}
+                </span>
+              </div>
+            </div>
+
+            <div class="tool-suite">
+              <div class="mini-title">
+                <span>TOOLS</span>
+                <small>{{ agent.tools.length }} 项能力</small>
+              </div>
+              <div class="tool-cloud">
+                <span v-for="tool in agent.tools" :key="tool">{{ tool }}</span>
+              </div>
+            </div>
+
+            <button class="agent-detail-trigger" type="button" @click="openAgentDetail(agent)">
+              <span>
+                <small>CONFIGURATION</small>
+                查看 Prompt 与工具详情
+              </span>
+              <b>↗</b>
+            </button>
+
+            <footer class="agent-io">
+              <span><small>输入</small>{{ agent.input }}</span>
+              <i>→</i>
+              <span><small>输出</small>{{ agent.output }}</span>
+            </footer>
+
+            <span v-if="index < agentSystem.length - 1" class="agent-flow-arrow">
+              <i />
+              <b>›</b>
+            </span>
+          </article>
+        </div>
+
+        <div class="system-exit">
+          <span>OUTPUT</span>
+          <strong>可执行供给方向</strong>
+        </div>
+
+        <div class="agent-foundation">
+          <span class="foundation-label">SHARED FOUNDATION</span>
+          <div>
+            <span><i class="foundation-dot data" /> 真实业务数据</span>
+            <span><i class="foundation-dot reason" /> 可解释判断</span>
+            <span><i class="foundation-dot trace" /> 全链路可追溯</span>
+          </div>
+          <small>OpenRouter LLM · ReAct Loop · Structured Tools</small>
+        </div>
+      </div>
+    </section>
+
+    <section class="section-block pipeline-section">
+      <div class="section-heading">
+        <div>
+          <span class="section-index">03 / PIPELINE</span>
+          <h2>每日自动供给流水线</h2>
+        </div>
+        <div class="pipeline-meta">
+          <span class="live-pill"><i /> 每日 14:30 自动执行</span>
+          <span>6 个阶段串行推进</span>
+        </div>
+      </div>
+      <div class="pipeline-track">
+        <article v-for="(stage, index) in pipeline" :key="stage.index" class="stage">
+          <div class="stage-marker" :class="stage.color">
+            <span>{{ stage.index }}</span>
+          </div>
+          <div class="stage-copy">
+            <h3>{{ stage.title }}</h3>
+            <p>{{ stage.description }}</p>
+          </div>
+          <span v-if="index < pipeline.length - 1" class="stage-line" />
+        </article>
+      </div>
+    </section>
+
+    <section class="operations-grid">
+      <div class="operation-card">
+        <div class="card-heading">
+          <div>
+            <span class="section-index">04 / OPERATIONS</span>
+            <h2>任务控制中心</h2>
+          </div>
+          <label class="date-control">
+            <span>业务日</span>
+            <input v-model="bizDt" type="text" inputmode="numeric" maxlength="8" aria-label="业务日" />
+          </label>
+        </div>
+
+        <div v-if="notice" class="notice" :class="noticeType">{{ notice }}</div>
+
+        <div class="job-list">
+          <div v-for="job in displayJobs" :key="job.id" class="job-row">
+            <span class="job-state" :class="{ active: runningJobId === job.id }" />
+            <div class="job-copy">
+              <strong>{{ job.name }}</strong>
+              <small>{{ job.description }}</small>
+            </div>
+            <button
+              type="button"
+              :disabled="Boolean(runningJobId)"
+              @click="startJob(job)"
+            >
+              {{ runningJobId === job.id ? '执行中' : '运行' }}
+            </button>
+          </div>
+          <div v-if="!displayJobs.length" class="empty-row">
+            {{ health === 'offline' ? '服务未连接,启动 API 后可控制任务' : '正在加载任务…' }}
+          </div>
+        </div>
+      </div>
+
+      <aside class="audit-card">
+        <div class="card-heading">
+          <div>
+            <span class="section-index">05 / TRACEABILITY</span>
+            <h2>最近 Agent 审计</h2>
+          </div>
+          <RouterLink to="/demand-process">全部日志 ↗</RouterLink>
+        </div>
+        <div class="audit-list">
+          <button
+            v-for="item in logs.slice(0, 5)"
+            :key="item.id"
+            type="button"
+            class="audit-item"
+            @click="openLog(item)"
+          >
+            <span class="audit-icon">AI</span>
+            <span class="audit-copy">
+              <strong>{{ item.log_name || '需求归类运行' }}</strong>
+              <small>{{ readableTime(item.create_time) }}</small>
+            </span>
+            <span class="audit-arrow">›</span>
+          </button>
+          <div v-if="!logs.length" class="empty-audit">
+            <span>◎</span>
+            <strong>暂无可展示的运行日志</strong>
+            <small>Agent 执行后将在这里显示完整审计记录</small>
+          </div>
+        </div>
+      </aside>
+    </section>
+
+    <footer class="overview-footer">
+      <span><b>SupplyAgent</b> · AI-native Content Supply Infrastructure</span>
+      <span>OpenRouter LLM · Tools · Skills · ReAct · FastAPI</span>
+    </footer>
+
+    <Teleport to="body">
+      <Transition name="agent-detail">
+        <div
+          v-if="selectedAgentSummary"
+          class="agent-detail-overlay"
+          role="dialog"
+          aria-modal="true"
+          :aria-label="`${selectedAgentSummary.name} 详情`"
+          @mousedown.self="closeAgentDetail"
+        >
+          <section class="agent-detail-modal" :class="selectedAgentSummary.tone">
+            <header class="detail-modal-header">
+              <div class="detail-agent-identity">
+                <span>{{ selectedAgentSummary.step }}</span>
+                <div>
+                  <small>AGENT CONFIGURATION</small>
+                  <h2>{{ selectedAgentSummary.name }}</h2>
+                  <code>{{ selectedAgentSummary.code }}</code>
+                </div>
+              </div>
+              <button type="button" aria-label="关闭 Agent 详情" @click="closeAgentDetail">×</button>
+            </header>
+
+            <div class="detail-tabs" role="tablist" aria-label="Agent 详情类型">
+              <button
+                type="button"
+                role="tab"
+                :aria-selected="agentDetailTab === 'prompt'"
+                :class="{ active: agentDetailTab === 'prompt' }"
+                @click="agentDetailTab = 'prompt'"
+              >
+                <span>SYSTEM PROMPT</span>
+                <small>完整系统指令</small>
+              </button>
+              <button
+                type="button"
+                role="tab"
+                :aria-selected="agentDetailTab === 'tools'"
+                :class="{ active: agentDetailTab === 'tools' }"
+                @click="agentDetailTab = 'tools'"
+              >
+                <span>TOOLS</span>
+                <small>{{ agentDetail?.tools.length ?? selectedAgentSummary.tools.length }} 个注册工具</small>
+              </button>
+            </div>
+
+            <div v-if="agentDetailLoading" class="detail-state">
+              <span class="detail-loader" />
+              正在读取当前 Agent 配置…
+            </div>
+
+            <div v-else-if="agentDetailError" class="detail-state detail-error">
+              <strong>详情加载失败</strong>
+              <span>{{ agentDetailError }}</span>
+              <button type="button" @click="openAgentDetail(selectedAgentSummary)">重新加载</button>
+            </div>
+
+            <div v-else-if="agentDetail" class="detail-content">
+              <section v-show="agentDetailTab === 'prompt'" class="prompt-panel">
+                <div class="detail-content-head">
+                  <div>
+                    <span>LIVE SYSTEM PROMPT</span>
+                    <small>内容直接读取自当前 Agent 配置文件</small>
+                  </div>
+                  <button type="button" @click="copySystemPrompt">
+                    {{ promptCopied ? '已复制 ✓' : '复制 Prompt' }}
+                  </button>
+                </div>
+
+                <div class="prompt-document">
+                  <template v-for="(line, index) in promptLines" :key="`${index}-${line}`">
+                    <h3 v-if="promptLineType(line) === 'heading'">
+                      <span>{{ String(index + 1).padStart(2, '0') }}</span>
+                      {{ cleanPromptLine(line) }}
+                    </h3>
+                    <p v-else-if="promptLineType(line) === 'bullet'" class="prompt-bullet">
+                      <i /> {{ cleanPromptLine(line) }}
+                    </p>
+                    <p v-else-if="promptLineType(line) === 'step'" class="prompt-step">
+                      <b>{{ line.trim().match(/^\d+/)?.[0] }}</b>
+                      {{ cleanPromptLine(line) }}
+                    </p>
+                    <p v-else-if="promptLineType(line) === 'text'" class="prompt-text">
+                      {{ cleanPromptLine(line) }}
+                    </p>
+                    <div v-else class="prompt-space" />
+                  </template>
+                </div>
+              </section>
+
+              <section v-show="agentDetailTab === 'tools'" class="tools-panel">
+                <div class="detail-content-head">
+                  <div>
+                    <span>REGISTERED TOOLSET</span>
+                    <small>Agent 当前可调用的全部结构化工具</small>
+                  </div>
+                  <strong>{{ agentDetail.tools.length }} TOOLS</strong>
+                </div>
+
+                <div class="detail-tool-list">
+                  <article
+                    v-for="(tool, index) in agentDetail.tools"
+                    :key="tool.name"
+                    class="detail-tool-card"
+                  >
+                    <div class="detail-tool-index">{{ String(index + 1).padStart(2, '0') }}</div>
+                    <div class="detail-tool-main">
+                      <div class="detail-tool-title">
+                        <code>{{ tool.name }}</code>
+                        <span>FUNCTION</span>
+                      </div>
+                      <p>{{ tool.description || '暂无工具说明' }}</p>
+                      <div v-if="toolParameters(tool).length" class="parameter-list">
+                        <span
+                          v-for="[name, schema] in toolParameters(tool)"
+                          :key="name"
+                          :class="{ required: tool.parameters.required?.includes(name) }"
+                        >
+                          {{ name }}
+                          <small>{{ schema.type || 'string' }}</small>
+                        </span>
+                      </div>
+                    </div>
+                  </article>
+                </div>
+              </section>
+            </div>
+          </section>
+        </div>
+      </Transition>
+    </Teleport>
+  </div>
+</template>
+
+<style scoped>
+.overview {
+  --ink: #172033;
+  --muted: #6c7484;
+  --line: #e7e9ef;
+  display: flex;
+  flex-direction: column;
+  gap: 26px;
+  max-width: 1480px;
+  margin: 0 auto;
+  padding: 0 4px 30px;
+  color: var(--ink);
+}
+
+.hero {
+  position: relative;
+  display: grid;
+  grid-template-columns: minmax(0, 1.06fr) minmax(420px, 0.94fr);
+  min-height: 430px;
+  overflow: hidden;
+  border: 1px solid #e2e5ee;
+  border-radius: 26px;
+  background:
+    linear-gradient(120deg, rgba(255, 255, 255, 0.98) 0 53%, rgba(246, 247, 252, 0.96) 53%),
+    #fff;
+  box-shadow: 0 20px 60px rgba(28, 35, 55, 0.07);
+}
+
+.hero::before {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(83, 96, 128, 0.045) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(83, 96, 128, 0.045) 1px, transparent 1px);
+  background-size: 36px 36px;
+  mask-image: linear-gradient(to right, transparent 46%, black);
+  content: '';
+  pointer-events: none;
+}
+
+.hero-copy {
+  position: relative;
+  z-index: 1;
+  align-self: center;
+  padding: 58px 6vw 58px 58px;
+}
+
+.hero-kicker,
+.section-index {
+  color: #636b7f;
+  font-size: 10px;
+  font-weight: 800;
+  letter-spacing: 0.18em;
+}
+
+.hero-kicker {
+  display: flex;
+  align-items: center;
+  gap: 9px;
+}
+
+.signal {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #aab1bf;
+  box-shadow: 0 0 0 4px rgba(170, 177, 191, 0.16);
+}
+
+.signal.online {
+  background: #18a879;
+  box-shadow: 0 0 0 4px rgba(24, 168, 121, 0.14);
+}
+
+.signal.offline {
+  background: #e0605c;
+  box-shadow: 0 0 0 4px rgba(224, 96, 92, 0.14);
+}
+
+.version {
+  margin-left: 4px;
+  padding: 3px 7px;
+  border-radius: 999px;
+  background: #eef0f6;
+  color: #747c8d;
+  font-size: 9px;
+  letter-spacing: 0.08em;
+}
+
+.hero h1 {
+  margin: 22px 0 18px;
+  font-size: clamp(42px, 4.2vw, 66px);
+  line-height: 1.05;
+  letter-spacing: -0.055em;
+}
+
+.hero h1 em {
+  color: #5b5bd6;
+  font-family: 'STKaiti', 'Kaiti SC', serif;
+  font-style: normal;
+  font-weight: 500;
+}
+
+.hero-copy > p {
+  max-width: 590px;
+  margin: 0;
+  color: var(--muted);
+  font-size: 15px;
+  line-height: 1.85;
+}
+
+.hero-actions {
+  display: flex;
+  gap: 12px;
+  margin-top: 30px;
+}
+
+.primary-action,
+.secondary-action {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+  height: 46px;
+  padding: 0 20px;
+  border-radius: 10px;
+  font-size: 13px;
+  font-weight: 700;
+  text-decoration: none;
+  transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
+}
+
+.primary-action {
+  background: #1e2638;
+  color: #fff;
+  box-shadow: 0 10px 24px rgba(30, 38, 56, 0.18);
+}
+
+.secondary-action {
+  border: 1px solid #d9dce4;
+  background: #fff;
+  color: #2d3445;
+  cursor: pointer;
+}
+
+.primary-action:hover,
+.secondary-action:not(:disabled):hover {
+  transform: translateY(-2px);
+}
+
+.secondary-action:disabled {
+  cursor: not-allowed;
+  opacity: 0.58;
+}
+
+.hero-visual {
+  position: relative;
+  min-height: 430px;
+}
+
+.orbit {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  border: 1px dashed rgba(90, 91, 214, 0.24);
+  border-radius: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.orbit-one {
+  width: 320px;
+  height: 320px;
+}
+
+.orbit-two {
+  width: 440px;
+  height: 440px;
+  border-color: rgba(90, 91, 214, 0.1);
+}
+
+.core {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  display: flex;
+  width: 146px;
+  height: 146px;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  border: 1px solid rgba(91, 91, 214, 0.2);
+  border-radius: 42%;
+  background: linear-gradient(145deg, #fff, #f0efff);
+  box-shadow: 0 30px 70px rgba(91, 91, 214, 0.24);
+  transform: translate(-50%, -50%) rotate(8deg);
+}
+
+.core::after {
+  position: absolute;
+  inset: 10px;
+  border: 1px solid rgba(91, 91, 214, 0.13);
+  border-radius: 38%;
+  content: '';
+}
+
+.core strong,
+.core small {
+  position: relative;
+  z-index: 1;
+  transform: rotate(-8deg);
+}
+
+.core strong {
+  color: #5555d3;
+  font-size: 46px;
+  letter-spacing: -0.08em;
+}
+
+.core small {
+  color: #8d91a6;
+  font-size: 8px;
+  font-weight: 800;
+  letter-spacing: 0.17em;
+}
+
+.core-ring {
+  position: absolute;
+  width: 14px;
+  height: 14px;
+  top: -7px;
+  right: 30px;
+  border: 3px solid #fff;
+  border-radius: 50%;
+  background: #23b786;
+}
+
+.float-card {
+  position: absolute;
+  display: flex;
+  min-width: 142px;
+  align-items: center;
+  gap: 10px;
+  padding: 11px 13px;
+  border: 1px solid rgba(221, 224, 233, 0.9);
+  border-radius: 13px;
+  background: rgba(255, 255, 255, 0.88);
+  box-shadow: 0 15px 38px rgba(44, 50, 72, 0.1);
+  backdrop-filter: blur(10px);
+}
+
+.float-card small,
+.float-card strong {
+  display: block;
+}
+
+.float-card small {
+  color: #9399a8;
+  font-size: 10px;
+}
+
+.float-card strong {
+  margin-top: 1px;
+  font-size: 20px;
+}
+
+.float-icon {
+  display: grid;
+  width: 34px;
+  height: 34px;
+  place-items: center;
+  border-radius: 9px;
+  font-size: 12px;
+  font-weight: 800;
+}
+
+.float-icon.indigo { background: #ececff; color: #5b5bd6; }
+.float-icon.amber { background: #fff0d9; color: #c47718; }
+.float-icon.green { background: #e2f7ef; color: #168762; }
+.card-demand { top: 54px; left: 6%; }
+.card-grade { top: 72px; right: 6%; }
+.card-video { right: 5%; bottom: 48px; }
+
+.agent-label {
+  position: absolute;
+  color: #858ca0;
+  font-size: 9px;
+  font-weight: 800;
+  letter-spacing: 0.15em;
+}
+
+.label-llm { left: 16%; bottom: 80px; }
+.label-tools { right: 18%; top: 46%; }
+.label-skills { left: 32%; top: 20%; }
+
+.metric-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.metric-card {
+  min-height: 128px;
+  padding: 20px 22px;
+  border: 1px solid var(--line);
+  border-radius: 16px;
+  background: #fff;
+  box-shadow: 0 8px 30px rgba(38, 45, 66, 0.035);
+}
+
+.metric-card > span,
+.metric-card > strong,
+.metric-card > small {
+  display: block;
+}
+
+.metric-label {
+  margin-bottom: 8px;
+  color: #878e9d;
+  font-size: 11px;
+}
+
+.metric-card > strong {
+  font-size: 27px;
+  letter-spacing: -0.04em;
+}
+
+.metric-card > small {
+  margin-top: 9px;
+  color: #949aa7;
+  font-size: 10px;
+}
+
+.metric-card small b {
+  font-weight: 700;
+}
+
+.grade-s { color: #cb5b48; }
+.status-value { display: flex !important; align-items: center; gap: 9px; }
+.status-dot { width: 9px; height: 9px; border-radius: 50%; background: #c5cad3; }
+.status-dot.active { background: #18a879; box-shadow: 0 0 0 5px rgba(24, 168, 121, 0.1); }
+
+.section-block,
+.operation-card,
+.audit-card {
+  padding: 28px;
+  border: 1px solid var(--line);
+  border-radius: 20px;
+  background: rgba(255, 255, 255, 0.92);
+}
+
+.section-heading,
+.card-heading {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  gap: 24px;
+}
+
+.section-heading h2,
+.card-heading h2 {
+  margin: 6px 0 0;
+  font-size: 24px;
+  letter-spacing: -0.035em;
+}
+
+.section-heading > p,
+.pipeline-meta {
+  max-width: 420px;
+  margin: 0;
+  color: var(--muted);
+  font-size: 12px;
+}
+
+.feature-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 14px;
+  margin-top: 24px;
+}
+
+.feature-card {
+  position: relative;
+  min-height: 260px;
+  overflow: hidden;
+  padding: 20px;
+  border: 1px solid #e4e7ed;
+  border-radius: 16px;
+  background: #fbfbfd;
+  color: var(--ink);
+  text-decoration: none;
+  transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
+}
+
+.feature-card::after {
+  position: absolute;
+  right: -50px;
+  bottom: -70px;
+  width: 150px;
+  height: 150px;
+  border-radius: 50%;
+  background: var(--accent-soft);
+  content: '';
+  filter: blur(5px);
+}
+
+.feature-card:hover {
+  transform: translateY(-4px);
+  border-color: var(--accent);
+  box-shadow: 0 18px 40px rgba(31, 39, 58, 0.08);
+}
+
+.feature-card.indigo { --accent: #6868d9; --accent-soft: #e8e8ff; }
+.feature-card.sky { --accent: #3f91c8; --accent-soft: #ddf3ff; }
+.feature-card.orange { --accent: #d17835; --accent-soft: #ffead7; }
+.feature-card.green { --accent: #299575; --accent-soft: #dff5ed; }
+
+.feature-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.feature-symbol {
+  display: grid;
+  width: 43px;
+  height: 43px;
+  place-items: center;
+  border-radius: 12px;
+  background: var(--accent-soft);
+  color: var(--accent);
+  font-size: 22px;
+  font-weight: 800;
+}
+
+.feature-arrow {
+  color: #a2a8b4;
+  font-size: 18px;
+}
+
+.feature-eyebrow {
+  display: block;
+  margin-top: 26px;
+  color: var(--accent);
+  font-size: 9px;
+  font-weight: 800;
+  letter-spacing: 0.15em;
+}
+
+.feature-card h3 {
+  margin: 7px 0 10px;
+  font-size: 18px;
+}
+
+.feature-card p {
+  margin: 0;
+  color: var(--muted);
+  font-size: 12px;
+  line-height: 1.75;
+}
+
+.feature-card > small {
+  position: absolute;
+  z-index: 1;
+  bottom: 19px;
+  left: 20px;
+  color: #9298a6;
+  font-size: 9px;
+}
+
+.agent-system-section {
+  position: relative;
+  overflow: hidden;
+  background:
+    radial-gradient(circle at 10% 0%, rgba(96, 102, 214, 0.09), transparent 28%),
+    radial-gradient(circle at 95% 100%, rgba(213, 116, 64, 0.08), transparent 30%),
+    #fbfbfd;
+}
+
+.agent-system-section::before {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(92, 99, 128, 0.035) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(92, 99, 128, 0.035) 1px, transparent 1px);
+  background-size: 32px 32px;
+  content: '';
+  mask-image: linear-gradient(to bottom, black, transparent 58%);
+  pointer-events: none;
+}
+
+.agent-system-heading {
+  position: relative;
+  z-index: 1;
+}
+
+.agent-system-canvas {
+  position: relative;
+  z-index: 1;
+  overflow: hidden;
+  margin-top: 26px;
+  padding: 72px 20px 20px;
+  border: 1px solid #dfe2e9;
+  border-radius: 19px;
+  background:
+    linear-gradient(145deg, rgba(255, 255, 255, 0.82), rgba(241, 243, 249, 0.9)),
+    #f4f5f9;
+}
+
+.system-signal {
+  position: absolute;
+  border: 1px solid rgba(98, 103, 205, 0.1);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.system-signal-one {
+  top: -180px;
+  left: 32%;
+  width: 460px;
+  height: 460px;
+}
+
+.system-signal-two {
+  right: -120px;
+  bottom: -200px;
+  width: 400px;
+  height: 400px;
+}
+
+.system-entry,
+.system-exit {
+  position: absolute;
+  top: 19px;
+  display: flex;
+  align-items: center;
+  gap: 9px;
+  color: #687082;
+}
+
+.system-entry {
+  left: 23px;
+}
+
+.system-exit {
+  right: 23px;
+}
+
+.system-entry span,
+.system-exit span {
+  padding: 4px 7px;
+  border-radius: 5px;
+  background: #e8eaf2;
+  color: #7e8597;
+  font-size: 7px;
+  font-weight: 900;
+  letter-spacing: 0.14em;
+}
+
+.system-entry strong,
+.system-exit strong {
+  font-size: 10px;
+}
+
+.agent-chain {
+  position: relative;
+  z-index: 1;
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 28px;
+}
+
+.agent-card {
+  --agent: #5377c9;
+  --agent-soft: #eaf1ff;
+  position: relative;
+  display: flex;
+  min-width: 0;
+  min-height: 570px;
+  padding: 20px;
+  flex-direction: column;
+  overflow: visible;
+  border: 1px solid rgba(220, 224, 234, 0.96);
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.94);
+  box-shadow:
+    0 20px 45px rgba(35, 42, 64, 0.065),
+    inset 0 1px 0 #fff;
+}
+
+.agent-card::before {
+  position: absolute;
+  top: 0;
+  right: 18px;
+  left: 18px;
+  height: 3px;
+  border-radius: 0 0 6px 6px;
+  background: linear-gradient(90deg, transparent, var(--agent), transparent);
+  content: '';
+}
+
+.agent-card.blue {
+  --agent: #4f78ce;
+  --agent-soft: #eaf1ff;
+}
+
+.agent-card.violet {
+  --agent: #7060cb;
+  --agent-soft: #efecff;
+}
+
+.agent-card.orange {
+  --agent: #cc7540;
+  --agent-soft: #fff0e5;
+}
+
+.agent-card-head {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 10px;
+}
+
+.agent-identity {
+  display: flex;
+  align-items: center;
+  gap: 11px;
+}
+
+.agent-index {
+  display: grid;
+  width: 37px;
+  height: 37px;
+  flex-shrink: 0;
+  place-items: center;
+  border-radius: 10px;
+  background: var(--agent-soft);
+  color: var(--agent);
+  font-size: 10px;
+  font-weight: 900;
+}
+
+.agent-identity h3 {
+  margin: 0;
+  font-size: 15px;
+  letter-spacing: -0.02em;
+}
+
+.agent-identity small {
+  display: block;
+  margin-top: 3px;
+  color: #949aa8;
+  font-size: 8px;
+}
+
+.agent-status {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  margin-top: 3px;
+  color: #9096a3;
+  font-size: 7px;
+  font-weight: 900;
+  letter-spacing: 0.1em;
+}
+
+.agent-status i {
+  width: 5px;
+  height: 5px;
+  border-radius: 50%;
+  background: #25ac7e;
+  box-shadow: 0 0 0 3px rgba(37, 172, 126, 0.1);
+}
+
+.agent-core-visual {
+  position: relative;
+  display: flex;
+  height: 126px;
+  align-items: center;
+  justify-content: center;
+  margin: 18px 0 16px;
+  flex-direction: column;
+  overflow: hidden;
+  border-radius: 13px;
+  background:
+    radial-gradient(circle at 50% 55%, var(--agent-soft), transparent 42%),
+    linear-gradient(135deg, #fafbfe, #f4f5f9);
+}
+
+.agent-core-visual::before,
+.agent-core-visual::after {
+  position: absolute;
+  border-radius: 50%;
+  content: '';
+}
+
+.agent-core-visual::before {
+  width: 72px;
+  height: 72px;
+  border: 1px solid var(--agent);
+  opacity: 0.2;
+}
+
+.agent-core-visual::after {
+  width: 48px;
+  height: 48px;
+  background: linear-gradient(145deg, #fff, var(--agent-soft));
+  box-shadow: 0 10px 28px rgba(52, 59, 83, 0.11);
+}
+
+.agent-core-orbit {
+  position: absolute;
+  border: 1px dashed var(--agent);
+  border-radius: 50%;
+  opacity: 0.14;
+}
+
+.agent-core-orbit.orbit-a {
+  width: 100px;
+  height: 100px;
+}
+
+.agent-core-orbit.orbit-b {
+  width: 150px;
+  height: 58px;
+  transform: rotate(-12deg);
+}
+
+.agent-core-visual strong,
+.agent-core-visual small {
+  position: relative;
+  z-index: 1;
+}
+
+.agent-core-visual strong {
+  color: var(--agent);
+  font-size: 18px;
+}
+
+.agent-core-visual small {
+  margin-top: 4px;
+  color: #969dab;
+  font-size: 5px;
+  font-weight: 900;
+  letter-spacing: 0.13em;
+}
+
+.agent-description {
+  min-height: 56px;
+  margin: 0 0 17px;
+  color: #697183;
+  font-size: 10px;
+  line-height: 1.75;
+}
+
+.system-charter,
+.tool-suite {
+  padding-top: 14px;
+  border-top: 1px solid #eceef3;
+}
+
+.tool-suite {
+  margin-top: 15px;
+}
+
+.mini-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.mini-title > span {
+  color: var(--agent);
+  font-size: 7px;
+  font-weight: 900;
+  letter-spacing: 0.16em;
+}
+
+.mini-title > small {
+  color: #a0a6b2;
+  font-size: 7px;
+}
+
+.principle-list {
+  display: flex;
+  margin-top: 10px;
+  gap: 7px;
+  flex-direction: column;
+}
+
+.principle-list span {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  color: #555e70;
+  font-size: 9px;
+}
+
+.principle-list i {
+  width: 5px;
+  height: 5px;
+  flex-shrink: 0;
+  border-radius: 50%;
+  background: var(--agent);
+  box-shadow: 0 0 0 3px var(--agent-soft);
+}
+
+.tool-cloud {
+  display: flex;
+  align-content: flex-start;
+  gap: 6px;
+  margin-top: 10px;
+  flex-wrap: wrap;
+}
+
+.tool-cloud span {
+  display: inline-flex;
+  min-height: 23px;
+  align-items: center;
+  padding: 0 8px;
+  border: 1px solid #e5e7ed;
+  border-radius: 6px;
+  background: #fafafd;
+  color: #747c8d;
+  font-size: 7px;
+  white-space: nowrap;
+}
+
+.tool-cloud span::before {
+  width: 3px;
+  height: 3px;
+  margin-right: 5px;
+  border-radius: 50%;
+  background: var(--agent);
+  content: '';
+  opacity: 0.75;
+}
+
+.agent-detail-trigger {
+  display: flex;
+  width: 100%;
+  min-height: 43px;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-top: 15px;
+  padding: 7px 11px;
+  border: 1px solid var(--agent-soft);
+  border-radius: 9px;
+  background: linear-gradient(90deg, var(--agent-soft), #fafafd);
+  color: var(--agent);
+  text-align: left;
+  cursor: pointer;
+  transition: transform 0.16s, border-color 0.16s, box-shadow 0.16s;
+}
+
+.agent-detail-trigger:hover {
+  border-color: var(--agent);
+  box-shadow: 0 8px 20px rgba(43, 51, 73, 0.07);
+  transform: translateY(-1px);
+}
+
+.agent-detail-trigger span {
+  font-size: 8px;
+  font-weight: 800;
+}
+
+.agent-detail-trigger small {
+  display: block;
+  margin-bottom: 2px;
+  color: #9ca2ae;
+  font-size: 5px;
+  font-weight: 900;
+  letter-spacing: 0.14em;
+}
+
+.agent-detail-trigger b {
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.agent-io {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
+  align-items: center;
+  gap: 7px;
+  margin-top: auto;
+  padding-top: 17px;
+}
+
+.agent-io > span {
+  display: flex;
+  min-width: 0;
+  min-height: 42px;
+  align-items: flex-start;
+  justify-content: center;
+  padding: 8px;
+  flex-direction: column;
+  border-radius: 8px;
+  background: #f5f6f9;
+  color: #50596b;
+  font-size: 8px;
+  font-weight: 700;
+}
+
+.agent-io small {
+  display: block;
+  margin-bottom: 3px;
+  color: #a0a6b2;
+  font-size: 6px;
+  font-weight: 800;
+}
+
+.agent-io > i {
+  color: var(--agent);
+  font-size: 12px;
+  font-style: normal;
+}
+
+.agent-flow-arrow {
+  position: absolute;
+  z-index: 3;
+  top: 49%;
+  right: -29px;
+  display: flex;
+  width: 28px;
+  align-items: center;
+}
+
+.agent-flow-arrow i {
+  width: 20px;
+  height: 1px;
+  background: linear-gradient(90deg, var(--agent), #bbc0ca);
+  opacity: 0.6;
+}
+
+.agent-flow-arrow b {
+  color: #8a91a0;
+  font-size: 16px;
+  font-weight: 400;
+}
+
+.agent-foundation {
+  position: relative;
+  z-index: 1;
+  display: grid;
+  min-height: 56px;
+  grid-template-columns: auto 1fr auto;
+  align-items: center;
+  gap: 24px;
+  margin-top: 18px;
+  padding: 0 17px;
+  border-radius: 11px;
+  background: #242b3b;
+  color: #fff;
+}
+
+.foundation-label {
+  color: #9ca5b8;
+  font-size: 7px;
+  font-weight: 900;
+  letter-spacing: 0.14em;
+}
+
+.agent-foundation > div {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 22px;
+}
+
+.agent-foundation > div span {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  color: #d8dce5;
+  font-size: 8px;
+}
+
+.foundation-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+}
+
+.foundation-dot.data { background: #5781dd; }
+.foundation-dot.reason { background: #7d69d8; }
+.foundation-dot.trace { background: #38ad83; }
+
+.agent-foundation > small {
+  color: #838c9f;
+  font-size: 7px;
+  letter-spacing: 0.04em;
+}
+
+.agent-detail-overlay {
+  position: fixed;
+  z-index: 100;
+  inset: 0;
+  display: grid;
+  place-items: center;
+  padding: 28px;
+  background: rgba(20, 25, 38, 0.62);
+  backdrop-filter: blur(10px);
+}
+
+.agent-detail-modal {
+  --detail-agent: #4f78ce;
+  --detail-soft: #eaf1ff;
+  display: flex;
+  width: min(1080px, 100%);
+  height: min(820px, calc(100vh - 56px));
+  flex-direction: column;
+  overflow: hidden;
+  border: 1px solid rgba(255, 255, 255, 0.6);
+  border-radius: 22px;
+  background: #f7f8fb;
+  box-shadow: 0 40px 100px rgba(15, 20, 34, 0.34);
+}
+
+.agent-detail-modal.blue {
+  --detail-agent: #4f78ce;
+  --detail-soft: #eaf1ff;
+}
+
+.agent-detail-modal.violet {
+  --detail-agent: #7060cb;
+  --detail-soft: #efecff;
+}
+
+.agent-detail-modal.orange {
+  --detail-agent: #cc7540;
+  --detail-soft: #fff0e5;
+}
+
+.detail-modal-header {
+  position: relative;
+  display: flex;
+  min-height: 104px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 28px;
+  overflow: hidden;
+  border-bottom: 1px solid #e3e6ed;
+  background:
+    radial-gradient(circle at 20% -50%, var(--detail-soft), transparent 48%),
+    #fff;
+}
+
+.detail-modal-header::after {
+  position: absolute;
+  right: 90px;
+  width: 260px;
+  height: 260px;
+  border: 1px solid var(--detail-agent);
+  border-radius: 50%;
+  content: '';
+  opacity: 0.08;
+}
+
+.detail-agent-identity {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.detail-agent-identity > span {
+  display: grid;
+  width: 54px;
+  height: 54px;
+  place-items: center;
+  border: 1px solid var(--detail-agent);
+  border-radius: 15px;
+  background: var(--detail-soft);
+  color: var(--detail-agent);
+  font-size: 16px;
+  font-weight: 900;
+  box-shadow: 0 10px 25px rgba(41, 48, 68, 0.08);
+}
+
+.detail-agent-identity small {
+  color: var(--detail-agent);
+  font-size: 7px;
+  font-weight: 900;
+  letter-spacing: 0.15em;
+}
+
+.detail-agent-identity h2 {
+  margin: 4px 0 3px;
+  color: #242b3b;
+  font-size: 21px;
+  letter-spacing: -0.035em;
+}
+
+.detail-agent-identity code {
+  color: #9299a8;
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  font-size: 8px;
+}
+
+.detail-modal-header > button {
+  position: relative;
+  z-index: 2;
+  display: grid;
+  width: 36px;
+  height: 36px;
+  place-items: center;
+  padding: 0 0 3px;
+  border: 1px solid #e0e3e9;
+  border-radius: 10px;
+  background: #fff;
+  color: #828998;
+  font-size: 22px;
+  cursor: pointer;
+  transition: border-color 0.15s, color 0.15s, transform 0.15s;
+}
+
+.detail-modal-header > button:hover {
+  border-color: var(--detail-agent);
+  color: var(--detail-agent);
+  transform: rotate(4deg);
+}
+
+.detail-tabs {
+  display: grid;
+  min-height: 67px;
+  grid-template-columns: 1fr 1fr;
+  padding: 0 28px;
+  border-bottom: 1px solid #e2e5ec;
+  background: #fff;
+}
+
+.detail-tabs button {
+  position: relative;
+  display: flex;
+  align-items: flex-start;
+  justify-content: center;
+  padding: 17px 0 0;
+  flex-direction: column;
+  border: 0;
+  background: transparent;
+  color: #969dab;
+  cursor: pointer;
+}
+
+.detail-tabs button::after {
+  position: absolute;
+  right: 0;
+  bottom: -1px;
+  left: 0;
+  height: 2px;
+  border-radius: 3px 3px 0 0;
+  background: transparent;
+  content: '';
+}
+
+.detail-tabs button.active {
+  color: var(--detail-agent);
+}
+
+.detail-tabs button.active::after {
+  background: var(--detail-agent);
+}
+
+.detail-tabs span {
+  font-size: 9px;
+  font-weight: 900;
+  letter-spacing: 0.12em;
+}
+
+.detail-tabs small {
+  margin-top: 4px;
+  color: #a5abb6;
+  font-size: 8px;
+}
+
+.detail-content {
+  min-height: 0;
+  flex: 1;
+  overflow: hidden;
+}
+
+.prompt-panel,
+.tools-panel {
+  display: flex;
+  height: 100%;
+  min-height: 0;
+  flex-direction: column;
+}
+
+.detail-content-head {
+  display: flex;
+  min-height: 61px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 28px;
+  flex-shrink: 0;
+  border-bottom: 1px solid #e4e7ed;
+  background: #f9fafe;
+}
+
+.detail-content-head > div > span {
+  display: block;
+  color: #5f6676;
+  font-size: 8px;
+  font-weight: 900;
+  letter-spacing: 0.13em;
+}
+
+.detail-content-head > div > small {
+  display: block;
+  margin-top: 4px;
+  color: #a1a7b3;
+  font-size: 8px;
+}
+
+.detail-content-head > button {
+  padding: 8px 12px;
+  border: 1px solid #dce0e8;
+  border-radius: 8px;
+  background: #fff;
+  color: #6e7585;
+  font-size: 8px;
+  font-weight: 800;
+  cursor: pointer;
+}
+
+.detail-content-head > button:hover {
+  border-color: var(--detail-agent);
+  color: var(--detail-agent);
+}
+
+.detail-content-head > strong {
+  color: var(--detail-agent);
+  font-size: 9px;
+  letter-spacing: 0.08em;
+}
+
+.prompt-document {
+  min-height: 0;
+  flex: 1;
+  overflow: auto;
+  padding: 26px 30px 50px;
+  background:
+    linear-gradient(90deg, transparent 59px, rgba(104, 111, 139, 0.07) 60px, transparent 61px),
+    #fff;
+}
+
+.prompt-document h3 {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  margin: 30px 0 14px;
+  color: #273044;
+  font-size: 15px;
+  letter-spacing: -0.02em;
+}
+
+.prompt-document h3:first-child {
+  margin-top: 0;
+}
+
+.prompt-document h3 > span {
+  display: grid;
+  width: 31px;
+  height: 24px;
+  flex-shrink: 0;
+  place-items: center;
+  border-radius: 6px;
+  background: var(--detail-soft);
+  color: var(--detail-agent);
+  font-size: 7px;
+  font-weight: 900;
+}
+
+.prompt-document p {
+  max-width: 920px;
+  margin: 0 0 9px 46px;
+  color: #5e6779;
+  font-size: 10px;
+  line-height: 1.8;
+}
+
+.prompt-bullet {
+  position: relative;
+}
+
+.prompt-bullet > i {
+  display: inline-block;
+  width: 5px;
+  height: 5px;
+  margin: 0 8px 1px 0;
+  border-radius: 50%;
+  background: var(--detail-agent);
+  box-shadow: 0 0 0 3px var(--detail-soft);
+}
+
+.prompt-step {
+  display: grid;
+  grid-template-columns: 22px minmax(0, 1fr);
+  align-items: start;
+  gap: 8px;
+}
+
+.prompt-step > b {
+  display: grid;
+  width: 21px;
+  height: 21px;
+  place-items: center;
+  border-radius: 6px;
+  background: #eef0f5;
+  color: #697184;
+  font-size: 7px;
+}
+
+.prompt-space {
+  height: 7px;
+}
+
+.detail-tool-list {
+  display: grid;
+  min-height: 0;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+  flex: 1;
+  overflow: auto;
+  padding: 20px 24px 40px;
+  align-content: start;
+}
+
+.detail-tool-card {
+  display: grid;
+  min-height: 165px;
+  grid-template-columns: 34px minmax(0, 1fr);
+  gap: 13px;
+  padding: 16px;
+  border: 1px solid #e2e5eb;
+  border-radius: 13px;
+  background: #fff;
+  box-shadow: 0 8px 25px rgba(36, 43, 62, 0.035);
+}
+
+.detail-tool-index {
+  display: grid;
+  width: 32px;
+  height: 32px;
+  place-items: center;
+  border-radius: 9px;
+  background: var(--detail-soft);
+  color: var(--detail-agent);
+  font-size: 8px;
+  font-weight: 900;
+}
+
+.detail-tool-main {
+  min-width: 0;
+}
+
+.detail-tool-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+}
+
+.detail-tool-title code {
+  overflow: hidden;
+  color: #3c4558;
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  font-size: 9px;
+  font-weight: 700;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.detail-tool-title span {
+  padding: 3px 5px;
+  border-radius: 4px;
+  background: #f0f2f6;
+  color: #969dab;
+  font-size: 5px;
+  font-weight: 900;
+  letter-spacing: 0.1em;
+}
+
+.detail-tool-main > p {
+  display: -webkit-box;
+  overflow: hidden;
+  margin: 10px 0 12px;
+  color: #747c8d;
+  font-size: 9px;
+  line-height: 1.65;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 4;
+}
+
+.parameter-list {
+  display: flex;
+  gap: 5px;
+  flex-wrap: wrap;
+}
+
+.parameter-list > span {
+  display: inline-flex;
+  min-height: 22px;
+  align-items: center;
+  gap: 5px;
+  padding: 0 7px;
+  border: 1px solid #e3e6ec;
+  border-radius: 5px;
+  background: #fafafd;
+  color: #646d7f;
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  font-size: 7px;
+}
+
+.parameter-list > span.required {
+  border-color: var(--detail-soft);
+}
+
+.parameter-list > span.required::before {
+  color: var(--detail-agent);
+  content: '*';
+}
+
+.parameter-list small {
+  color: #a3a9b4;
+  font-size: 6px;
+}
+
+.detail-state {
+  display: flex;
+  min-height: 0;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  flex: 1;
+  color: #858c9c;
+  font-size: 10px;
+}
+
+.detail-loader {
+  width: 16px;
+  height: 16px;
+  border: 2px solid #dfe2ec;
+  border-top-color: var(--detail-agent);
+  border-radius: 50%;
+  animation: spin 0.75s linear infinite;
+}
+
+.detail-state.detail-error {
+  flex-direction: column;
+  color: #a9534e;
+}
+
+.detail-error strong {
+  font-size: 13px;
+}
+
+.detail-error button {
+  margin-top: 6px;
+  padding: 7px 12px;
+  border: 1px solid #e0b5b2;
+  border-radius: 7px;
+  background: #fff;
+  color: #a9534e;
+  cursor: pointer;
+}
+
+.agent-detail-enter-active,
+.agent-detail-leave-active {
+  transition: opacity 0.2s ease;
+}
+
+.agent-detail-enter-active .agent-detail-modal,
+.agent-detail-leave-active .agent-detail-modal {
+  transition: transform 0.22s ease, opacity 0.22s ease;
+}
+
+.agent-detail-enter-from,
+.agent-detail-leave-to {
+  opacity: 0;
+}
+
+.agent-detail-enter-from .agent-detail-modal,
+.agent-detail-leave-to .agent-detail-modal {
+  opacity: 0;
+  transform: translateY(16px) scale(0.985);
+}
+
+.pipeline-section {
+  overflow: hidden;
+  background: #1d2434;
+  color: #fff;
+}
+
+.pipeline-section .section-index { color: #858da0; }
+.pipeline-section .section-heading > p,
+.pipeline-meta { color: #9ca4b5; }
+.pipeline-meta { display: flex; align-items: center; gap: 16px; }
+
+.live-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  padding: 7px 10px;
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.04);
+}
+
+.live-pill i {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #35c28f;
+  box-shadow: 0 0 0 4px rgba(53, 194, 143, 0.12);
+}
+
+.pipeline-track {
+  display: grid;
+  grid-template-columns: repeat(6, minmax(0, 1fr));
+  gap: 0;
+  margin-top: 32px;
+}
+
+.stage {
+  position: relative;
+  min-width: 0;
+  padding-right: 24px;
+}
+
+.stage-marker {
+  position: relative;
+  z-index: 1;
+  display: grid;
+  width: 36px;
+  height: 36px;
+  place-items: center;
+  border: 5px solid #1d2434;
+  border-radius: 50%;
+  font-size: 9px;
+  font-weight: 800;
+}
+
+.stage-marker.blue { background: #5678e6; }
+.stage-marker.cyan { background: #3f9ab0; }
+.stage-marker.violet { background: #7a67d5; }
+.stage-marker.amber { background: #ca8c3b; }
+.stage-marker.orange { background: #d56842; }
+.stage-marker.green { background: #309772; }
+
+.stage-line {
+  position: absolute;
+  top: 17px;
+  left: 34px;
+  width: calc(100% - 20px);
+  height: 1px;
+  background: linear-gradient(90deg, #4f586d, #343c4e);
+}
+
+.stage-copy h3 {
+  margin: 17px 0 8px;
+  font-size: 14px;
+}
+
+.stage-copy p {
+  margin: 0;
+  color: #949daf;
+  font-size: 10px;
+  line-height: 1.65;
+}
+
+.operations-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1.55fr) minmax(330px, 0.8fr);
+  gap: 16px;
+}
+
+.card-heading {
+  align-items: center;
+}
+
+.card-heading a {
+  color: #666dda;
+  font-size: 11px;
+  font-weight: 700;
+  text-decoration: none;
+}
+
+.date-control {
+  display: flex;
+  align-items: center;
+  gap: 9px;
+  color: #8a91a0;
+  font-size: 10px;
+}
+
+.date-control input {
+  width: 102px;
+  height: 34px;
+  padding: 0 10px;
+  border: 1px solid #dfe2e9;
+  border-radius: 8px;
+  outline: none;
+  color: #343b4b;
+  font: inherit;
+  font-size: 11px;
+  letter-spacing: 0.04em;
+}
+
+.date-control input:focus {
+  border-color: #777ddd;
+  box-shadow: 0 0 0 3px rgba(103, 109, 218, 0.1);
+}
+
+.notice {
+  margin-top: 16px;
+  padding: 10px 12px;
+  border-radius: 9px;
+  background: #eaf7f2;
+  color: #287a61;
+  font-size: 11px;
+}
+
+.notice.error {
+  background: #fff0ef;
+  color: #b64e49;
+}
+
+.job-list,
+.audit-list {
+  margin-top: 18px;
+}
+
+.job-row {
+  display: grid;
+  grid-template-columns: 9px minmax(0, 1fr) auto;
+  align-items: center;
+  gap: 12px;
+  min-height: 59px;
+  border-top: 1px solid #eef0f4;
+}
+
+.job-state {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  background: #c8cdd6;
+}
+
+.job-state.active {
+  background: #5b5bd6;
+  box-shadow: 0 0 0 4px rgba(91, 91, 214, 0.1);
+}
+
+.job-copy {
+  min-width: 0;
+}
+
+.job-copy strong,
+.job-copy small {
+  display: block;
+}
+
+.job-copy strong {
+  font-size: 12px;
+}
+
+.job-copy small {
+  overflow: hidden;
+  margin-top: 3px;
+  color: #9298a6;
+  font-size: 9px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.job-row button {
+  padding: 6px 12px;
+  border: 1px solid #dde0e7;
+  border-radius: 7px;
+  background: #fff;
+  color: #555d6e;
+  font-size: 10px;
+  font-weight: 700;
+  cursor: pointer;
+}
+
+.job-row button:hover:not(:disabled) {
+  border-color: #6868d9;
+  color: #5b5bd6;
+}
+
+.job-row button:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
+.empty-row,
+.empty-audit {
+  display: flex;
+  min-height: 160px;
+  align-items: center;
+  justify-content: center;
+  color: #9ba1ae;
+  font-size: 11px;
+}
+
+.audit-item {
+  display: grid;
+  width: 100%;
+  grid-template-columns: 35px minmax(0, 1fr) auto;
+  align-items: center;
+  gap: 10px;
+  min-height: 59px;
+  padding: 0;
+  border: 0;
+  border-top: 1px solid #eef0f4;
+  background: transparent;
+  text-align: left;
+  cursor: pointer;
+}
+
+.audit-icon {
+  display: grid;
+  width: 32px;
+  height: 32px;
+  place-items: center;
+  border-radius: 9px;
+  background: #f0efff;
+  color: #6262d2;
+  font-size: 9px;
+  font-weight: 800;
+}
+
+.audit-copy {
+  min-width: 0;
+}
+
+.audit-copy strong,
+.audit-copy small {
+  display: block;
+}
+
+.audit-copy strong {
+  overflow: hidden;
+  font-size: 11px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.audit-copy small {
+  margin-top: 3px;
+  color: #9ca2af;
+  font-size: 9px;
+}
+
+.audit-arrow {
+  color: #aeb3bd;
+  font-size: 20px;
+}
+
+.empty-audit {
+  flex-direction: column;
+  text-align: center;
+}
+
+.empty-audit > span {
+  color: #777ddd;
+  font-size: 24px;
+}
+
+.empty-audit strong {
+  margin-top: 8px;
+  color: #656d7c;
+  font-size: 11px;
+}
+
+.empty-audit small {
+  margin-top: 4px;
+  color: #a0a6b1;
+  font-size: 9px;
+}
+
+.overview-footer {
+  display: flex;
+  justify-content: space-between;
+  padding: 4px 8px 0;
+  color: #9aa0ad;
+  font-size: 9px;
+  letter-spacing: 0.05em;
+}
+
+.overview-footer b {
+  color: #5a6171;
+}
+
+.mini-spinner {
+  width: 12px;
+  height: 12px;
+  border: 2px solid rgba(48, 55, 72, 0.2);
+  border-top-color: #303748;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+@media (max-width: 1120px) {
+  .hero { grid-template-columns: 1fr 0.8fr; }
+  .hero-copy { padding-left: 38px; }
+  .feature-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+  .agent-chain { gap: 20px; }
+  .agent-card { padding: 17px; }
+  .agent-flow-arrow { right: -21px; width: 20px; }
+  .agent-flow-arrow i { width: 13px; }
+  .agent-foundation { gap: 14px; }
+  .agent-foundation > div { gap: 13px; }
+  .pipeline-track { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px 0; }
+  .stage:nth-child(3) .stage-line { display: none; }
+}
+
+@media (max-width: 820px) {
+  .hero { grid-template-columns: 1fr; }
+  .hero-copy { padding: 42px 28px 20px; }
+  .hero-visual { min-height: 350px; }
+  .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+  .agent-chain { grid-template-columns: 1fr; gap: 32px; }
+  .agent-card { min-height: auto; }
+  .agent-description { min-height: auto; }
+  .agent-flow-arrow {
+    top: auto;
+    right: 50%;
+    bottom: -29px;
+    width: 28px;
+    transform: translateX(50%) rotate(90deg);
+  }
+  .agent-foundation {
+    grid-template-columns: 1fr;
+    gap: 10px;
+    padding: 16px;
+    text-align: center;
+  }
+  .agent-foundation > div { flex-wrap: wrap; }
+  .agent-foundation > small { line-height: 1.6; }
+  .agent-detail-overlay { padding: 14px; }
+  .agent-detail-modal {
+    height: calc(100vh - 28px);
+    border-radius: 17px;
+  }
+  .detail-tool-list { grid-template-columns: 1fr; }
+  .operations-grid { grid-template-columns: 1fr; }
+  .section-heading { align-items: flex-start; flex-direction: column; }
+}
+
+@media (max-width: 560px) {
+  .overview { gap: 16px; }
+  .hero { border-radius: 18px; }
+  .hero h1 { font-size: 38px; }
+  .hero-actions { align-items: stretch; flex-direction: column; }
+  .metric-grid,
+  .feature-grid { grid-template-columns: 1fr; }
+  .section-block,
+  .operation-card,
+  .audit-card { padding: 20px; border-radius: 16px; }
+  .agent-system-canvas { padding: 72px 11px 11px; }
+  .system-entry { left: 14px; }
+  .system-exit { right: 14px; }
+  .system-entry strong,
+  .system-exit strong { display: none; }
+  .agent-card { padding: 16px; }
+  .agent-foundation > div { align-items: flex-start; flex-direction: column; }
+  .agent-detail-overlay { padding: 0; }
+  .agent-detail-modal {
+    width: 100%;
+    height: 100vh;
+    border: 0;
+    border-radius: 0;
+  }
+  .detail-modal-header { min-height: 91px; padding: 0 17px; }
+  .detail-agent-identity { gap: 11px; }
+  .detail-agent-identity > span { width: 43px; height: 43px; border-radius: 12px; }
+  .detail-agent-identity h2 { font-size: 18px; }
+  .detail-agent-identity code {
+    display: block;
+    max-width: 220px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .detail-tabs { padding: 0 17px; }
+  .detail-content-head { padding: 0 17px; }
+  .prompt-document {
+    padding: 20px 17px 40px;
+    background: #fff;
+  }
+  .prompt-document p { margin-left: 0; }
+  .prompt-document h3 { gap: 9px; }
+  .detail-tool-list { padding: 14px 12px 30px; }
+  .detail-tool-card { grid-template-columns: 28px minmax(0, 1fr); padding: 13px; }
+  .detail-tool-index { width: 27px; height: 27px; }
+  .pipeline-track { grid-template-columns: 1fr; gap: 20px; }
+  .stage { display: grid; grid-template-columns: 38px 1fr; gap: 12px; }
+  .stage-copy h3 { margin-top: 0; }
+  .stage-line { top: 35px; left: 17px; width: 1px; height: calc(100% + 20px); }
+  .stage:nth-child(3) .stage-line { display: block; }
+  .stage:last-child .stage-line { display: none; }
+  .pipeline-meta { align-items: flex-start; flex-direction: column; }
+  .card-heading { align-items: flex-start; flex-direction: column; }
+  .overview-footer { gap: 8px; flex-direction: column; }
+}
+</style>

+ 1 - 1
web/src/views/VideoDiscoveryView.vue

@@ -318,7 +318,7 @@ async function copyText(text: string, key: string) {
 
     <div v-if="loading" class="page-state">
       <span class="state-spinner" />
-      正在整理今日需求与视频
+      正在整理今日需求汇总
     </div>
     <div v-else-if="error" class="page-state error-state">
       <strong>数据加载失败</strong>