diff --git a/frontend/web/src/admin/modules/Dashboard.tsx b/frontend/web/src/admin/modules/Dashboard.tsx
index 1fa997e..9382722 100644
--- a/frontend/web/src/admin/modules/Dashboard.tsx
+++ b/frontend/web/src/admin/modules/Dashboard.tsx
@@ -10,9 +10,10 @@ import {
import { useAppStore } from "../../store/useAppStore";
import { navigateAdmin, type AdminSectionId } from "../router";
import { CardGrid, StatCard } from "../components";
-import { Surface, Tag } from "../../components/primitives";
+import { Tag } from "../../components/primitives";
import { Icon, type IconName } from "../../components/Icon";
import { useAutoReload } from "../hooks";
+import { OverviewSection } from "./dashboard/OverviewSection";
interface DashCard {
id: AdminSectionId;
@@ -88,28 +89,7 @@ export function DashboardModule(): React.ReactNode {
))}
-
-
-
资源 / LLM / 业务概览
- 占位(后端 M10+ 提供)
-
-
-
-
-
-
- 卡片即入口,点击上溯卡片下钻到对应模块。
-
-
- );
-}
-
-function MiniMetric({ label, value, hint }: { label: string; value: string; hint: string }): React.ReactNode {
- return (
-
-
{label}
-
{value}
-
{hint}
+
);
}
diff --git a/frontend/web/src/admin/modules/dashboard/MiniMetric.tsx b/frontend/web/src/admin/modules/dashboard/MiniMetric.tsx
new file mode 100644
index 0000000..4392a3b
--- /dev/null
+++ b/frontend/web/src/admin/modules/dashboard/MiniMetric.tsx
@@ -0,0 +1,54 @@
+import type { ReactNode } from "react";
+import { Icon, type IconName } from "../../../components/Icon";
+import { Tag, type TagTone } from "../../../components/primitives";
+import { navigateAdmin, type AdminSectionId } from "../../router";
+
+/** Honest per-card state: real data landed / still loading / endpoint failed /
+ * no endpoint exists in lib/api.ts (a placeholder, never a fabricated value). */
+export type MetricState = "ok" | "loading" | "error" | "placeholder";
+
+/** One derived overview card (design/10 §4: 卡片多而全, 详情在各独立页面). */
+export interface MiniMetricSpec {
+ icon: IconName;
+ label: string;
+ value: string;
+ hint: string;
+ state: MetricState;
+ /** Admin module this card drills down to; absent = not a link. */
+ section?: AdminSectionId;
+}
+
+const STATE_BADGE: Record = {
+ ok: { label: "已接入", tone: "ok" },
+ loading: { label: "加载中", tone: "neutral" },
+ error: { label: "端点报错", tone: "err" },
+ placeholder: { label: "占位", tone: "warn" },
+};
+
+export function MiniMetric({ icon, label, value, hint, state, section }: MiniMetricSpec): ReactNode {
+ const badge = STATE_BADGE[state];
+ const body = (
+ <>
+
+
+ {label}
+ {badge.label}
+
+ {value}
+ {hint}
+ >
+ );
+ const base =
+ "rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-3";
+ if (!section) return {body}
;
+ return (
+
+ );
+}
diff --git a/frontend/web/src/admin/modules/dashboard/OverviewSection.tsx b/frontend/web/src/admin/modules/dashboard/OverviewSection.tsx
new file mode 100644
index 0000000..0430181
--- /dev/null
+++ b/frontend/web/src/admin/modules/dashboard/OverviewSection.tsx
@@ -0,0 +1,29 @@
+import type { ReactNode } from "react";
+import { Surface } from "../../../components/primitives";
+import { MiniMetric } from "./MiniMetric";
+import { buildOverviewMetrics } from "./overviewMetrics";
+import { useDashboardExtras } from "./useDashboardExtras";
+
+/** The secondary dashboard block: 资源 / LLM / 业务 aggregates pulled from the
+ * real admin endpoints (design/10 §3 #1, §4). Each card drills down; the most
+ * detailed view always lives on its own module page. */
+export function OverviewSection(): ReactNode {
+ const extras = useDashboardExtras();
+ const metrics = buildOverviewMetrics(extras);
+ return (
+
+
+
资源 / LLM / 业务概览
+ 真实聚合 · 每卡可下钻
+
+
+ {metrics.map((m) => (
+
+ ))}
+
+
+ 卡片即入口,点击下钻到对应模块;缺端点的卡片只标注缺口,不伪造数值。
+
+
+ );
+}
diff --git a/frontend/web/src/admin/modules/dashboard/overviewMetrics.ts b/frontend/web/src/admin/modules/dashboard/overviewMetrics.ts
new file mode 100644
index 0000000..b338e2c
--- /dev/null
+++ b/frontend/web/src/admin/modules/dashboard/overviewMetrics.ts
@@ -0,0 +1,203 @@
+/**
+ * Pure derivation of the dashboard's secondary overview cards from the loaded
+ * API sources (design/10 §4: 卡片多而全, 最详细信息在各独立页面). Nothing here
+ * invents a value: a source that errored reports the error, an empty list says
+ * so, and a capability with no read endpoint in `lib/api.ts` stays a placeholder.
+ */
+import type { IconName } from "../../../components/Icon";
+import type {
+ AdminAutomationRecord,
+ AdminLlmConfig,
+ AdminObservabilityMetrics,
+ AdminRagResponse,
+ AdminUserRecord,
+ InstalledPlugin,
+} from "../../../lib/types";
+import type { AdminSectionId } from "../../router";
+import type { MetricState, MiniMetricSpec } from "./MiniMetric";
+import type { DashboardExtras, Loaded } from "./useDashboardExtras";
+
+/** Runtime constant from the agent loop (design/03): the tool loop is capped at
+ * 8 rounds before the agent must answer. Shown as a fact, not as a metric. */
+const TOOL_LOOP_CAP = 8;
+
+export function buildOverviewMetrics(extras: DashboardExtras): MiniMetricSpec[] {
+ return [
+ llmMetric(extras.llm),
+ ragMetric(extras.rag),
+ automationMetric(extras.automations),
+ pluginMetric(extras.installed),
+ userMetric(extras.users),
+ gatewayMetric(extras.metrics),
+ agentCallMetric(extras.metrics),
+ storageMetric(),
+ ];
+}
+
+function stateOf(source: Loaded): MetricState {
+ if (source.data != null) return "ok";
+ if (source.error != null) return "error";
+ return "loading";
+}
+
+/** A source that has not produced data yet: an em dash instead of a number, and
+ * the endpoint's own error text when it failed. */
+function pending(
+ icon: IconName,
+ label: string,
+ planned: string,
+ source: Loaded,
+ section?: AdminSectionId
+): MiniMetricSpec {
+ return {
+ icon,
+ label,
+ value: "—",
+ hint: source.error ? `端点报错:${source.error}` : planned,
+ state: stateOf(source),
+ section,
+ };
+}
+
+function llmMetric(llm: Loaded): MiniMetricSpec {
+ if (llm.data == null) {
+ return pending("robot", "LLM 供应商 / 模型", "多模态检测 / fallback / 角色路由", llm, "llm");
+ }
+ const providers = llm.data.providers;
+ const enabled = providers.filter((p) => p.enabled).length;
+ const multimodal = providers.filter((p) => p.multimodal).length;
+ const fallback = providers.filter((p) => p.fallback).length;
+ return {
+ icon: "robot",
+ label: "LLM 供应商 / 模型",
+ value: `${providers.length} 组模型`,
+ hint:
+ providers.length === 0
+ ? "未配置供应商"
+ : `启用 ${enabled} · 多模态 ${multimodal} · 带 fallback ${fallback}`,
+ state: "ok",
+ section: "llm",
+ };
+}
+
+function ragMetric(rag: Loaded): MiniMetricSpec {
+ if (rag.data == null) {
+ return pending("docs", "知识库 RAG", "文档 / 分块 / 嵌入状态", rag, "rag");
+ }
+ const docs = rag.data.documents;
+ const chunks = docs.reduce((n, d) => n + d.chunks, 0);
+ const embedded = docs.filter((d) => d.embedded).length;
+ return {
+ icon: "docs",
+ label: "知识库 RAG",
+ value: `${docs.length} 文档`,
+ hint: docs.length === 0 ? "暂无知识文档" : `${chunks} 分块 · ${embedded}/${docs.length} 已嵌入`,
+ state: "ok",
+ section: "rag",
+ };
+}
+
+function automationMetric(autos: Loaded): MiniMetricSpec {
+ if (autos.data == null) {
+ return pending("clock", "自动化与提醒", "cron / 事件 / 后台任务", autos, "automations");
+ }
+ const rows = autos.data;
+ const enabled = rows.filter((a) => a.enabled).length;
+ const next = rows.map((a) => a.nextRun).filter((t): t is string => Boolean(t)).sort()[0];
+ return {
+ icon: "clock",
+ label: "自动化与提醒",
+ value: `${rows.length} 条规则`,
+ hint: rows.length === 0 ? "暂无自动化规则" : `启用 ${enabled} · 下次运行 ${formatTime(next)}`,
+ state: "ok",
+ section: "automations",
+ };
+}
+
+function pluginMetric(installed: Loaded): MiniMetricSpec {
+ if (installed.data == null) {
+ return pending("plugin", "已安装插件 / MCP", "liveness / 工具数", installed, "plugins");
+ }
+ const rows = installed.data;
+ const live = rows.filter((p) => p.live).length;
+ const tools = rows.reduce((n, p) => n + p.toolCount, 0);
+ return {
+ icon: "plugin",
+ label: "已安装插件 / MCP",
+ value: `${rows.length} 个`,
+ hint: rows.length === 0 ? "无运行时安装插件" : `${live} 在线 · 提供 ${tools} 个工具`,
+ state: "ok",
+ section: "plugins",
+ };
+}
+
+function userMetric(users: Loaded): MiniMetricSpec {
+ if (users.data == null) {
+ return pending("users", "用户与权限", "RBAC / API Key", users, "users");
+ }
+ const rows = users.data;
+ const owners = rows.filter((u) => u.role === "owner").length;
+ const admins = rows.filter((u) => u.role === "admin").length;
+ const viewers = rows.length - owners - admins;
+ return {
+ icon: "users",
+ label: "用户与权限",
+ value: `${rows.length} 用户`,
+ hint: rows.length === 0 ? "暂无后端用户" : `owner ${owners} · admin ${admins} · viewer ${viewers}`,
+ state: "ok",
+ section: "users",
+ };
+}
+
+function gatewayMetric(m: Loaded): MiniMetricSpec {
+ if (m.data == null) {
+ return pending("trend", "网关请求", "QPS / 延迟 / 状态码", m, "observability");
+ }
+ const d = m.data;
+ return {
+ icon: "trend",
+ label: "网关请求",
+ value: `${d.totalRequests} 次`,
+ hint: `QPS ${d.qps} · 平均 ${d.avgLatencyMs}ms · 峰值 ${d.maxLatencyMs}ms`,
+ state: "ok",
+ section: "observability",
+ };
+}
+
+function agentCallMetric(m: Loaded): MiniMetricSpec {
+ if (m.data == null) {
+ return pending("agent", "LLM 调用 / 子代理", `工具循环上限 ${TOOL_LOOP_CAP} 轮`, m, "observability");
+ }
+ const d = m.data;
+ return {
+ icon: "agent",
+ label: "LLM 调用 / 子代理",
+ value: `${d.llmCalls} 次`,
+ hint: `子代理事件 ${d.subagentEvents} · 工具循环上限 ${TOOL_LOOP_CAP} 轮`,
+ state: "ok",
+ section: "observability",
+ };
+}
+
+/** Storage has no read-only endpoint in `lib/api.ts` (backup/restore are writes
+ * that would touch the filesystem), so this card states the configured default
+ * and names the missing endpoint instead of showing a made-up figure. */
+function storageMetric(): MiniMetricSpec {
+ return {
+ icon: "storage",
+ label: "存储",
+ value: "SQLite",
+ hint: "默认引擎 · PG/pgvector 可选增强;缺只读查询端点(未在 lib/api.ts 暴露)",
+ state: "placeholder",
+ section: "system",
+ };
+}
+
+/** ISO → local "MM-DD HH:mm"; absent or unparseable values render as an em dash. */
+function formatTime(iso?: string): string {
+ if (!iso) return "—";
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return iso;
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
diff --git a/frontend/web/src/admin/modules/dashboard/useDashboardExtras.ts b/frontend/web/src/admin/modules/dashboard/useDashboardExtras.ts
new file mode 100644
index 0000000..69ec45c
--- /dev/null
+++ b/frontend/web/src/admin/modules/dashboard/useDashboardExtras.ts
@@ -0,0 +1,54 @@
+import {
+ adminGetLlm,
+ adminListAutomations,
+ adminListInstalledPlugins,
+ adminListRag,
+ adminListUsers,
+ adminObservabilityMetrics,
+} from "../../../lib/api";
+import { useAsyncData } from "../../../lib/useAsyncData";
+import type {
+ AdminAutomationRecord,
+ AdminLlmConfig,
+ AdminObservabilityMetrics,
+ AdminRagResponse,
+ AdminUserRecord,
+ InstalledPlugin,
+} from "../../../lib/types";
+import { useAutoReload } from "../../hooks";
+
+/** The load state of one async source. Structurally identical to what
+ * `useAsyncData` returns, so its result passes straight through. */
+export interface Loaded {
+ data: T | null;
+ loading: boolean;
+ error: string | null;
+}
+
+export interface DashboardExtras {
+ llm: Loaded;
+ metrics: Loaded;
+ installed: Loaded;
+ automations: Loaded;
+ rag: Loaded;
+ users: Loaded;
+}
+
+/** The secondary dashboard sources, all from endpoints that already exist in
+ * `lib/api.ts`. The dashboard is an overview, not a live console, so each one
+ * refreshes on its own slow interval. */
+export function useDashboardExtras(): DashboardExtras {
+ const llm = useAsyncData(adminGetLlm, []);
+ const metrics = useAsyncData(adminObservabilityMetrics, []);
+ const installed = useAsyncData(adminListInstalledPlugins, []);
+ const automations = useAsyncData(adminListAutomations, []);
+ const rag = useAsyncData(adminListRag, []);
+ const users = useAsyncData(adminListUsers, []);
+ useAutoReload(llm.reload, 30000);
+ useAutoReload(metrics.reload, 15000);
+ useAutoReload(installed.reload, 30000);
+ useAutoReload(automations.reload, 30000);
+ useAutoReload(rag.reload, 30000);
+ useAutoReload(users.reload, 30000);
+ return { llm, metrics, installed, automations, rag, users };
+}