import express, { Request, Response, NextFunction } from 'express'; import { requestLogger, errorHandler, AppError, MissingMaterialError } from './middlewares'; import { callLLM } from './llm'; import { CaseContext, PeerRegSummary, GapAnalysis, ComplianceCheck, LegalReviewPack, MissingMaterialItem, LogAnalysisResult, CodeSecurityResult, RegulatoryPenaltySummary } from './models'; import crawlerRouter from './crawler/api'; import { initDB } from './crawler/db'; import { initScheduler } from './crawler/scheduler'; const app = express(); app.use(express.json()); // Initialize crawler database initDB(); // Initialize crawler scheduler initScheduler(); // 挂载日志中间件 app.use(requestLogger); const PORT = process.env.PORT || 3000; // 挂载抓取子系统路由 app.use('/api/crawler', crawlerRouter); // API-01 同业/监管变化摘要 app.post('/api/agents/peer-reg-summary', async (req: Request, res: Response, next: NextFunction) => { try { const { peer_updates, regulatory_updates, business_line, app_name } = req.body; const missingItems: MissingMaterialItem[] = []; if (!peer_updates) missingItems.push({ material_type: 'peer_updates', reason: '缺少同业动态', requirement_level: 'required' }); if (!regulatory_updates) missingItems.push({ material_type: 'regulatory_updates', reason: '缺少监管动态', requirement_level: 'required' }); if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } const startLLM = Date.now(); const response = await callLLM(JSON.stringify(req.body), 'PeerRegSummary'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0' }); } catch (error) { next(error); } }); // API-02 协议重构 app.post('/api/agents/policy-rewrite', async (req: Request, res: Response, next: NextFunction) => { try { const { case_context, peer_reg_summary } = req.body; const missingItems: MissingMaterialItem[] = []; if (!case_context || !case_context.materials) { missingItems.push({ material_type: 'case_context.materials', reason: '缺少用例材料上下文', requirement_level: 'required' }); } else { const { current_policy_text, prd_text, permission_items, sdk_items } = case_context.materials; if (!current_policy_text) missingItems.push({ material_type: 'current_policy_text', reason: '缺少当前协议文本', requirement_level: 'required' }); if (!prd_text) missingItems.push({ material_type: 'prd_text', reason: '缺少PRD文本', requirement_level: 'required' }); if (!permission_items) missingItems.push({ material_type: 'permission_items', reason: '缺少权限清单', requirement_level: 'required' }); if (!sdk_items) missingItems.push({ material_type: 'sdk_items', reason: '缺少SDK清单', requirement_level: 'required' }); } if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } if (!peer_reg_summary) { throw new Error('MISSING_CONTEXT'); } const startLLM = Date.now(); const response = await callLLM(JSON.stringify(req.body), 'GapAnalysis'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0' }); } catch (error) { next(error); } }); // API-03 合规校验 app.post('/api/agents/compliance-check', async (req: Request, res: Response, next: NextFunction) => { try { const { case_context, gap_analysis } = req.body; const missingItems: MissingMaterialItem[] = []; if (!case_context || !case_context.materials) { missingItems.push({ material_type: 'case_context.materials', reason: '缺少用例材料上下文', requirement_level: 'required' }); } if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } if (!gap_analysis) { throw new Error('MISSING_CONTEXT'); } const startLLM = Date.now(); const response = await callLLM(JSON.stringify(req.body), 'ComplianceCheck'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0' }); } catch (error) { next(error); } }); // API-04 法务审核包生成 app.post('/api/agents/legal-pack', async (req: Request, res: Response, next: NextFunction) => { try { const { case_context, peer_reg_summary, gap_analysis, compliance_check } = req.body; const missingItems: MissingMaterialItem[] = []; if (!case_context) missingItems.push({ material_type: 'case_context', reason: '缺少用例材料上下文', requirement_level: 'required' }); if (!peer_reg_summary) missingItems.push({ material_type: 'peer_reg_summary', reason: '缺少同业/监管变化摘要', requirement_level: 'required' }); if (!gap_analysis) missingItems.push({ material_type: 'gap_analysis', reason: '缺少协议重构结果', requirement_level: 'required' }); if (!compliance_check) missingItems.push({ material_type: 'compliance_check', reason: '缺少合规校验结果', requirement_level: 'required' }); if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } const startLLM = Date.now(); const response = await callLLM(JSON.stringify(req.body), 'LegalReviewPack'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0' }); } catch (error) { next(error); } }); function normString(value: unknown): string { if (value === null || value === undefined) return ''; if (Array.isArray(value)) return value.map(v => String(v)).join('\n').trim(); if (typeof value !== 'string') return String(value).trim(); return value.trim(); } function maskSecrets(text: string): string { let s = text; s = s.replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-***'); s = s.replace(/(? l.trim()).filter(Boolean); const map = new Map(); const timeRe = /(\d{2}:\d{2}:\d{2}|\d{2}:\d{2})/; for (const line of lines) { const timeMatch = line.match(timeRe); const time = timeMatch ? timeMatch[1] : ''; const upper = line.toUpperCase(); const level: 'high' | 'medium' | 'low' = upper.includes('ERROR') || upper.includes('EXCEPTION') || upper.includes('FATAL') || upper.includes('TIMEOUT') ? 'high' : upper.includes('WARN') ? 'medium' : 'low'; const normalized = line .replace(/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\s*/g, '') .replace(/^\[?\d{2}:\d{2}:\d{2}\]?\s*/g, '') .replace(/\b0x[0-9a-fA-F]+\b/g, '0x#') .replace(/\b\d+\b/g, '#') .replace(/\s+/g, ' ') .slice(0, 220); const key = normalized || line.slice(0, 220); const existing = map.get(key); if (!existing) { const record: { count: number; first?: string; last?: string; samples: string[]; level: 'high' | 'medium' | 'low' } = { count: 1, samples: [maskSecrets(line).slice(0, 320)], level }; if (time) { record.first = time; record.last = time; } map.set(key, record); } else { existing.count += 1; if (time) { if (!existing.first) existing.first = time; existing.last = time; } if (existing.samples.length < 3) existing.samples.push(maskSecrets(line).slice(0, 320)); if (level === 'high' || (level === 'medium' && existing.level === 'low')) existing.level = level; } } const patterns = Array.from(map.entries()) .sort((a, b) => b[1].count - a[1].count) .slice(0, 12) .map(([pattern, v]) => ({ pattern, count: v.count, first_seen: v.first || '', last_seen: v.last || '', level: v.level, sample_logs: v.samples })); const timeline = patterns .filter(p => p.first_seen) .slice(0, 8) .map(p => ({ time: p.first_seen, event: `首次出现:${p.pattern}` })); return { patterns, timeline, line_count: lines.length }; } function parseScanFindings(scanText: string) { const s = scanText.trim(); if (!s) return []; let obj: any; try { obj = JSON.parse(s); } catch { return []; } const findings: any[] = []; const semgrepResults = Array.isArray(obj?.results) ? obj.results : []; for (const r of semgrepResults) { const file = r?.path || r?.extra?.path || ''; const line = Number(r?.start?.line || r?.extra?.line || 0); const ruleId = r?.check_id || r?.rule_id || r?.extra?.metadata?.id || ''; const severityRaw = String(r?.extra?.severity || r?.extra?.metadata?.severity || r?.severity || 'medium').toLowerCase(); const severity = severityRaw.includes('critical') ? 'critical' : severityRaw.includes('high') || severityRaw.includes('error') ? 'high' : severityRaw.includes('low') ? 'low' : severityRaw.includes('info') ? 'info' : 'medium'; const message = r?.extra?.message || r?.extra?.metadata?.message || r?.message || ''; const evidence = r?.extra?.lines || r?.extra?.metavars ? JSON.stringify({ lines: r?.extra?.lines, metavars: r?.extra?.metavars }) : ''; findings.push({ tool: 'semgrep', type: 'static_analysis', severity, status: 'confirmed', file: String(file), line, rule_id: String(ruleId), description: String(message), evidence: maskSecrets(String(evidence || message || '')) }); } const gitleaksResults = Array.isArray(obj) ? obj : Array.isArray(obj?.leaks) ? obj.leaks : []; for (const r of gitleaksResults) { const ruleId = r?.RuleID || r?.rule_id || ''; const file = r?.File || r?.file || ''; const line = Number(r?.StartLine || r?.line || 0); const desc = r?.Description || r?.description || r?.Message || r?.message || 'Possible secret'; const secret = r?.Secret || r?.secret || ''; const evidence = secret ? maskSecrets(String(secret)) : maskSecrets(JSON.stringify(r).slice(0, 240)); if (ruleId || file) { findings.push({ tool: 'gitleaks', type: 'secret', severity: 'high', status: 'confirmed', file: String(file), line, rule_id: String(ruleId || 'gitleaks'), description: String(desc), evidence }); } } return findings.slice(0, 80); } const AGENT_VERSION = 'v1.0.0'; const PROMPT_VERSION = 'v1.0.0'; const handleLogAnalysis = async (req: Request, res: Response, next: NextFunction) => { try { const system_name = normString(req.body?.system_name); const environment = normString(req.body?.environment); const time_range = normString(req.body?.time_range); const log_text = normString(req.body?.log_text || req.body?.log_content || req.body?.logs); const known_changes = req.body?.known_changes ?? req.body?.change_notes ?? ''; const business_impact = normString(req.body?.business_impact); const log_format = normString(req.body?.log_format); const knowledge_context = normString(req.body?.knowledge_context || req.body?.kb || req.body?.knowledge_base); const missingItems: MissingMaterialItem[] = []; if (!system_name) missingItems.push({ material_type: 'system_name', reason: '缺少系统名称', requirement_level: 'required' }); if (!environment) missingItems.push({ material_type: 'environment', reason: '缺少环境信息', requirement_level: 'required' }); if (!time_range) missingItems.push({ material_type: 'time_range', reason: '缺少时间范围', requirement_level: 'required' }); if (!log_text) missingItems.push({ material_type: 'log_text', reason: '缺少日志文本', requirement_level: 'required' }); if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } const parsed = parseLogPatterns(log_text); const payload = { system_name, environment, time_range, business_impact, log_format, parsed, known_changes, knowledge_context }; const startLLM = Date.now(); const response = await callLLM(JSON.stringify(payload), 'LogAnalysisResult'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0', agent_version: AGENT_VERSION, prompt_version: PROMPT_VERSION, model_version: process.env.LLM_MODEL_NAME || 'deepseek-chat' }); } catch (error) { next(error); } }; const handleCodeSecurity = async (req: Request, res: Response, next: NextFunction) => { try { const repo_name = normString(req.body?.repo_name); const language = normString(req.body?.language); const branch = normString(req.body?.branch); const security_standard = normString(req.body?.security_standard); const review_mode = normString(req.body?.review_mode); const scan_result_text = normString(req.body?.scan_result_text || req.body?.scan_results || req.body?.scan_result); const code_text = normString(req.body?.code_text || req.body?.code_snippet || req.body?.code); const knowledge_context = normString(req.body?.knowledge_context || req.body?.kb || req.body?.knowledge_base); const missingItems: MissingMaterialItem[] = []; if (!repo_name) missingItems.push({ material_type: 'repo_name', reason: '缺少仓库名称', requirement_level: 'required' }); if (!language) missingItems.push({ material_type: 'language', reason: '缺少代码语言', requirement_level: 'required' }); if (!security_standard) missingItems.push({ material_type: 'security_standard', reason: '缺少安全标准', requirement_level: 'required' }); if (!review_mode) missingItems.push({ material_type: 'review_mode', reason: '缺少审查模式', requirement_level: 'required' }); if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } const findings = parseScanFindings(scan_result_text); const hasScan = findings.length > 0; if (!hasScan && !code_text) { throw new MissingMaterialError([{ material_type: 'scan_result_text/code_text', reason: '缺少扫描结果或代码内容', requirement_level: 'required' }]); } const payload = { repo_name, language, branch, security_standard, review_mode, findings, code_text: maskSecrets(code_text).slice(0, 8000), knowledge_context, has_scan_results: hasScan }; const startLLM = Date.now(); const response = await callLLM(JSON.stringify(payload), 'CodeSecurityResult'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0', agent_version: AGENT_VERSION, prompt_version: PROMPT_VERSION, model_version: process.env.LLM_MODEL_NAME || 'deepseek-chat' }); } catch (error) { next(error); } }; const handleRegPenaltySummary = async (req: Request, res: Response, next: NextFunction) => { try { const topic = normString(req.body?.topic); const industry = normString(req.body?.industry); const time_range = normString(req.body?.time_range); const regulators = req.body?.regulators; const keywords = req.body?.keywords; const report_type = normString(req.body?.report_type); const cases = Array.isArray(req.body?.cases) ? req.body.cases : []; const knowledge_context = normString(req.body?.knowledge_context || req.body?.kb || req.body?.knowledge_base); const missingItems: MissingMaterialItem[] = []; if (!topic) missingItems.push({ material_type: 'topic', reason: '缺少主题', requirement_level: 'required' }); if (!industry) missingItems.push({ material_type: 'industry', reason: '缺少行业', requirement_level: 'required' }); if (!time_range) missingItems.push({ material_type: 'time_range', reason: '缺少时间范围', requirement_level: 'required' }); if (!regulators || (Array.isArray(regulators) && regulators.length === 0)) { missingItems.push({ material_type: 'regulators', reason: '缺少监管机构', requirement_level: 'required' }); } if (!report_type) missingItems.push({ material_type: 'report_type', reason: '缺少报告类型', requirement_level: 'required' }); if (missingItems.length > 0) { throw new MissingMaterialError(missingItems); } const normalizedCases = cases .map((c: any) => ({ case_name: normString(c?.case_name || c?.title || ''), regulator: normString(c?.regulator || ''), date: normString(c?.date || c?.publish_date || ''), source_url: normString(c?.source_url || c?.url || ''), excerpt: normString(c?.excerpt || c?.summary || '') })) .filter((c: any) => c.case_name && c.regulator && c.date && c.source_url && c.excerpt); const payload = { topic, industry, time_range, regulators, keywords, report_type, cases: normalizedCases.slice(0, 80), knowledge_context }; const startLLM = Date.now(); const response = await callLLM(JSON.stringify(payload), 'RegulatoryPenaltySummary'); res.locals.llm_latency_ms = Date.now() - startLLM; res.json({ ...response, schema_version: 'v1.0', agent_version: AGENT_VERSION, prompt_version: PROMPT_VERSION, model_version: process.env.LLM_MODEL_NAME || 'deepseek-chat' }); } catch (error) { next(error); } }; app.post('/api/agents/log-analysis', handleLogAnalysis); app.post('/api/agents/log-analysis/run', handleLogAnalysis); app.post('/api/agents/code-security', handleCodeSecurity); app.post('/api/agents/code-security/run', handleCodeSecurity); app.post('/api/agents/regulatory-penalty-summary', handleRegPenaltySummary); app.post('/api/agents/regulatory-penalty-summary/run', handleRegPenaltySummary); // 挂载全局错误处理中间件 app.use(errorHandler); if (require.main === module) { app.listen(PORT, () => { console.log(`Agent Services API is running on http://localhost:${PORT}`); }); } export default app;