初步完成ai插件对接
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* 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 来源)
|
||||
*/
|
||||
|
||||
/** 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.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 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');
|
||||
Reference in New Issue
Block a user