diff --git a/frontend/web/src/admin/modules/Automations.tsx b/frontend/web/src/admin/modules/Automations.tsx
index bd67a0c..2763ea0 100644
--- a/frontend/web/src/admin/modules/Automations.tsx
+++ b/frontend/web/src/admin/modules/Automations.tsx
@@ -10,20 +10,101 @@ import {
import type { AdminAutomationRecord } from "../../lib/types";
import { useAuditStore } from "../audit";
import { Notice, PageHeader } from "../components";
-import { Button, Surface, Switch, Tag } from "../../components/primitives";
+import { Button, EmptyState, Surface, Switch, Tag } from "../../components/primitives";
import { Icon } from "../../components/Icon";
-const sample: AdminAutomationRecord[] = [
- { id: "r1", name: "睡前总结", kind: "reminder", cron: "0 22 * * *", enabled: true },
- { id: "r2", name: "晨间问候", kind: "cron", cron: "0 8 * * *", enabled: true },
- { id: "r3", name: "设备待机提醒", kind: "queue", cron: "—", enabled: false },
-];
+interface AutomationRowProps {
+ r: AdminAutomationRecord;
+ busy: boolean;
+ onRun: (id: string, label: string) => void;
+ onDelete: (id: string, label: string) => void;
+}
+
+/** 一条自动化任务行:字段全部来自接口返回值,动作直接打在真实端点上。 */
+function AutomationRow({ r, busy, onRun, onDelete }: AutomationRowProps): React.ReactNode {
+ return (
+
+ {r.name}
+ {r.kind}
+ {r.cron || "—"}
+
+ {}} label="启用" />
+
+
+
+
+ );
+}
+
+interface AutomationListProps {
+ rows: AdminAutomationRecord[];
+ loading: boolean;
+ error: string | null;
+ /** 后端明确表示未提供该端点(404/501)—— 与「请求失败」「真为空」是三种状态。 */
+ unimplemented: boolean;
+ busy: boolean;
+ onRun: (id: string, label: string) => void;
+ onDelete: (id: string, label: string) => void;
+}
+
+/** 列表区:把「加载中 / 端点未实现 / 请求失败 / 真为空 / 有数据」分开渲染。
+ * 每种状态都只反映接口的真实结果,绝不回退到示例条目。 */
+function AutomationList({
+ rows,
+ loading,
+ error,
+ unimplemented,
+ busy,
+ onRun,
+ onDelete,
+}: AutomationListProps): React.ReactNode {
+ if (loading) {
+ return 加载中…
;
+ }
+ if (rows.length === 0) {
+ if (unimplemented) {
+ return (
+
+ 后端未提供 GET /api/v1/admin/automations(404/501):列表读不到。本页只渲染接口返回的真实数据,不显示任何示例条目。
+
+ );
+ }
+ if (error) {
+ return (
+
+ 加载自动化列表失败(后端不可达或接口报错):{error} —— 这是错误态,不是空列表。
+
+ );
+ }
+ return (
+ }
+ title="还没有自动化任务"
+ hint="用上方表单新增第一条提醒 / cron / 队列事件。"
+ />
+ );
+ }
+ return (
+
+ {error ?
刷新失败,下方仍是上次成功读取的数据:{error} : null}
+ {rows.map((r) => (
+
+ ))}
+
+ );
+}
export function AutomationsModule(): React.ReactNode {
- const { data, error, reload } = useAsyncData(adminListAutomations, []);
+ const { data, error, loading, reload } = useAsyncData(adminListAutomations, []);
const log = useAuditStore((s) => s.log);
- const backend = !isNotImplemented(error);
- const rows = backend && data && data.length > 0 ? data : sample;
+ // 端点未实现 / 请求失败必须分开:前者是 404/501,后者是断连或 5xx。
+ const unimplemented = isNotImplemented(error);
+ // 只渲染接口真的返回过的数据;没有数据就是空列表,绝不拿样例顶替。
+ const rows = data ?? [];
const [name, setName] = useState("");
const [kind, setKind] = useState("cron");
@@ -93,9 +174,6 @@ export function AutomationsModule(): React.ReactNode {
/>
{msg ? {msg} : null}
- {backend && !error ? 后端已返回自动化配置(数据以接口为准)。 : null}
- {error && !backend ? 后端未提供 /api/v1/admin/automations,以下为示例壳(M10 提供真实数据)。 : null}
- {error && backend ? {error} : null}
提醒 / CRON / 队列事件
@@ -117,22 +195,15 @@ export function AutomationsModule(): React.ReactNode {
- {rows.map((r) => (
-
- {r.name}
- {r.kind}
- {r.cron || "—"}
-
- {}} label="启用" />
-
-
-
-
- ))}
+
diff --git a/frontend/web/src/admin/modules/Llm.tsx b/frontend/web/src/admin/modules/Llm.tsx
index 8d90218..5a2a6be 100644
--- a/frontend/web/src/admin/modules/Llm.tsx
+++ b/frontend/web/src/admin/modules/Llm.tsx
@@ -1,20 +1,14 @@
import { useState } from "react";
import { useAsyncData } from "../../lib/useAsyncData";
import { adminGetLlm, adminTestLlm, isNotImplemented } from "../../lib/api";
-import type { AdminLlmProvider, AdminLlmConfig } from "../../lib/types";
+import type { AdminLlmProvider } from "../../lib/types";
import { useAuditStore } from "../audit";
import { Notice, PageHeader } from "../components";
-import { Button, Surface, Tag } from "../../components/primitives";
+import { Button, EmptyState, Surface, Tag } from "../../components/primitives";
+import { Icon } from "../../components/Icon";
import { LlmProviderForm } from "./llm/LlmProviderForm";
-const sample: AdminLlmConfig = {
- providers: [
- { id: "p1", name: "主模型", provider: "openai", baseUrl: "https://api.example.com/v1", model: "example-ultra", multimodal: true, enabled: true, maxTokens: 8192 },
- { id: "p2", name: "备选模型", provider: "openai", baseUrl: "https://api.backup.example.com/v1", model: "example-fast", multimodal: false, enabled: true },
- ],
-};
-
-/** One provider group row: identity + current token budget + test/edit actions. */
+/** 一个供应商分组行:身份 + 当前 token 预算 + 测试/编辑动作。 */
function ProviderRow({
p,
busy,
@@ -48,11 +42,71 @@ function ProviderRow({
);
}
+interface ProviderListProps {
+ providers: AdminLlmProvider[];
+ loading: boolean;
+ error: string | null;
+ /** 后端明确表示未提供该端点(404/501)—— 与「请求失败」「真为空」是三种状态。 */
+ unimplemented: boolean;
+ busy: boolean;
+ onTest: (p: AdminLlmProvider) => void;
+ onEdit: (p: AdminLlmProvider) => void;
+}
+
+/** 供应商列表:把「加载中 / 端点未实现 / 请求失败 / 真为空 / 有数据」分开渲染。
+ * 每种状态都只反映接口的真实结果,绝不回退到示例供应商。 */
+function ProviderList({
+ providers,
+ loading,
+ error,
+ unimplemented,
+ busy,
+ onTest,
+ onEdit,
+}: ProviderListProps): React.ReactNode {
+ if (loading) {
+ return 加载中…
;
+ }
+ if (providers.length === 0) {
+ if (unimplemented) {
+ return (
+
+ 后端未提供 GET /api/v1/admin/llm(404/501):配置读不到。本页只渲染接口返回的真实数据,不显示任何示例供应商。
+
+ );
+ }
+ if (error) {
+ return (
+
+ 加载 LLM 配置失败(后端不可达或接口报错):{error} —— 这是错误态,不是空配置。
+
+ );
+ }
+ return (
+ }
+ title="还没有配置 LLM 供应商"
+ hint="用下方表单新增第一个供应商 / 模型分组。"
+ />
+ );
+ }
+ return (
+
+ {error ?
刷新失败,下方仍是上次成功读取的配置:{error} : null}
+ {providers.map((r) => (
+
+ ))}
+
+ );
+}
+
export function LlmModule(): React.ReactNode {
- const { data, error, reload } = useAsyncData(adminGetLlm, []);
+ const { data, error, loading, reload } = useAsyncData(adminGetLlm, []);
const log = useAuditStore((s) => s.log);
- const backend = !isNotImplemented(error);
- const providers = backend && data ? data.providers : sample.providers;
+ // 端点未实现 / 请求失败必须分开:前者是 404/501,后者是断连或 5xx。
+ const unimplemented = isNotImplemented(error);
+ // 只渲染接口真的返回过的供应商;没有就是空列表,绝不拿示例配置顶替。
+ const providers = data?.providers ?? [];
const [editing, setEditing] = useState(null);
const [busy, setBusy] = useState(false);
@@ -83,17 +137,20 @@ export function LlmModule(): React.ReactNode {
subtitle="供应商 + 模型配置(base/key/多模态检测/token 预算) + 测试(真实调用) + fallback + 按角色路由 + 用量"
/>
- {backend && !error ? 后端已返回 LLM 配置(数据以接口为准)。 : null}
- {error && !backend ? 后端未提供 /api/v1/admin/llm,以下为示例壳(M10 提供真实数据)。 : null}
- {error && backend ? {error} : null}
{fail ? {fail} : null}
{result ? {result} : null}
供应商 / 模型
- {providers.map((r) => (
-
- ))}
+