Files
moc/src/service/api/ai.ts
T
2026-09-01 16:12:33 +08:00

461 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ai-server OpenApiMOC 插件)接入层
* 契约:《变更管理系统插件接口说明文档 V1.0》(ai-server Module/OpenApi
* - base 走 vite 代理 /proxy-ai -> http://127.0.0.1:12333(见 vite.config.ts
* - 鉴权:app 凭据换应用令牌(缓存 2h)→ exchange 换会员 JWTai_token,会话级缓存)
* - 本地联调形态:前端直连 exchange(moc-server 的登录双令牌下发链路就绪后,
* 改为登录时由 moc-server 下发 ai_token,本层仅需替换 getAiToken 来源)
*/
import { request } from '../request/ai';
/** ai-server 统一响应 */
interface AiResp<T = any> {
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<AiUserIdentity>) {
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<T>(path: string, init: RequestInit): Promise<AiResp<T>> {
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<T>;
} catch {
throw new AiError(resp.status, `AI 服务响应异常(HTTP ${resp.status}`);
}
}
async function getAppToken(): Promise<string> {
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<string> {
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<T = any>(path: string, payload?: unknown, method: 'GET' | 'POST' = 'POST', retried = false): Promise<T> {
const token = await getAiToken();
const body = await rawFetch<T>(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<T>(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.official5×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.dims6 维固化) */
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<RecognizeResult>('/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<string, string> | null;
risk_tools: string[];
department_opinions?: any;
}
export const aiApplication = (content: string, rows: CompareRow[]) =>
aiFetch<ApplicationResult>('/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<PreRiskResult>('/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<TypeJudgeResult>('/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<LevelScoreResult>('/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<OpenTask>(`/open/v1/tasks/${taskId}`, undefined, 'GET');
export const getTaskResult = (taskId: string) => aiFetch<OpenTask>(`/open/v1/tasks/${taskId}/result`, undefined, 'GET');
/** 轮询任务直至完成/失败 */
export async function pollTask(
taskId: string,
opts: { intervalMs?: number; timeoutMs?: number; onProgress?: (t: OpenTask) => void } = {}
): Promise<any> {
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');