feat(perception): 智能感知模块 - event feed, SSE impact analysis, tab registration
This commit is contained in:
128
frontend/src/api/perception.ts
Normal file
128
frontend/src/api/perception.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
const PERCEPTION_API_BASE = '/api/v1';
|
||||
|
||||
export type ImpactLevel = 'high' | 'medium' | 'low';
|
||||
export type EventStatus = 'enacted' | 'draft' | 'consultation';
|
||||
export type EventSource = 'MIIT' | 'UN-ECE' | 'ISO' | '国标委' | 'EUR-Lex' | 'IATF';
|
||||
|
||||
export interface RegulationEvent {
|
||||
id: string;
|
||||
source: EventSource;
|
||||
source_label: string;
|
||||
standard_code: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
impact_level: ImpactLevel;
|
||||
published_at: string;
|
||||
effective_at: string | null;
|
||||
category: string;
|
||||
tags: string[];
|
||||
source_url: string;
|
||||
status: EventStatus;
|
||||
}
|
||||
|
||||
export interface PerceptionStats {
|
||||
total: number;
|
||||
high_impact: number;
|
||||
medium_impact: number;
|
||||
low_impact: number;
|
||||
recent_90d: number;
|
||||
}
|
||||
|
||||
export interface EventListResponse {
|
||||
events: RegulationEvent[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AffectedDoc {
|
||||
doc_id: string;
|
||||
doc_name: string;
|
||||
score: number;
|
||||
snippet: string;
|
||||
clause: string;
|
||||
}
|
||||
|
||||
export interface AnalysisSSEMessage {
|
||||
type: 'sources' | 'content' | 'done' | 'error';
|
||||
docs?: AffectedDoc[];
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export async function getPerceptionStats(): Promise<PerceptionStats> {
|
||||
const res = await fetch(`${PERCEPTION_API_BASE}/perception/stats`);
|
||||
if (!res.ok) throw new Error(`stats failed: ${res.status}`);
|
||||
return res.json() as Promise<PerceptionStats>;
|
||||
}
|
||||
|
||||
export async function listEvents(params?: {
|
||||
source?: string;
|
||||
impact_level?: string;
|
||||
limit?: number;
|
||||
}): Promise<EventListResponse> {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.source) query.set('source', params.source);
|
||||
if (params?.impact_level) query.set('impact_level', params.impact_level);
|
||||
if (params?.limit) query.set('limit', String(params.limit));
|
||||
const res = await fetch(`${PERCEPTION_API_BASE}/perception/events?${query.toString()}`);
|
||||
if (!res.ok) throw new Error(`list events failed: ${res.status}`);
|
||||
return res.json() as Promise<EventListResponse>;
|
||||
}
|
||||
|
||||
export async function analyzeEvent(
|
||||
eventId: string,
|
||||
onMessage: (msg: AnalysisSSEMessage) => void,
|
||||
onComplete?: () => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const res = await fetch(`${PERCEPTION_API_BASE}/perception/events/${eventId}/analyze`, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
signal,
|
||||
});
|
||||
if (!res.ok || !res.body) throw new Error(`analyze failed: ${res.status}`);
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const parts = buffer.split('\n\n');
|
||||
buffer = parts.pop() ?? '';
|
||||
for (const block of parts) {
|
||||
if (!block.trim()) continue;
|
||||
let eventName = 'message';
|
||||
const dataLines: string[] = [];
|
||||
for (const line of block.split('\n')) {
|
||||
if (line.startsWith('event:')) eventName = line.slice(6).trim();
|
||||
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
|
||||
}
|
||||
const payload = dataLines.join('\n');
|
||||
if (!payload) continue;
|
||||
|
||||
if (eventName === 'sources') {
|
||||
try {
|
||||
const docs = JSON.parse(payload) as AffectedDoc[];
|
||||
onMessage({ type: 'sources', docs });
|
||||
} catch { /* ignore */ }
|
||||
} else if (eventName === 'content') {
|
||||
onMessage({ type: 'content', text: payload });
|
||||
} else if (eventName === 'done') {
|
||||
onMessage({ type: 'done' });
|
||||
} else if (eventName === 'error') {
|
||||
onMessage({ type: 'error', text: payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buffer.trim()) {
|
||||
// flush remaining
|
||||
}
|
||||
onComplete?.();
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return;
|
||||
onMessage({ type: 'error', text: err instanceof Error ? err.message : String(err) });
|
||||
onComplete?.();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user