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) => ( - - ))} +