From 7279c9f71f7cf5bbeeb320cdb698f4a8a968ff8a Mon Sep 17 00:00:00 2001 From: bot_dev2 Date: Thu, 6 Aug 2026 12:54:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20issue=20#166=20[A3?= =?UTF-8?q?]=20iAOP=20API=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=B0=81=E8=A3=85?= =?UTF-8?q?=EF=BC=88infer.ts=20LLM=20=E7=BD=91=E5=85=B3=20+=20templates.ts?= =?UTF-8?q?=20=E6=A8=A1=E6=9D=BF=20JSON=20+=20plans=20=E8=BF=81=E5=85=A5?= =?UTF-8?q?=20public=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/iaop/plans/alarm_panel.ti.json | 37 +++ .../public/iaop/plans/resin.json | 236 ++++++++++++++++ .../public/iaop/plans/ti-cl4.json | 260 ++++++++++++++++++ .../apps/web-antdv-next/src/api/iaop/index.ts | 8 + .../apps/web-antdv-next/src/api/iaop/infer.ts | 105 +++++++ .../web-antdv-next/src/api/iaop/templates.ts | 68 +++++ 6 files changed, 714 insertions(+) create mode 100644 deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/alarm_panel.ti.json create mode 100644 deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/resin.json create mode 100644 deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/ti-cl4.json create mode 100644 deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/index.ts create mode 100644 deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts create mode 100644 deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/templates.ts diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/alarm_panel.ti.json b/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/alarm_panel.ti.json new file mode 100644 index 0000000..2eb6cb6 --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/alarm_panel.ti.json @@ -0,0 +1,37 @@ +{ + "subscribe": "alarm_stream", + "severityStyles": { + "P0": { + "fg": "#ff4d4f", + "bg": "rgba(255,77,79,0.10)", + "icon": "alert-octagon", + "border": "#ff4d4f" + }, + "P1": { + "fg": "#faad14", + "bg": "rgba(250,173,20,0.10)", + "icon": "alert-triangle", + "border": "#faad14" + }, + "P2": { + "fg": "#1677ff", + "bg": "rgba(22,119,255,0.08)", + "icon": "info" + } + }, + "rulesSource": { + "kind": "asset", + "ref": "ti-cl4/impurity-forecast/config/alert_rules.template.yaml" + }, + "showSop": true, + "requireAck": true, + "muteLower": "P2", + "groupBy": "severity", + "sortBy": "severity", + "maxItems": 50, + "thresholdsSource": { + "kind": "asset", + "ref": "ti-cl4/impurity-forecast/config/alert_rules.template.yaml" + }, + "ackTimeoutS": 300 +} \ No newline at end of file diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/resin.json b/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/resin.json new file mode 100644 index 0000000..e54ee80 --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/resin.json @@ -0,0 +1,236 @@ +{ + "$schema": "iAOP-cockpit-layout-v1", + "title": "吸附树脂车间驾驶舱", + "theme": "light", + "themeTokens": { + "--cockpit-bg": "#f0f2f5", + "--cockpit-surface": "#ffffff", + "--cockpit-fg": "rgba(0,0,0,0.88)", + "--cockpit-fg-muted": "rgba(0,0,0,0.45)", + "--cockpit-accent": "#1677ff", + "--cockpit-warn": "#faad14", + "--cockpit-grid-line": "rgba(5,5,5,0.06)" + }, + "grid": { + "columns": 12 + }, + "widgets": [ + { + "id": "process_view-0", + "component": "ProcessView", + "type": "process_view", + "grid": { + "columnStart": 1, + "columnSpan": 12, + "rowStart": 1, + "rowSpan": 4, + "widthPct": 100.0, + "style": "grid-column: 1 / span 12; grid-row: 1 / span 4;" + }, + "description": "四状态工艺流程(合成 → 交联 → 洗涤 → 干燥)", + "props": { + "src": "resin_four_state.svg", + "states": [ + "running", + "warning", + "alarm", + "offline" + ] + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "trend-1", + "component": "TrendChart", + "type": "trend", + "grid": { + "columnStart": 1, + "columnSpan": 6, + "rowStart": 5, + "rowSpan": 2, + "widthPct": 50.0, + "style": "grid-column: 1 / span 6; grid-row: 5 / span 2;" + }, + "description": "反应釜温度实时趋势", + "props": { + "series": "R-801.TEMP", + "window": "PT30M" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "trend-2", + "component": "TrendChart", + "type": "trend", + "grid": { + "columnStart": 7, + "columnSpan": 6, + "rowStart": 5, + "rowSpan": 2, + "widthPct": 50.0, + "style": "grid-column: 7 / span 6; grid-row: 5 / span 2;" + }, + "description": "搅拌转速实时趋势", + "props": { + "series": "R-801.AGIT", + "window": "PT30M" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-3", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 1, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 1 / span 3; grid-row: 7 / span 2;" + }, + "description": "当批平均交换容量(mmol/g)", + "props": { + "metric": "resin_exchange_capacity", + "label": "交换容量" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-4", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 4, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 4 / span 3; grid-row: 7 / span 2;" + }, + "description": "当批平均交联度(%)", + "props": { + "metric": "resin_crosslink_degree", + "label": "交联度" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-5", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 7, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 7 / span 3; grid-row: 7 / span 2;" + }, + "description": "当批产率(%)", + "props": { + "metric": "batch_yield", + "label": "批产率" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-6", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 10, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 10 / span 3; grid-row: 7 / span 2;" + }, + "description": "单吨树脂综合能耗(kWh/t)", + "props": { + "metric": "energy_per_ton", + "label": "单吨能耗" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "alarm_panel-7", + "component": "AlarmPanel", + "type": "alarm_panel", + "grid": { + "columnStart": 1, + "columnSpan": 9, + "rowStart": 9, + "rowSpan": 3, + "widthPct": 75.0, + "style": "grid-column: 1 / span 9; grid-row: 9 / span 3;" + }, + "description": "告警面板(温度/交联度/质量预测异常)", + "props": { + "subscribe": "alarm_stream" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "nl_query-8", + "component": "NlQuery", + "type": "nl_query", + "grid": { + "columnStart": 10, + "columnSpan": 3, + "rowStart": 9, + "rowSpan": 3, + "widthPct": 25.0, + "style": "grid-column: 10 / span 3; grid-row: 9 / span 3;" + }, + "description": "自然语言查询入口(配方/质量/能耗问答)", + "props": { + "placeholder": "输入自然语言查询(配方 / 质量 / 能耗)" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + } + ], + "widgetCount": 9, + "perfFlags": { + "virtualScroll": false, + "downsample": false, + "worker": false + }, + "perfHint": null +} \ No newline at end of file diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/ti-cl4.json b/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/ti-cl4.json new file mode 100644 index 0000000..5f963e6 --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/public/iaop/plans/ti-cl4.json @@ -0,0 +1,260 @@ +{ + "$schema": "iAOP-cockpit-layout-v1", + "title": "海绵钛车间驾驶舱", + "theme": "light", + "themeTokens": { + "--cockpit-bg": "#f0f2f5", + "--cockpit-surface": "#ffffff", + "--cockpit-fg": "rgba(0,0,0,0.88)", + "--cockpit-fg-muted": "rgba(0,0,0,0.45)", + "--cockpit-accent": "#1677ff", + "--cockpit-warn": "#faad14", + "--cockpit-grid-line": "rgba(5,5,5,0.06)" + }, + "grid": { + "columns": 12 + }, + "widgets": [ + { + "id": "process_view-0", + "component": "ProcessView", + "type": "process_view", + "grid": { + "columnStart": 1, + "columnSpan": 12, + "rowStart": 1, + "rowSpan": 4, + "widthPct": 100.0, + "style": "grid-column: 1 / span 12; grid-row: 1 / span 4;" + }, + "description": "四状态工艺流程(氯化 → 精制 → 还原 → 蒸馏)", + "props": { + "src": "ti_four_state.svg", + "states": [ + "running", + "warning", + "alarm", + "offline" + ], + "stages": [ + { + "id": "chlorination", + "name": "氯化", + "device": "CLF-01", + "order": 1 + }, + { + "id": "purification", + "name": "精制", + "order": 2 + }, + { + "id": "reduction", + "name": "还原", + "device": "RF-01", + "order": 3 + }, + { + "id": "distillation", + "name": "蒸馏", + "order": 4 + } + ] + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "trend-1", + "component": "TrendChart", + "type": "trend", + "grid": { + "columnStart": 1, + "columnSpan": 6, + "rowStart": 5, + "rowSpan": 2, + "widthPct": 50.0, + "style": "grid-column: 1 / span 6; grid-row: 5 / span 2;" + }, + "description": "氯化炉温度实时趋势(沸腾氯化炉温 850±50℃)", + "props": { + "series": "CLF-01.TEMP", + "window": "PT30M" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "trend-2", + "component": "TrendChart", + "type": "trend", + "grid": { + "columnStart": 7, + "columnSpan": 6, + "rowStart": 5, + "rowSpan": 2, + "widthPct": 50.0, + "style": "grid-column: 7 / span 6; grid-row: 5 / span 2;" + }, + "description": "氯气流量实时趋势(流态化监控)", + "props": { + "series": "CLF-01.CL2", + "window": "PT30M" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-3", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 1, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 1 / span 3; grid-row: 7 / span 2;" + }, + "description": "还原 TiCl₄ 纯度(%,工艺 ≥ 99.9%)", + "props": { + "metric": "ticl4_purity", + "label": "TiCl₄纯度" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-4", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 4, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 4 / span 3; grid-row: 7 / span 2;" + }, + "description": "还原杂质含量(%,越低越好)", + "props": { + "metric": "ticl4_impurity", + "label": "杂质含量" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-5", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 7, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 7 / span 3; grid-row: 7 / span 2;" + }, + "description": "车间累计电耗(kWh,单吨海绵钛综合能耗输入)", + "props": { + "metric": "energy_per_ton", + "label": "累计电耗" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "kpi_card-6", + "component": "KpiCard", + "type": "kpi_card", + "grid": { + "columnStart": 10, + "columnSpan": 3, + "rowStart": 7, + "rowSpan": 2, + "widthPct": 25.0, + "style": "grid-column: 10 / span 3; grid-row: 7 / span 2;" + }, + "description": "蒸汽流量(t/h,公用工程监控)", + "props": { + "metric": "steam_flow", + "label": "蒸汽流量" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "alarm_panel-7", + "component": "AlarmPanel", + "type": "alarm_panel", + "grid": { + "columnStart": 1, + "columnSpan": 9, + "rowStart": 9, + "rowSpan": 3, + "widthPct": 75.0, + "style": "grid-column: 1 / span 9; grid-row: 9 / span 3;" + }, + "description": "告警面板(氯化炉温/还原真空度/纯度/杂质异常)", + "props": { + "subscribe": "alarm_stream" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + }, + { + "id": "nl_query-8", + "component": "NlQuery", + "type": "nl_query", + "grid": { + "columnStart": 10, + "columnSpan": 3, + "rowStart": 9, + "rowSpan": 3, + "widthPct": 25.0, + "style": "grid-column: 10 / span 3; grid-row: 9 / span 3;" + }, + "description": "自然语言查询入口(工艺/质量/能耗问答)", + "props": { + "placeholder": "输入自然语言查询(配方 / 质量 / 能耗)" + }, + "perf": { + "virtualScroll": false, + "downsample": false, + "worker": false + } + } + ], + "widgetCount": 9, + "perfFlags": { + "virtualScroll": false, + "downsample": false, + "worker": false + }, + "perfHint": null +} \ No newline at end of file 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 new file mode 100644 index 0000000..e70ed86 --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/index.ts @@ -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'; diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts new file mode 100644 index 0000000..eb1c988 --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/infer.ts @@ -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; + 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; +} + +/** 带超时的 fetch */ +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', ...(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 | null> { + try { + return await fetchJson>('/health'); + } catch { + return null; + } +} + +/** 模型列表 */ +export async function fetchModels(): Promise { + 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 { + const body = { + model: opts?.model || '', + prompt, + max_tokens: opts?.maxTokens || 1024, + temperature: opts?.temperature ?? 0.1, + }; + return fetchJson('/v1/chat/completions', { + method: 'POST', + body: JSON.stringify(body), + }); +} + +/** 便捷:返回纯文本(B 系列页面统一使用) */ +export async function ask(prompt: string, opts?: Parameters[1]): Promise { + const resp = await chatCompletion(prompt, opts); + const msg = resp.choices?.[0]?.message?.content || ''; + return { + text: msg, + meta: (resp.meta as Record) || {}, + backend: String((resp.meta as Record)?.backend || 'gpu'), + latencyMs: 0, + }; +} diff --git a/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/templates.ts b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/templates.ts new file mode 100644 index 0000000..cda925d --- /dev/null +++ b/deploy/fba/fba-ui-src/apps/web-antdv-next/src/api/iaop/templates.ts @@ -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; + grid?: { rows?: number; cols?: number; gap?: number }; + widgets?: Array>; + [k: string]: unknown; +} + +export interface AlarmPanelConfig { + subscribe?: string; + severityStyles?: Record>; + rulesSource?: string; + showSop?: boolean; + requireAck?: boolean; + muteLower?: number; + [k: string]: unknown; +} + +const CACHE = new Map(); + +async function loadJson(name: string, base: string): Promise { + 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 { + return loadJson(`${template}.json`, '/iaop/plans'); +} + +/** 告警面板配置 */ +export function loadAlarmPanel(): Promise { + return loadJson('alarm_panel.ti.json', '/iaop/plans'); +} + +/** 清理模板缓存(发布新版本后调用) */ +export function clearTemplateCache(): void { + CACHE.clear(); +}