初步完成ai插件对接

This commit is contained in:
2026-08-26 22:10:04 +08:00
parent 26192167f3
commit f5176ada20
13 changed files with 4367 additions and 3540 deletions
+453
View File
@@ -0,0 +1,453 @@
/**
* 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 来源)
*/
/** 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: string;
after: string;
hl?: 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 const aiRecognize = (content: string, deviceTags: string[] = []) =>
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 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');