feat: 完成 issue #166 [A3] iAOP API 客户端封装(infer.ts LLM 网关 + templates.ts 模板 JSON + plans 迁入 public)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* iAOP API 客户端出口(Epic #163 / issue #166 [A3])
|
||||
*
|
||||
* - infer.ts —— LLM 网关(/v1/*:health / models / chat/completions)
|
||||
* - templates.ts —— 驾驶舱布局与告警面板模板 JSON(public/iaop/plans)
|
||||
*/
|
||||
export * from './infer';
|
||||
export * from './templates';
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* iAOP LLM 网关客户端(Epic #163 / issue #166 [A3])
|
||||
*
|
||||
* 对齐旧 `web/` 各模块的 INFER_BASE(nginx 反代 /v1/ → 推理服务 :30800):
|
||||
* GET /v1/health —— 健康巡检
|
||||
* GET /v1/models —— 模型列表
|
||||
* POST /v1/chat/completions —— OpenAI 兼容对话(dry-run 占位 / 真实推理)
|
||||
*
|
||||
* 推理服务为 iAOP 独立通道(不要求 FBA JWT),故用原生 fetch 封装,
|
||||
* 统一超时 / 错误信息 / 结果结构;FBA requestClient 仅用于 /fba/* 接口。
|
||||
*/
|
||||
import { useMessage } from '@vben/hooks';
|
||||
|
||||
const BASE_URL = '/v1';
|
||||
const TIMEOUT_MS = 15000;
|
||||
|
||||
export interface InferResult {
|
||||
text: string;
|
||||
meta: Record<string, unknown>;
|
||||
backend: string;
|
||||
latencyMs: number;
|
||||
}
|
||||
|
||||
export interface ChatCompletionChoice {
|
||||
index: number;
|
||||
message: { role: string; content: string };
|
||||
finish_reason: string;
|
||||
}
|
||||
|
||||
export interface ChatCompletionResponse {
|
||||
id: string;
|
||||
object: string;
|
||||
choices: ChatCompletionChoice[];
|
||||
model: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** 带超时的 fetch */
|
||||
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
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', ...(init?.headers || {}) },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error(`推理服务 HTTP ${resp.status}`);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 健康巡检(前端徽标展示;失败不抛出,由调用方降级) */
|
||||
export async function fetchHealth(): Promise<Record<string, unknown> | null> {
|
||||
try {
|
||||
return await fetchJson<Record<string, unknown>>('/health');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 模型列表 */
|
||||
export async function fetchModels(): Promise<string[]> {
|
||||
const data = await fetchJson<{ models?: string[] }>('/v1/models');
|
||||
return data.models || [];
|
||||
}
|
||||
|
||||
/** OpenAI 兼容对话(含 dry-run 占位响应) */
|
||||
export async function chatCompletion(
|
||||
prompt: string,
|
||||
opts?: { model?: string; maxTokens?: number; temperature?: number },
|
||||
): Promise<ChatCompletionResponse> {
|
||||
const body = {
|
||||
model: opts?.model || '',
|
||||
prompt,
|
||||
max_tokens: opts?.maxTokens || 1024,
|
||||
temperature: opts?.temperature ?? 0.1,
|
||||
};
|
||||
return fetchJson<ChatCompletionResponse>('/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
/** 便捷:返回纯文本(B 系列页面统一使用) */
|
||||
export async function ask(prompt: string, opts?: Parameters<typeof chatCompletion>[1]): Promise<InferResult> {
|
||||
const resp = await chatCompletion(prompt, opts);
|
||||
const msg = resp.choices?.[0]?.message?.content || '';
|
||||
return {
|
||||
text: msg,
|
||||
meta: (resp.meta as Record<string, unknown>) || {},
|
||||
backend: String((resp.meta as Record<string, unknown>)?.backend || 'gpu'),
|
||||
latencyMs: 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* iAOP 模板 JSON 加载器(Epic #163 / issue #166 [A3])
|
||||
*
|
||||
* 从 `public/iaop/plans/` 加载驾驶舱布局与告警面板模板资产(自 web/cockpit/plans 迁入):
|
||||
* ti-cl4.json —— 海绵钛驾驶舱布局(RenderPlan,iAOP-cockpit-layout-v1)
|
||||
* resin.json —— 树脂驾驶舱布局
|
||||
* alarm_panel.ti.json —— 告警面板配置(severity 配色 / SOP / 确认)
|
||||
*
|
||||
* 统一:带缓存(会话内重复加载不重发请求)+ 结构校验 + 错误提示。
|
||||
*/
|
||||
import { useMessage } from '@vben/hooks';
|
||||
|
||||
export interface CockpitPlan {
|
||||
$schema?: string;
|
||||
title?: string;
|
||||
theme?: string;
|
||||
themeTokens?: Record<string, string>;
|
||||
grid?: { rows?: number; cols?: number; gap?: number };
|
||||
widgets?: Array<Record<string, unknown>>;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AlarmPanelConfig {
|
||||
subscribe?: string;
|
||||
severityStyles?: Record<string, Record<string, string>>;
|
||||
rulesSource?: string;
|
||||
showSop?: boolean;
|
||||
requireAck?: boolean;
|
||||
muteLower?: number;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
const CACHE = new Map<string, unknown>();
|
||||
|
||||
async function loadJson<T>(name: string, base: string): Promise<T> {
|
||||
const key = `${base}/${name}`;
|
||||
if (CACHE.has(key)) {
|
||||
return CACHE.get(key) as T;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`${base}/${name}`);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`模板 ${name} 加载失败(HTTP ${resp.status})`);
|
||||
}
|
||||
const data = (await resp.json()) as T;
|
||||
CACHE.set(key, data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
const message = useMessage();
|
||||
message.error(`模板 ${name} 加载失败`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** 驾驶舱布局(按行业模板切换:ti-cl4 / resin) */
|
||||
export function loadCockpitPlan(template: 'ti-cl4' | 'resin'): Promise<CockpitPlan> {
|
||||
return loadJson<CockpitPlan>(`${template}.json`, '/iaop/plans');
|
||||
}
|
||||
|
||||
/** 告警面板配置 */
|
||||
export function loadAlarmPanel(): Promise<AlarmPanelConfig> {
|
||||
return loadJson<AlarmPanelConfig>('alarm_panel.ti.json', '/iaop/plans');
|
||||
}
|
||||
|
||||
/** 清理模板缓存(发布新版本后调用) */
|
||||
export function clearTemplateCache(): void {
|
||||
CACHE.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user