diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/index.ts b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/index.ts index e70ed86..cec7530 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/index.ts +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/index.ts @@ -1,8 +1,10 @@ /** * iAOP API 客户端出口(Epic #163 / issue #166 [A3]) * - * - infer.ts —— LLM 网关(/v1/*:health / models / chat/completions) - * - templates.ts —— 驾驶舱布局与告警面板模板 JSON(public/iaop/plans) + * - infer.ts —— LLM 网关(/v1/*:health / models / chat/completions) + * - templates.ts —— 驾驶舱布局与告警面板模板 JSON(public/iaop/plans) + * - registry.ts —— 模型注册表 HTTP API(E1 #182 / E2 #183:list/register/promote/rollback) */ export * from './infer'; +export * from './registry'; export * from './templates'; diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/registry.ts b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/registry.ts new file mode 100644 index 0000000..6a8b45b --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/registry.ts @@ -0,0 +1,147 @@ +/** + * iAOP 模型注册表 API 客户端(issue #183 [E2] / PRD 5.3 ③) + * + * 对接 E1 后端 ``core/model-framework/registry_api.py``(nginx 反代 + * ``/api/v1/registry/`` → :8090),四接口: + * + * GET /api/v1/registry/models 列表(可按 stage 过滤) + * POST /api/v1/registry/models 注册新版本(服务端校验) + * POST /api/v1/registry/models/{name}/{ver}/promote dev→staging→prod + * POST /api/v1/registry/models/{name}/rollback?version=&stage= 回滚 + * + * 鉴权:读接口接 FBA JWT,写接口要求权限码 iaop:admin/iaop:studio。 + * 复用 FBA requestClient 的 token 注入(``useAccessStore().accessToken``), + * 但注册表走 iAOP 后端独立路径 ``/api/v1/registry/``(非 /fba),故用原生 + * fetch 封装,与 infer.ts 同款风格(超时 / 错误信息 / 结果结构统一)。 + * + * 调用方应在 catch 中降级到 localStorage 演示模式(见 views/iaop/admin/models.vue)。 + */ +import { useAccessStore } from '@vben/stores'; + +const BASE_URL = '/api/v1/registry'; +const TIMEOUT_MS = 10000; + +/** 注册表模型视图(对齐后端 registry_api._tpl_view) */ +export interface RegistryModel { + name: string; + version: string; + backbone: string; + stage: 'dev' | 'staging' | 'prod'; + hyperparams?: Record; + feature_columns?: string[]; + target_column?: string; + metrics?: Record; + registered_at: number; + description?: string; + extra?: Record; + /** 种子/迁移字段(extra.author 提到顶层,便于前端展示) */ + author?: string; +} + +/** 列表响应 */ +interface ListResponse { + models: RegistryModel[]; + total: number; +} + +/** 是否处于「服务不可达」错误(调用方据此降级 localStorage) */ +export function isRegistryUnavailable(err: unknown): boolean { + if (!(err instanceof Error)) return false; + return /Failed to fetch|NetworkError|超时|HTTP 5\d\d|HTTP 0/i.test(err.message); +} + +/** 注册新版本的请求体 */ +export interface RegisterModelParams { + name: string; + version: string; + backbone?: string; + stage?: 'dev' | 'staging' | 'prod'; + hyperparams?: Record; + feature_columns?: string[]; + target_column?: string; + metrics?: Record; + description?: string; + extra?: Record; +} + +/** 取当前 FBA access token(与 requestClient 同源注入) */ +function authHeader(): Record { + try { + const token = useAccessStore().accessToken; + return token ? { Authorization: `Bearer ${token}` } : {}; + } catch { + // store 未就绪(如独立运行 / 单测)时降级无鉴权 + return {}; + } +} + +/** 带超时的 fetch + FBA token 注入 */ +async function fetchJson(path: string, init?: RequestInit): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + try { + const resp = await fetch(`${BASE_URL}${path}`, { + ...init, + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + ...authHeader(), + ...(init?.headers || {}), + }, + }); + if (!resp.ok) { + // 401/403/404 等业务错误:尝试读后端 {error} 字段,否则给 HTTP 码 + let detail = `HTTP ${resp.status}`; + try { + const body = await resp.json(); + if (body?.error) detail = body.error; + } catch { /* 非 JSON 错误体忽略 */ } + throw new Error(detail); + } + const text = await resp.text(); + return (text ? JSON.parse(text) : {}) as T; + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + throw new Error(`注册表服务请求超时(>${TIMEOUT_MS / 1000}s)`); + } + throw err; + } finally { + clearTimeout(timer); + } +} + +/** 列表(可按 stage 过滤)。失败抛错,调用方降级。 */ +export async function listModels(stage?: 'dev' | 'staging' | 'prod'): Promise { + const qs = stage ? `?stage=${encodeURIComponent(stage)}` : ''; + const data = await fetchJson(`/models${qs}`); + return data.models || []; +} + +/** 注册新版本。冲突(同 name+version 已存在)抛 409 错误。 */ +export async function registerModel(params: RegisterModelParams): Promise { + return fetchJson('/models', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +/** 提升 dev→staging→prod(逐级)。已在 prod 抛错。 */ +export async function promoteModel(name: string, version: string): Promise { + return fetchJson( + `/models/${encodeURIComponent(name)}/${encodeURIComponent(version)}/promote`, + { method: 'POST' }, + ); +} + +/** 回滚某 stage 指针到指定版本。 */ +export async function rollbackModel( + name: string, + version: string, + stage: 'dev' | 'staging' | 'prod' = 'prod', +): Promise { + const qs = `?version=${encodeURIComponent(version)}&stage=${encodeURIComponent(stage)}`; + return fetchJson( + `/models/${encodeURIComponent(name)}/rollback${qs}`, + { method: 'POST' }, + ); +} diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/admin/models.vue b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/admin/models.vue index 1279f25..f97457d 100644 --- a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/admin/models.vue +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/views/iaop/admin/models.vue @@ -1,17 +1,24 @@