diff --git a/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx b/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx index 99e8e02..63970b1 100644 --- a/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx +++ b/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx @@ -1,10 +1,6 @@ import { useState } from "react"; import { useAsyncData } from "../../../lib/useAsyncData"; -import { isNotImplemented } from "../../../lib/api"; -// 目录端点是本页新引入的调用;按仓库惯例它本该落在 lib/api/admin.ts 里, -// 但本批次只允许改 Llm.tsx(+ 本文件),因此经 lib/api 的传输层直接调用 -// (信封解包 / 401 处理与其余端点一致)。 -import { request } from "../../../lib/api/core"; +import { adminGetLlmCatalog, isNotImplemented } from "../../../lib/api"; import type { AdminLlmProvider } from "../../../lib/types"; import { Notice } from "../../components"; import { Button, Surface, Tag } from "../../../components/primitives"; @@ -382,7 +378,7 @@ export function CatalogPanel({ onPick }: { onPick: (draft: AdminLlmProvider) => const [refreshSeq, setRefreshSeq] = useState(0); const force = refreshSeq > 0; const { data, error, loading } = useAsyncData( - () => request(force ? "/admin/llm/catalog?refresh=1" : "/admin/llm/catalog"), + () => adminGetLlmCatalog({ refresh: force }), [refreshSeq] ); const [pasted, setPasted] = useState([]); diff --git a/frontend/web/src/lib/api.ts b/frontend/web/src/lib/api.ts index 768d4ef..e4e0cd4 100644 --- a/frontend/web/src/lib/api.ts +++ b/frontend/web/src/lib/api.ts @@ -58,6 +58,7 @@ export { adminDeleteRag, adminExportSession, adminGetLlm, + adminGetLlmCatalog, adminGetOwnerProfile, adminIndexRag, adminIoTControl, diff --git a/frontend/web/src/lib/api/admin.ts b/frontend/web/src/lib/api/admin.ts index 1cf7c2f..c4a1bc5 100644 --- a/frontend/web/src/lib/api/admin.ts +++ b/frontend/web/src/lib/api/admin.ts @@ -2,6 +2,7 @@ import type { AdminAutomationRecord, AdminChannelRecord, AdminIoTDevice, + AdminLlmCatalog, AdminLlmConfig, AdminLlmProvider, AdminPluginRecord, @@ -90,6 +91,14 @@ export function adminListUsers(): Promise { export function adminGetLlm(): Promise { return request("/admin/llm"); } +/** GET /api/v1/admin/llm/catalog — models.dev 候选目录(RBAC owner|admin),面板 + * 「从模型目录自动化填入」的数据来源。服务端优先用本地快照,opts.refresh 时带 + * `?refresh=1` 强制在线刷新;无快照又抓不到时后端回 503 catalog_unavailable, + * 调用方据此提示改用「粘贴导入」,而不是当空目录渲染。 */ +export function adminGetLlmCatalog(opts: { refresh?: boolean } = {}): Promise { + const qs = opts.refresh ? "?refresh=1" : ""; + return request(`/admin/llm/catalog${qs}`); +} /** GET /api/v1/admin/automations */ export function adminListAutomations(): Promise { return request("/admin/automations"); diff --git a/frontend/web/src/lib/types/admin.ts b/frontend/web/src/lib/types/admin.ts index c147882..afd06c6 100644 --- a/frontend/web/src/lib/types/admin.ts +++ b/frontend/web/src/lib/types/admin.ts @@ -142,6 +142,45 @@ export interface AdminLlmConfig { providers: AdminLlmProvider[]; } +/** 目录里的一个模型能力位(GET /api/v1/admin/llm/catalog)。attachment 为 true + * 表示该模型接受图片/文件输入(→ 面板的 multimodal);两个 limit 缺失/为 0 + * 表示「目录未标注」,不是猜测值。 */ +export interface AdminLlmCatalogModel { + id: string; + name: string; + attachment: boolean; + reasoning: boolean; + toolCall: boolean; + contextLimit?: number; + outputLimit?: number; +} + +/** 目录里的一个供应商:baseURL + 模型清单 + 申请 key 的提示(env 只是变量名 + * 提示,面板不写 .env)。 */ +export interface AdminLlmCatalogProvider { + id: string; + name: string; + /** 目录未标注 baseURL 时缺省,面板会如实提示需手填。 */ + api?: string; + doc?: string; + env?: string[]; + /** 目录标注的 SDK 包名,面板据此推荐 wire 协议。 */ + npm?: string; + models: AdminLlmCatalogModel[]; +} + +/** GET /api/v1/admin/llm/catalog 的响应(models.dev 候选目录)。source 说明候选 + * 从哪来:`cache`(本地快照)或 `models.dev`(本次在线抓取);stale + warning + * 标记「在线刷新失败、退回上次快照」的离线兜底。两者都拿不到时后端回 + * 503 catalog_unavailable(不是空目录)。 */ +export interface AdminLlmCatalog { + source: string; + fetchedAt?: string; + stale?: boolean; + warning?: string; + providers: AdminLlmCatalogProvider[]; +} + /** An automation / reminder entry (GET/POST /api/v1/admin/automations, planned). */ export interface AdminAutomationRecord { id: string;