/** * ai-server OpenApi(MOC 插件)接入层 * 契约:《变更管理系统插件接口说明文档 V1.0》(ai-server Module/OpenApi) * - base 走 vite 代理 /proxy-ai -> http://127.0.0.1:12333(见 vite.config.ts) * - 鉴权:app 凭据换应用令牌(缓存 2h)→ exchange 换会员 JWT(ai_token,会话级缓存) * - 本地联调形态:前端直连 exchange(moc-server 的登录双令牌下发链路就绪后, * 改为登录时由 moc-server 下发 ai_token,本层仅需替换 getAiToken 来源) */ import { request } from '../request/ai'; /** ai-server 统一响应 */ interface AiResp { code: number; message: string; data: T; } export class AiError extends Error { code: number; data: any; constructor(code: number, message: string, data?: any) { super(message || `AI 服务错误(${code})`); this.code = code; this.data = data; } } const BASE = (import.meta.env.VITE_AI_BASE_URL as string) || '/proxy-ai'; const APP_ID = (import.meta.env.VITE_AI_APP_ID as string) || 'moc_jcyh'; const APP_SECRET = (import.meta.env.VITE_AI_APP_SECRET as string) || 'moc-jcyh-dev-secret'; /** exchange 用的当前 MOC 用户身份(登录成功后由调用方注入) */ export interface AiUserIdentity { platform_user_id: string; align_key: string; display_name: string; } let identity: AiUserIdentity = { // 本地联调默认用户(文档 §8.2) platform_user_id: '1001', align_key: 'zhang.gong@jcyh.local', display_name: '张工' }; /** 登录后注入真实用户身份;用户变更会强制重新 exchange */ export function setAiUser(user: Partial) { identity = { ...identity, ...user }; aiTokenCache = null; } let appTokenCache: { token: string; expireAt: number } | null = null; let aiTokenCache: { token: string; userKey: string } | null = null; let requestSeq = 0; async function rawFetch(path: string, init: RequestInit): Promise> { let resp: Response; try { resp = await fetch(`${BASE}${path}`, init); } catch (e: any) { throw new AiError(-1, `AI 服务不可达:${e?.message ?? e}`); } try { return (await resp.json()) as AiResp; } catch { throw new AiError(resp.status, `AI 服务响应异常(HTTP ${resp.status})`); } } async function getAppToken(): Promise { if (appTokenCache && appTokenCache.expireAt > Date.now() + 60_000) return appTokenCache.token; const body = await rawFetch<{ access_token: string; expires_in: number }>('/open/v1/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET }) }); if (body.code !== 0) throw new AiError(body.code, body.message || '应用令牌获取失败', body.data); appTokenCache = { token: body.data.access_token, expireAt: Date.now() + (body.data.expires_in ?? 7200) * 1000 }; return appTokenCache.token; } async function getAiToken(): Promise { const userKey = identity.platform_user_id || identity.align_key; if (aiTokenCache && aiTokenCache.userKey === userKey) return aiTokenCache.token; const cached = sessionStorage.getItem(`ai_token:${userKey}`); if (cached) { aiTokenCache = { token: cached, userKey }; return cached; } const appToken = await getAppToken(); const body = await rawFetch<{ token: string }>('/open/v1/auth/exchange', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${appToken}` }, body: JSON.stringify({ platform: 'moc', platform_user_id: identity.platform_user_id, align_key: identity.align_key, display_name: identity.display_name }) }); if (body.code !== 0) throw new AiError(body.code, body.message || 'ai_token 签发失败', body.data); aiTokenCache = { token: body.data.token, userKey }; sessionStorage.setItem(`ai_token:${userKey}`, body.data.token); return body.data.token; } /** ai_token 失效(40101)时清缓存并自动重新 exchange 重试一次 */ async function aiFetch(path: string, payload?: unknown, method: 'GET' | 'POST' = 'POST', retried = false): Promise { const token = await getAiToken(); const body = await rawFetch(path, { method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: method === 'GET' ? undefined : JSON.stringify(payload ?? {}) }); if (body.code === 0) return body.data; if (body.code === 40101 && !retried) { aiTokenCache = null; sessionStorage.removeItem(`ai_token:${identity.platform_user_id || identity.align_key}`); return aiFetch(path, payload, method, true); } throw new AiError(body.code, body.message || 'AI 分析失败', body.data); } function nextRequestId(prefix: string): string { requestSeq += 1; return `moc-${Date.now()}-${requestSeq}`; } /* ---------- 类型与默认载荷 ---------- */ export interface CompareRow { item: string; before_text: string; after_text: string; } export interface MatrixPayload { size: string; l_range?: [number, number]; s_range?: [number, number]; rr?: { min: number; level: string; name: string }[]; default_level?: string; } export interface LevelDim { dim: string; name: string; max: number; } /** MOC 官方风险矩阵(对齐 service_system_config: risk.matrix.official,5×5 LS) */ export const DEFAULT_MATRIX: MatrixPayload = { size: '5x5', l_range: [1, 5], s_range: [1, 5], rr: [ { min: 15, level: 'MAJOR', name: '重大' }, { min: 10, level: 'HIGH', name: '较大' }, { min: 5, level: 'MID', name: '一般' } ], default_level: 'LOW' }; /** MOC 等级评分维度(对齐 change.level.dims,6 维固化) */ export const DEFAULT_LEVEL_DIMS: LevelDim[] = [ { dim: 'EQUIPMENT', name: '设备影响', max: 4 }, { dim: 'PROCESS', name: '工艺影响', max: 4 }, { dim: 'SAFETY', name: '安全风险', max: 4 }, { dim: 'ENVIRONMENT', name: '环境影响', max: 4 }, { dim: 'QUALITY', name: '质量影响', max: 4 }, { dim: 'SCHEDULE', name: '进度与成本', max: 4 } ]; /* ---------- 变更智能分析(同步) ---------- */ export interface RecognizeResult { rows: CompareRow[]; is_change: boolean; same_kind_replace: boolean; materials_suggest: string[]; } export function aiRecognize(content: string, deviceTags: string[] = []) { return aiFetch('/open/v1/moc/change/recognize', { request_id: nextRequestId('rec'), content, device_tags: deviceTags }); } export interface ApplicationResult { name: string; main_changes: string[]; related_changes: string[]; purposes: string[]; effect: string; change_type: string; level_suggestion: string | null; duration_suggestion: string | null; materials_text: string; update_docs: string[]; disciplines: string[]; signers: Record | null; risk_tools: string[]; department_opinions?: any; } export const aiApplication = (content: string, rows: CompareRow[]) => aiFetch('/open/v1/moc/change/application', { request_id: nextRequestId('app'), content, rows }); export interface PreRiskResult { severity: string; probability: string; protection: string; } // 风险预分析 export const aiRiskPreAnalysis = (content: string, rows: CompareRow[]) => aiFetch('/open/v1/moc/change/risk-pre-analysis', { request_id: nextRequestId('pre'), content, rows }); // export function aiRiskPreAnalysis (params: { content: string, rows: CompareRow[] }) { // return request({ // url: '/open/v1/moc/change/risk-pre-analysis', // method: 'post', // data: params // }) // } export interface TypeJudgeResult { type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[]; } export const aiTypeJudge = (content: string, rows: CompareRow[]) => aiFetch('/open/v1/moc/change/type-judge', { request_id: nextRequestId('type'), content, rows }); export interface LevelScoreResult { dims: { dim: string; name: string; score: number | null; basis: string }[]; total_score: number; analysis_text: string; } export const aiLevelScore = (content: string, rows: CompareRow[], levelDims: LevelDim[] = DEFAULT_LEVEL_DIMS) => aiFetch('/open/v1/moc/change/level-score', { request_id: nextRequestId('level'), content, rows, level_dims: levelDims }); export interface RiskCheckItem { no?: number; group: string; q: string; ans: string; risk?: string; measure?: string; } export const aiRiskCheck = (content: string, rows: CompareRow[], checkTemplate?: object) => aiFetch<{ items: RiskCheckItem[] }>('/open/v1/moc/change/risk-check', { request_id: nextRequestId('check'), content, rows, ...(checkTemplate ? { check_template: checkTemplate } : {}) }); export interface RiskRecord { item: string; scene: string; l: number | null; s: number | null; existing: string; suggest: string; residual_l: number | null; residual_s: number | null; } export const aiRiskOrganize = (content: string, rows: CompareRow[], toolResults?: object) => aiFetch<{ records: RiskRecord[] }>('/open/v1/moc/risk/organize', { request_id: nextRequestId('org'), content, rows, tool_results: toolResults ?? {}, matrix_payload: DEFAULT_MATRIX }); /* ---------- 风险分析插件(同步,仅出 L/S) ---------- */ export interface HazopRow { cause: string; consequence: string; l: number | null; s: number | null; safeguards: string; suggestions: string; } export const aiHazopDeviation = (payload: { node_name: string; deviation: string; object_name?: string; change_context?: object; }) => aiFetch<{ rows: HazopRow[] }>('/open/v1/moc/hazop/deviation-analyze', { request_id: nextRequestId('hazop'), matrix_payload: DEFAULT_MATRIX, ...payload }); export interface JsaStep { step: string; desc: string; rows: { hazard: string; consequence: string; l: number | null; s: number | null; safeguards: string; suggestions: string }[]; } export const aiJsaAnalyze = (payload: { job_info: { job_name: string; job_type?: string; job_location?: string }; steps?: any[]; change_context?: object; mode?: 'supplement' | 'full'; }) => aiFetch<{ steps: JsaStep[] }>('/open/v1/moc/jsa/analyze', { request_id: nextRequestId('jsa'), mode: 'supplement', matrix_payload: DEFAULT_MATRIX, ...payload }); export interface SclGroup { g: string; rows: { item: string; std: string; consequence: string; l: number | null; s: number | null; controls: string; suggestions: string }[]; } export const aiSclAnalyze = (payload: { device: { name: string; material_info?: string; run_params?: string }; category: string; scl_template?: object; change_context?: object; }) => aiFetch<{ groups: SclGroup[] }>('/open/v1/moc/scl/analyze', { request_id: nextRequestId('scl'), matrix_payload: DEFAULT_MATRIX, ...payload }); /* ---------- 辅助类 ---------- */ export const aiTraining = (content: string, rows: CompareRow[], riskRecords?: any[]) => aiFetch<{ training_text: string; suggest_posts: string[] }>('/open/v1/moc/change/training', { request_id: nextRequestId('train'), content, rows, risk_records: riskRecords ?? [] }); export const aiPssr = (payload: { content: string; change_type?: string; risk_records?: any[]; action_items?: any[] }) => aiFetch<{ groups: { g: string; items: { content: string; phase: string }[] }[] }>('/open/v1/moc/change/pssr', { request_id: nextRequestId('pssr'), ...payload }); export const aiAcceptance = (payload: { purpose?: string; effect?: string }) => aiFetch<{ rows: { item: string; basis: string; standard: string; method: string; owner_suggest: string }[] }>( '/open/v1/moc/change/acceptance', { request_id: nextRequestId('acc'), ...payload } ); export const aiDisposalDraft = (payload: { original_summary: string; disposal_type: string; long_term_scene?: string }) => aiFetch<{ reason: string; risk: string; long_term_risk_points: string[] }>('/open/v1/moc/disposal/draft', { request_id: nextRequestId('disp'), ...payload }); export const aiGlossary = () => aiFetch<{ terms: { word: string; explanation: string }[] }>('/open/v1/moc/glossary', undefined, 'GET'); /* ---------- 异步任务 ---------- */ export interface OpenTask { task_id: string; task_type?: string; status: 'pending' | 'processing' | 'completed' | 'failed'; progress: number; current_step?: string; steps?: { step: string; status: string }[]; error_msg?: string; result?: any; } export const aiApprovalAssist = (payload: { snapshot: object; change_id_ref?: string; submit_version?: number; inflight_changes?: any[]; manual_filled?: string[]; }) => aiFetch<{ task_id: string; status: string; estimated_seconds: number }>('/open/v1/moc/approval/assist', { request_id: nextRequestId('assist'), matrix_payload: DEFAULT_MATRIX, level_dims: DEFAULT_LEVEL_DIMS, ...payload }); export const aiKnowledgeIngest = (doc: { name: string; file_url?: string; file_base64?: string; version?: string; category?: string; hash?: string; }) => aiFetch<{ task_id: string; status: string; estimated_seconds: number }>('/open/v1/moc/knowledge/ingest', { request_id: nextRequestId('ingest'), doc }); export const getTask = (taskId: string) => aiFetch(`/open/v1/tasks/${taskId}`, undefined, 'GET'); export const getTaskResult = (taskId: string) => aiFetch(`/open/v1/tasks/${taskId}/result`, undefined, 'GET'); /** 轮询任务直至完成/失败 */ export async function pollTask( taskId: string, opts: { intervalMs?: number; timeoutMs?: number; onProgress?: (t: OpenTask) => void } = {} ): Promise { const interval = opts.intervalMs ?? 3000; const deadline = Date.now() + (opts.timeoutMs ?? 300_000); for (;;) { // eslint-disable-next-line no-await-in-loop const t = await getTask(taskId); opts.onProgress?.(t); if (t.status === 'completed') { // eslint-disable-next-line no-await-in-loop const r = await getTaskResult(taskId); return r?.result ?? r; } if (t.status === 'failed') throw new AiError(50202, t.error_msg || 'AI 任务执行失败'); if (Date.now() > deadline) throw new AiError(50202, 'AI 任务等待超时,请稍后查看'); // eslint-disable-next-line no-await-in-loop await new Promise(r => setTimeout(r, interval)); } } /** 健康检查(联调验证用) */ export const aiHealth = () => aiFetch<{ status: string }>('/open/v1/health', undefined, 'GET');