Auto Deployer commited on
Commit
a273844
·
1 Parent(s): e030f23

Add enterprise agents: log/code/reg endpoints

Browse files
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .DS_Store
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
package-lock.json CHANGED
@@ -13,6 +13,7 @@
13
  "better-sqlite3": "^12.9.0",
14
  "cheerio": "^1.2.0",
15
  "express": "^5.2.1",
 
16
  "node-cron": "^4.2.1",
17
  "openai": "^6.34.0",
18
  "uuid": "^14.0.0"
@@ -279,6 +280,12 @@
279
  "dev": true,
280
  "license": "MIT"
281
  },
 
 
 
 
 
 
282
  "node_modules/asynckit": {
283
  "version": "0.4.0",
284
  "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1400,6 +1407,18 @@
1400
  "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1401
  "license": "MIT"
1402
  },
 
 
 
 
 
 
 
 
 
 
 
 
1403
  "node_modules/make-error": {
1404
  "version": "1.3.6",
1405
  "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
 
13
  "better-sqlite3": "^12.9.0",
14
  "cheerio": "^1.2.0",
15
  "express": "^5.2.1",
16
+ "js-yaml": "^4.1.1",
17
  "node-cron": "^4.2.1",
18
  "openai": "^6.34.0",
19
  "uuid": "^14.0.0"
 
280
  "dev": true,
281
  "license": "MIT"
282
  },
283
+ "node_modules/argparse": {
284
+ "version": "2.0.1",
285
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
286
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
287
+ "license": "Python-2.0"
288
+ },
289
  "node_modules/asynckit": {
290
  "version": "0.4.0",
291
  "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
 
1407
  "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1408
  "license": "MIT"
1409
  },
1410
+ "node_modules/js-yaml": {
1411
+ "version": "4.1.1",
1412
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
1413
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
1414
+ "license": "MIT",
1415
+ "dependencies": {
1416
+ "argparse": "^2.0.1"
1417
+ },
1418
+ "bin": {
1419
+ "js-yaml": "bin/js-yaml.js"
1420
+ }
1421
+ },
1422
  "node_modules/make-error": {
1423
  "version": "1.3.6",
1424
  "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
package.json CHANGED
@@ -17,6 +17,7 @@
17
  "better-sqlite3": "^12.9.0",
18
  "cheerio": "^1.2.0",
19
  "express": "^5.2.1",
 
20
  "node-cron": "^4.2.1",
21
  "openai": "^6.34.0",
22
  "uuid": "^14.0.0"
 
17
  "better-sqlite3": "^12.9.0",
18
  "cheerio": "^1.2.0",
19
  "express": "^5.2.1",
20
+ "js-yaml": "^4.1.1",
21
  "node-cron": "^4.2.1",
22
  "openai": "^6.34.0",
23
  "uuid": "^14.0.0"
src/crawler/api.ts CHANGED
@@ -250,7 +250,7 @@ router.get('/updates', (req: Request<{}, {}, {}, { app_name?: string, business_l
250
 
251
  let topicFilters: string[] = [];
252
  if (topics) {
253
- topicFilters = topics.split(',').map(t => t.trim());
254
  }
255
 
256
  const updatesMap = {
 
250
 
251
  let topicFilters: string[] = [];
252
  if (topics) {
253
+ topicFilters = topics.split(',').map((t: string) => t.trim());
254
  }
255
 
256
  const updatesMap = {
src/crawler/extractor.ts CHANGED
@@ -30,7 +30,7 @@ export function extractAndNormalize(snapshot: RawSnapshot): NormalizedDocument |
30
  // 2. Full-width to half-width (simplified)
31
  let normalizedText = rawText
32
  .replace(/\s+/g, ' ')
33
- .replace(/[\uFF01-\uFF5E]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 0xFEE0))
34
  .trim();
35
 
36
  const normalizedHash = crypto.createHash('sha256').update(normalizedText).digest('hex');
 
30
  // 2. Full-width to half-width (simplified)
31
  let normalizedText = rawText
32
  .replace(/\s+/g, ' ')
33
+ .replace(/[\uFF01-\uFF5E]/g, (char: string) => String.fromCharCode(char.charCodeAt(0) - 0xFEE0))
34
  .trim();
35
 
36
  const normalizedHash = crypto.createHash('sha256').update(normalizedText).digest('hex');
src/index.ts CHANGED
@@ -7,7 +7,10 @@ import {
7
  GapAnalysis,
8
  ComplianceCheck,
9
  LegalReviewPack,
10
- MissingMaterialItem
 
 
 
11
  } from './models';
12
  import crawlerRouter from './crawler/api';
13
  import { initDB } from './crawler/db';
@@ -152,6 +155,320 @@ app.post('/api/agents/legal-pack', async (req: Request, res: Response, next: Nex
152
  }
153
  });
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  // 挂载全局错误处理中间件
156
  app.use(errorHandler);
157
 
 
7
  GapAnalysis,
8
  ComplianceCheck,
9
  LegalReviewPack,
10
+ MissingMaterialItem,
11
+ LogAnalysisResult,
12
+ CodeSecurityResult,
13
+ RegulatoryPenaltySummary
14
  } from './models';
15
  import crawlerRouter from './crawler/api';
16
  import { initDB } from './crawler/db';
 
155
  }
156
  });
157
 
158
+ function normString(value: unknown): string {
159
+ if (value === null || value === undefined) return '';
160
+ if (Array.isArray(value)) return value.map(v => String(v)).join('\n').trim();
161
+ if (typeof value !== 'string') return String(value).trim();
162
+ return value.trim();
163
+ }
164
+
165
+ function maskSecrets(text: string): string {
166
+ let s = text;
167
+ s = s.replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-***');
168
+ s = s.replace(/(?<![A-Za-z0-9])[A-Fa-f0-9]{32,}(?![A-Za-z0-9])/g, '***');
169
+ s = s.replace(/(?<![A-Za-z0-9])[A-Za-z0-9_\\-]{24,}(?![A-Za-z0-9])/g, '***');
170
+ return s;
171
+ }
172
+
173
+ function parseLogPatterns(logText: string) {
174
+ const lines = logText.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
175
+ const map = new Map<string, { count: number; first?: string; last?: string; samples: string[]; level: 'high' | 'medium' | 'low' }>();
176
+
177
+ const timeRe = /(\d{2}:\d{2}:\d{2}|\d{2}:\d{2})/;
178
+ for (const line of lines) {
179
+ const timeMatch = line.match(timeRe);
180
+ const time = timeMatch ? timeMatch[1] : '';
181
+ const upper = line.toUpperCase();
182
+ const level: 'high' | 'medium' | 'low' =
183
+ upper.includes('ERROR') || upper.includes('EXCEPTION') || upper.includes('FATAL') || upper.includes('TIMEOUT') ? 'high' :
184
+ upper.includes('WARN') ? 'medium' : 'low';
185
+
186
+ const normalized = line
187
+ .replace(/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\s*/g, '')
188
+ .replace(/^\[?\d{2}:\d{2}:\d{2}\]?\s*/g, '')
189
+ .replace(/\b0x[0-9a-fA-F]+\b/g, '0x#')
190
+ .replace(/\b\d+\b/g, '#')
191
+ .replace(/\s+/g, ' ')
192
+ .slice(0, 220);
193
+
194
+ const key = normalized || line.slice(0, 220);
195
+ const existing = map.get(key);
196
+ if (!existing) {
197
+ const record: { count: number; first?: string; last?: string; samples: string[]; level: 'high' | 'medium' | 'low' } = {
198
+ count: 1,
199
+ samples: [maskSecrets(line).slice(0, 320)],
200
+ level
201
+ };
202
+ if (time) {
203
+ record.first = time;
204
+ record.last = time;
205
+ }
206
+ map.set(key, record);
207
+ } else {
208
+ existing.count += 1;
209
+ if (time) {
210
+ if (!existing.first) existing.first = time;
211
+ existing.last = time;
212
+ }
213
+ if (existing.samples.length < 3) existing.samples.push(maskSecrets(line).slice(0, 320));
214
+ if (level === 'high' || (level === 'medium' && existing.level === 'low')) existing.level = level;
215
+ }
216
+ }
217
+
218
+ const patterns = Array.from(map.entries())
219
+ .sort((a, b) => b[1].count - a[1].count)
220
+ .slice(0, 12)
221
+ .map(([pattern, v]) => ({
222
+ pattern,
223
+ count: v.count,
224
+ first_seen: v.first || '',
225
+ last_seen: v.last || '',
226
+ level: v.level,
227
+ sample_logs: v.samples
228
+ }));
229
+
230
+ const timeline = patterns
231
+ .filter(p => p.first_seen)
232
+ .slice(0, 8)
233
+ .map(p => ({ time: p.first_seen, event: `首次出现:${p.pattern}` }));
234
+
235
+ return { patterns, timeline, line_count: lines.length };
236
+ }
237
+
238
+ function parseScanFindings(scanText: string) {
239
+ const s = scanText.trim();
240
+ if (!s) return [];
241
+ let obj: any;
242
+ try {
243
+ obj = JSON.parse(s);
244
+ } catch {
245
+ return [];
246
+ }
247
+
248
+ const findings: any[] = [];
249
+
250
+ const semgrepResults = Array.isArray(obj?.results) ? obj.results : [];
251
+ for (const r of semgrepResults) {
252
+ const file = r?.path || r?.extra?.path || '';
253
+ const line = Number(r?.start?.line || r?.extra?.line || 0);
254
+ const ruleId = r?.check_id || r?.rule_id || r?.extra?.metadata?.id || '';
255
+ const severityRaw = String(r?.extra?.severity || r?.extra?.metadata?.severity || r?.severity || 'medium').toLowerCase();
256
+ const severity =
257
+ severityRaw.includes('critical') ? 'critical' :
258
+ severityRaw.includes('high') || severityRaw.includes('error') ? 'high' :
259
+ severityRaw.includes('low') ? 'low' :
260
+ severityRaw.includes('info') ? 'info' :
261
+ 'medium';
262
+ const message = r?.extra?.message || r?.extra?.metadata?.message || r?.message || '';
263
+ const evidence = r?.extra?.lines || r?.extra?.metavars ? JSON.stringify({ lines: r?.extra?.lines, metavars: r?.extra?.metavars }) : '';
264
+ findings.push({
265
+ tool: 'semgrep',
266
+ type: 'static_analysis',
267
+ severity,
268
+ status: 'confirmed',
269
+ file: String(file),
270
+ line,
271
+ rule_id: String(ruleId),
272
+ description: String(message),
273
+ evidence: maskSecrets(String(evidence || message || ''))
274
+ });
275
+ }
276
+
277
+ const gitleaksResults = Array.isArray(obj) ? obj : Array.isArray(obj?.leaks) ? obj.leaks : [];
278
+ for (const r of gitleaksResults) {
279
+ const ruleId = r?.RuleID || r?.rule_id || '';
280
+ const file = r?.File || r?.file || '';
281
+ const line = Number(r?.StartLine || r?.line || 0);
282
+ const desc = r?.Description || r?.description || r?.Message || r?.message || 'Possible secret';
283
+ const secret = r?.Secret || r?.secret || '';
284
+ const evidence = secret ? maskSecrets(String(secret)) : maskSecrets(JSON.stringify(r).slice(0, 240));
285
+ if (ruleId || file) {
286
+ findings.push({
287
+ tool: 'gitleaks',
288
+ type: 'secret',
289
+ severity: 'high',
290
+ status: 'confirmed',
291
+ file: String(file),
292
+ line,
293
+ rule_id: String(ruleId || 'gitleaks'),
294
+ description: String(desc),
295
+ evidence
296
+ });
297
+ }
298
+ }
299
+
300
+ return findings.slice(0, 80);
301
+ }
302
+
303
+ const AGENT_VERSION = 'v1.0.0';
304
+ const PROMPT_VERSION = 'v1.0.0';
305
+
306
+ app.post('/api/agents/log-analysis/run', async (req: Request, res: Response, next: NextFunction) => {
307
+ try {
308
+ const system_name = normString(req.body?.system_name);
309
+ const environment = normString(req.body?.environment);
310
+ const time_range = normString(req.body?.time_range);
311
+ const log_text = normString(req.body?.log_text || req.body?.log_content || req.body?.logs);
312
+ const known_changes = req.body?.known_changes ?? req.body?.change_notes ?? '';
313
+ const business_impact = normString(req.body?.business_impact);
314
+ const log_format = normString(req.body?.log_format);
315
+ const knowledge_context = normString(req.body?.knowledge_context || req.body?.kb || req.body?.knowledge_base);
316
+
317
+ const missingItems: MissingMaterialItem[] = [];
318
+ if (!system_name) missingItems.push({ material_type: 'system_name', reason: '缺少系统名称', requirement_level: 'required' });
319
+ if (!environment) missingItems.push({ material_type: 'environment', reason: '缺少环境信息', requirement_level: 'required' });
320
+ if (!time_range) missingItems.push({ material_type: 'time_range', reason: '缺少时间范围', requirement_level: 'required' });
321
+ if (!log_text) missingItems.push({ material_type: 'log_text', reason: '缺少日志文本', requirement_level: 'required' });
322
+
323
+ if (missingItems.length > 0) {
324
+ throw new MissingMaterialError(missingItems);
325
+ }
326
+
327
+ const parsed = parseLogPatterns(log_text);
328
+ const payload = {
329
+ system_name,
330
+ environment,
331
+ time_range,
332
+ business_impact,
333
+ log_format,
334
+ parsed,
335
+ known_changes,
336
+ knowledge_context
337
+ };
338
+
339
+ const startLLM = Date.now();
340
+ const response = await callLLM<LogAnalysisResult>(JSON.stringify(payload), 'LogAnalysisResult');
341
+ res.locals.llm_latency_ms = Date.now() - startLLM;
342
+
343
+ res.json({
344
+ ...response,
345
+ schema_version: 'v1.0',
346
+ agent_version: AGENT_VERSION,
347
+ prompt_version: PROMPT_VERSION,
348
+ model_version: process.env.LLM_MODEL_NAME || 'deepseek-chat'
349
+ });
350
+ } catch (error) {
351
+ next(error);
352
+ }
353
+ });
354
+
355
+ app.post('/api/agents/code-security/run', async (req: Request, res: Response, next: NextFunction) => {
356
+ try {
357
+ const repo_name = normString(req.body?.repo_name);
358
+ const language = normString(req.body?.language);
359
+ const branch = normString(req.body?.branch);
360
+ const security_standard = normString(req.body?.security_standard);
361
+ const review_mode = normString(req.body?.review_mode);
362
+ const scan_result_text = normString(req.body?.scan_result_text || req.body?.scan_results || req.body?.scan_result);
363
+ const code_text = normString(req.body?.code_text || req.body?.code_snippet || req.body?.code);
364
+ const knowledge_context = normString(req.body?.knowledge_context || req.body?.kb || req.body?.knowledge_base);
365
+
366
+ const missingItems: MissingMaterialItem[] = [];
367
+ if (!repo_name) missingItems.push({ material_type: 'repo_name', reason: '缺少仓库名称', requirement_level: 'required' });
368
+ if (!language) missingItems.push({ material_type: 'language', reason: '缺少代码语言', requirement_level: 'required' });
369
+ if (!security_standard) missingItems.push({ material_type: 'security_standard', reason: '缺少安全标准', requirement_level: 'required' });
370
+ if (!review_mode) missingItems.push({ material_type: 'review_mode', reason: '缺少审查模式', requirement_level: 'required' });
371
+
372
+ if (missingItems.length > 0) {
373
+ throw new MissingMaterialError(missingItems);
374
+ }
375
+
376
+ const findings = parseScanFindings(scan_result_text);
377
+ const hasScan = findings.length > 0;
378
+
379
+ if (!hasScan && !code_text) {
380
+ throw new MissingMaterialError([{ material_type: 'scan_result_text/code_text', reason: '缺少扫描结果或代码内容', requirement_level: 'required' }]);
381
+ }
382
+
383
+ const payload = {
384
+ repo_name,
385
+ language,
386
+ branch,
387
+ security_standard,
388
+ review_mode,
389
+ findings,
390
+ code_text: maskSecrets(code_text).slice(0, 8000),
391
+ knowledge_context,
392
+ has_scan_results: hasScan
393
+ };
394
+
395
+ const startLLM = Date.now();
396
+ const response = await callLLM<CodeSecurityResult>(JSON.stringify(payload), 'CodeSecurityResult');
397
+ res.locals.llm_latency_ms = Date.now() - startLLM;
398
+
399
+ res.json({
400
+ ...response,
401
+ schema_version: 'v1.0',
402
+ agent_version: AGENT_VERSION,
403
+ prompt_version: PROMPT_VERSION,
404
+ model_version: process.env.LLM_MODEL_NAME || 'deepseek-chat'
405
+ });
406
+ } catch (error) {
407
+ next(error);
408
+ }
409
+ });
410
+
411
+ app.post('/api/agents/regulatory-penalty-summary/run', async (req: Request, res: Response, next: NextFunction) => {
412
+ try {
413
+ const topic = normString(req.body?.topic);
414
+ const industry = normString(req.body?.industry);
415
+ const time_range = normString(req.body?.time_range);
416
+ const regulators = req.body?.regulators;
417
+ const keywords = req.body?.keywords;
418
+ const report_type = normString(req.body?.report_type);
419
+ const cases = Array.isArray(req.body?.cases) ? req.body.cases : [];
420
+ const knowledge_context = normString(req.body?.knowledge_context || req.body?.kb || req.body?.knowledge_base);
421
+
422
+ const missingItems: MissingMaterialItem[] = [];
423
+ if (!topic) missingItems.push({ material_type: 'topic', reason: '缺少主题', requirement_level: 'required' });
424
+ if (!industry) missingItems.push({ material_type: 'industry', reason: '缺少行业', requirement_level: 'required' });
425
+ if (!time_range) missingItems.push({ material_type: 'time_range', reason: '缺少时间范围', requirement_level: 'required' });
426
+ if (!regulators || (Array.isArray(regulators) && regulators.length === 0)) {
427
+ missingItems.push({ material_type: 'regulators', reason: '缺少监管机构', requirement_level: 'required' });
428
+ }
429
+ if (!report_type) missingItems.push({ material_type: 'report_type', reason: '缺少报告类型', requirement_level: 'required' });
430
+
431
+ if (missingItems.length > 0) {
432
+ throw new MissingMaterialError(missingItems);
433
+ }
434
+
435
+ const normalizedCases = cases
436
+ .map((c: any) => ({
437
+ case_name: normString(c?.case_name || c?.title || ''),
438
+ regulator: normString(c?.regulator || ''),
439
+ date: normString(c?.date || c?.publish_date || ''),
440
+ source_url: normString(c?.source_url || c?.url || ''),
441
+ excerpt: normString(c?.excerpt || c?.summary || '')
442
+ }))
443
+ .filter((c: any) => c.case_name && c.regulator && c.date && c.source_url && c.excerpt);
444
+
445
+ const payload = {
446
+ topic,
447
+ industry,
448
+ time_range,
449
+ regulators,
450
+ keywords,
451
+ report_type,
452
+ cases: normalizedCases.slice(0, 80),
453
+ knowledge_context
454
+ };
455
+
456
+ const startLLM = Date.now();
457
+ const response = await callLLM<RegulatoryPenaltySummary>(JSON.stringify(payload), 'RegulatoryPenaltySummary');
458
+ res.locals.llm_latency_ms = Date.now() - startLLM;
459
+
460
+ res.json({
461
+ ...response,
462
+ schema_version: 'v1.0',
463
+ agent_version: AGENT_VERSION,
464
+ prompt_version: PROMPT_VERSION,
465
+ model_version: process.env.LLM_MODEL_NAME || 'deepseek-chat'
466
+ });
467
+ } catch (error) {
468
+ next(error);
469
+ }
470
+ });
471
+
472
  // 挂载全局错误处理中间件
473
  app.use(errorHandler);
474
 
src/llm.ts CHANGED
@@ -1,14 +1,23 @@
1
  import OpenAI from 'openai';
2
- import { PeerRegSummary, GapAnalysis, ComplianceCheck, LegalReviewPack } from './models';
3
 
4
- // 初始化 DeepSeek 官方客户端
5
- const openai = new OpenAI({
6
- baseURL: 'https://api.deepseek.com/v1',
7
- apiKey: process.env.DEEPSEEK_API_KEY || 'sk-a05d3ac4dcc440f689521151c718ead8',
8
- });
9
 
10
- // DeepSeek 官方模型名称
11
- const MODEL_NAME = 'deepseek-chat';
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  const PROMPTS: Record<string, string> = {
14
  PeerRegSummary: `你是银行APP隐私合规场景中的“同业/监管变化摘要智能体”。
@@ -91,6 +100,51 @@ const PROMPTS: Record<string, string> = {
91
  - focus_items 只保留最关键的法务关注点,并为其打上 priority 标签 (P0/P1/P2);
92
  - 不得出现最终审批语气。
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  输出语言:中文。
95
  请严格输出JSON格式,不要包裹在 \`\`\`json 中,直接输出 { 开始。`
96
  };
@@ -103,6 +157,7 @@ export async function callLLM<T>(prompt: string, schemaType: string): Promise<T>
103
  }
104
 
105
  try {
 
106
  const response = await openai.chat.completions.create({
107
  model: MODEL_NAME,
108
  messages: [
@@ -120,7 +175,11 @@ export async function callLLM<T>(prompt: string, schemaType: string): Promise<T>
120
 
121
  return JSON.parse(content) as T;
122
  } catch (error) {
123
- console.error(`LLM Call Failed for ${schemaType}:`, error);
 
 
 
 
124
  throw new Error('LLM_INFERENCE_FAILED');
125
  }
126
  }
 
1
  import OpenAI from 'openai';
2
+ import { PeerRegSummary, GapAnalysis, ComplianceCheck, LegalReviewPack, LogAnalysisResult, CodeSecurityResult, RegulatoryPenaltySummary } from './models';
3
 
4
+ const MODEL_NAME = process.env.LLM_MODEL_NAME || 'deepseek-chat';
5
+ const BASE_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1';
 
 
 
6
 
7
+ function getApiKey(): string {
8
+ const key = process.env.DEEPSEEK_API_KEY;
9
+ if (!key) {
10
+ throw new Error('MISSING_LLM_API_KEY');
11
+ }
12
+ return key;
13
+ }
14
+
15
+ function getClient(): OpenAI {
16
+ return new OpenAI({
17
+ baseURL: BASE_URL,
18
+ apiKey: getApiKey(),
19
+ });
20
+ }
21
 
22
  const PROMPTS: Record<string, string> = {
23
  PeerRegSummary: `你是银行APP隐私合规场景中的“同业/监管变化摘要智能体”。
 
100
  - focus_items 只保留最关键的法务关注点,并为其打上 priority 标签 (P0/P1/P2);
101
  - 不得出现最终审批语气。
102
 
103
+ 输出语言:中文。
104
+ 请严格输出JSON格式,不要包裹在 \`\`\`json 中,直接输出 { 开始。`
105
+ ,
106
+
107
+ LogAnalysisResult: `你是国内中小银行生产系统日志分析专家。
108
+
109
+ 你的任务:
110
+ 基于日志解析结果、错误聚类、时间线、已知变更、业务影响与知识库内容,生成日志异常摘要、疑似原因、排查建议和风险提示,并生成可归档 Markdown 报告。
111
+
112
+ 必须遵守:
113
+ 1. 不得直接认定根因,除非证据充分;优先使用“疑似”“可能”“建议进一步核查”。
114
+ 2. 不得建议用户直接重启生产服务、删除文件、回滚版本或修改生产配置;只给出检查/对比/验证类建议。
115
+ 3. 所有判断必须给出 evidence。
116
+ 4. 如果日志样本不足或信息缺失,必须写 insufficient_context=true,并说明缺失点。
117
+ 5. 输出必须为 JSON。
118
+
119
+ 输出语言:中文。
120
+ 请严格输出JSON格式,不要包裹在 \`\`\`json 中,直接输出 { 开始。`,
121
+
122
+ CodeSecurityResult: `你是银行代码安全检测结果解释与修复建议专家。
123
+
124
+ 你的任务:
125
+ 基于扫描工具结果(优先)与代码片段(可选)解释漏洞、给出修复建议与安全边界提示,并生成可归档 Markdown 报告。
126
+
127
+ 必须遵守:
128
+ 1. 以扫描结果为主;没有扫描结果时,不得输出“confirmed”确定性漏洞结论,只能输出“suspicious”并提示补充扫描或进入人工复核。
129
+ 2. 不得输出完整密钥、token、账号等敏感信息;若 evidence 中出现疑似 secret,必须掩码。
130
+ 3. 每条漏洞必须包含 file/line/rule_id/evidence。
131
+ 4. 输出必须为 JSON。
132
+
133
+ 输出语言:中文。
134
+ 请严格输出JSON格式,不要包裹在 \`\`\`json 中,直接输出 { 开始。`,
135
+
136
+ RegulatoryPenaltySummary: `你是国内中小银行监管处罚信息汇总专家。
137
+
138
+ 你的任务:
139
+ 基于白名单来源抓取/检索结果、监管案例材料与法规知识库,归纳监管处罚趋势、高频风险类型、典型案例和整改建议,并生成简报 Markdown。
140
+
141
+ 必须遵守:
142
+ 1. 不得编造监管处罚案例、日期、监管机构、来源链接。
143
+ 2. 每个典型案例必须包含 regulator、date、source_url、excerpt。
144
+ 3. 无来源信息时,必须写 insufficient_context=true,不得输出确定性结论。
145
+ 4. 不得作出最终法律意见,只能输出风险提示与整改建议。
146
+ 5. 输出必须为 JSON。
147
+
148
  输出语言:中文。
149
  请严格输出JSON格式,不要包裹在 \`\`\`json 中,直接输出 { 开始。`
150
  };
 
157
  }
158
 
159
  try {
160
+ const openai = getClient();
161
  const response = await openai.chat.completions.create({
162
  model: MODEL_NAME,
163
  messages: [
 
175
 
176
  return JSON.parse(content) as T;
177
  } catch (error) {
178
+ const msg = error instanceof Error ? error.message : String(error);
179
+ if (msg === 'MISSING_LLM_API_KEY') {
180
+ throw new Error('MISSING_LLM_API_KEY');
181
+ }
182
+ console.error(`LLM Call Failed for ${schemaType}: ${msg}`);
183
  throw new Error('LLM_INFERENCE_FAILED');
184
  }
185
  }
src/middlewares.ts CHANGED
@@ -29,7 +29,7 @@ export const requestLogger = (req: Request, res: Response, next: NextFunction) =
29
 
30
  // 拦截 res.send 记录结束时间
31
  const originalSend = res.send;
32
- res.send = function (body) {
33
  const ended_at = new Date();
34
 
35
  // 提取 case_context
@@ -46,6 +46,9 @@ export const requestLogger = (req: Request, res: Response, next: NextFunction) =
46
  else if (req.path.includes('policy-rewrite')) agent_name = 'AGENT-02';
47
  else if (req.path.includes('compliance-check')) agent_name = 'AGENT-03';
48
  else if (req.path.includes('legal-pack')) agent_name = 'AGENT-04';
 
 
 
49
 
50
  const status = res.statusCode >= 400 ? 'failed' : 'success';
51
  const error_code = res.locals.errorCode || 0;
@@ -105,6 +108,10 @@ export const errorHandler = (err: any, req: Request, res: Response, next: NextFu
105
  statusCode = 500;
106
  errorCode = 5002;
107
  message = '外部上下文缺失';
 
 
 
 
108
  }
109
 
110
  // 记录 errorCode 给 logger 使用
 
29
 
30
  // 拦截 res.send 记录结束时间
31
  const originalSend = res.send;
32
+ res.send = function (body: any) {
33
  const ended_at = new Date();
34
 
35
  // 提取 case_context
 
46
  else if (req.path.includes('policy-rewrite')) agent_name = 'AGENT-02';
47
  else if (req.path.includes('compliance-check')) agent_name = 'AGENT-03';
48
  else if (req.path.includes('legal-pack')) agent_name = 'AGENT-04';
49
+ else if (req.path.includes('log-analysis')) agent_name = 'LOG_ANALYSIS';
50
+ else if (req.path.includes('code-security')) agent_name = 'CODE_SECURITY';
51
+ else if (req.path.includes('regulatory-penalty-summary')) agent_name = 'REG_PENALTY_SUMMARY';
52
 
53
  const status = res.statusCode >= 400 ? 'failed' : 'success';
54
  const error_code = res.locals.errorCode || 0;
 
108
  statusCode = 500;
109
  errorCode = 5002;
110
  message = '外部上下文缺失';
111
+ } else if (err.message === 'MISSING_LLM_API_KEY') {
112
+ statusCode = 500;
113
+ errorCode = 5003;
114
+ message = 'LLM密钥未配置';
115
  }
116
 
117
  // 记录 errorCode 给 logger 使用
src/models/index.ts CHANGED
@@ -168,3 +168,88 @@ export interface NeedMoreMaterialResponse extends BaseResponse {
168
  message: string;
169
  missing_materials: MissingMaterialItem[];
170
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  message: string;
169
  missing_materials: MissingMaterialItem[];
170
  }
171
+
172
+ export interface LogAnalysisResult extends BaseResponse {
173
+ insufficient_context?: boolean;
174
+ summary: string;
175
+ error_patterns: {
176
+ pattern: string;
177
+ count: number;
178
+ first_seen?: string;
179
+ last_seen?: string;
180
+ level: 'high' | 'medium' | 'low';
181
+ sample_logs: string[];
182
+ }[];
183
+ timeline: {
184
+ time: string;
185
+ event: string;
186
+ }[];
187
+ possible_causes: {
188
+ cause: string;
189
+ confidence: 'high' | 'medium' | 'low';
190
+ evidence: string[];
191
+ }[];
192
+ suggested_actions: string[];
193
+ risks: {
194
+ risk: string;
195
+ level: 'high' | 'medium' | 'low';
196
+ suggestion: string;
197
+ }[];
198
+ need_human_review: boolean;
199
+ confidence?: 'high' | 'medium' | 'low';
200
+ evidence?: string[];
201
+ report_markdown: string;
202
+ }
203
+
204
+ export interface CodeSecurityResult extends BaseResponse {
205
+ insufficient_context?: boolean;
206
+ risk_summary: string;
207
+ vulnerabilities: {
208
+ type: string;
209
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
210
+ status: 'confirmed' | 'suspicious' | 'false_positive_candidate';
211
+ tool?: string;
212
+ file: string;
213
+ line: number;
214
+ rule_id: string;
215
+ description: string;
216
+ evidence: string;
217
+ impact: string;
218
+ fix_suggestion: string;
219
+ safe_example?: string;
220
+ }[];
221
+ fix_plan: string[];
222
+ compliance_notes: string[];
223
+ need_security_review: boolean;
224
+ confidence?: 'high' | 'medium' | 'low';
225
+ evidence?: string[];
226
+ report_markdown: string;
227
+ }
228
+
229
+ export interface RegulatoryPenaltySummary extends BaseResponse {
230
+ insufficient_context?: boolean;
231
+ topic: string;
232
+ summary: string;
233
+ risk_categories: {
234
+ category: string;
235
+ frequency: 'high' | 'medium' | 'low';
236
+ severity: 'high' | 'medium' | 'low';
237
+ description: string;
238
+ typical_cases: {
239
+ case_name: string;
240
+ regulator: string;
241
+ date: string;
242
+ source_url: string;
243
+ excerpt: string;
244
+ }[];
245
+ suggestions: string[];
246
+ }[];
247
+ top_risks: string[];
248
+ action_suggestions: string[];
249
+ references: {
250
+ source: string;
251
+ url: string;
252
+ details: string;
253
+ }[];
254
+ briefing_markdown: string;
255
+ }
src/schemas/CodeSecurityResult.schema.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "CodeSecurityResult",
4
+ "type": "object",
5
+ "required": [
6
+ "risk_summary",
7
+ "vulnerabilities",
8
+ "fix_plan",
9
+ "compliance_notes",
10
+ "need_security_review",
11
+ "report_markdown"
12
+ ],
13
+ "properties": {
14
+ "insufficient_context": { "type": "boolean" },
15
+ "risk_summary": { "type": "string" },
16
+ "vulnerabilities": {
17
+ "type": "array",
18
+ "items": {
19
+ "type": "object",
20
+ "required": ["type", "severity", "status", "file", "line", "rule_id", "description", "evidence", "impact", "fix_suggestion"],
21
+ "properties": {
22
+ "type": { "type": "string" },
23
+ "severity": { "type": "string", "enum": ["critical", "high", "medium", "low", "info"] },
24
+ "status": { "type": "string", "enum": ["confirmed", "suspicious", "false_positive_candidate"] },
25
+ "tool": { "type": "string" },
26
+ "file": { "type": "string" },
27
+ "line": { "type": "number" },
28
+ "rule_id": { "type": "string" },
29
+ "description": { "type": "string" },
30
+ "evidence": { "type": "string" },
31
+ "impact": { "type": "string" },
32
+ "fix_suggestion": { "type": "string" },
33
+ "safe_example": { "type": "string" }
34
+ }
35
+ }
36
+ },
37
+ "fix_plan": { "type": "array", "items": { "type": "string" } },
38
+ "compliance_notes": { "type": "array", "items": { "type": "string" } },
39
+ "need_security_review": { "type": "boolean" },
40
+ "confidence": { "type": "string", "enum": ["high", "medium", "low"] },
41
+ "evidence": { "type": "array", "items": { "type": "string" } },
42
+ "report_markdown": { "type": "string" }
43
+ }
44
+ }
45
+
src/schemas/LogAnalysisResult.schema.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "LogAnalysisResult",
4
+ "type": "object",
5
+ "required": [
6
+ "summary",
7
+ "error_patterns",
8
+ "timeline",
9
+ "possible_causes",
10
+ "suggested_actions",
11
+ "risks",
12
+ "need_human_review",
13
+ "report_markdown"
14
+ ],
15
+ "properties": {
16
+ "insufficient_context": { "type": "boolean" },
17
+ "summary": { "type": "string" },
18
+ "error_patterns": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "required": ["pattern", "count", "level", "sample_logs"],
23
+ "properties": {
24
+ "pattern": { "type": "string" },
25
+ "count": { "type": "number" },
26
+ "first_seen": { "type": "string" },
27
+ "last_seen": { "type": "string" },
28
+ "level": { "type": "string", "enum": ["high", "medium", "low"] },
29
+ "sample_logs": { "type": "array", "items": { "type": "string" } }
30
+ }
31
+ }
32
+ },
33
+ "timeline": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "object",
37
+ "required": ["time", "event"],
38
+ "properties": {
39
+ "time": { "type": "string" },
40
+ "event": { "type": "string" }
41
+ }
42
+ }
43
+ },
44
+ "possible_causes": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "object",
48
+ "required": ["cause", "confidence", "evidence"],
49
+ "properties": {
50
+ "cause": { "type": "string" },
51
+ "confidence": { "type": "string", "enum": ["high", "medium", "low"] },
52
+ "evidence": { "type": "array", "items": { "type": "string" } }
53
+ }
54
+ }
55
+ },
56
+ "suggested_actions": { "type": "array", "items": { "type": "string" } },
57
+ "risks": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "object",
61
+ "required": ["risk", "level", "suggestion"],
62
+ "properties": {
63
+ "risk": { "type": "string" },
64
+ "level": { "type": "string", "enum": ["high", "medium", "low"] },
65
+ "suggestion": { "type": "string" }
66
+ }
67
+ }
68
+ },
69
+ "need_human_review": { "type": "boolean" },
70
+ "confidence": { "type": "string", "enum": ["high", "medium", "low"] },
71
+ "evidence": { "type": "array", "items": { "type": "string" } },
72
+ "report_markdown": { "type": "string" }
73
+ }
74
+ }
75
+
src/schemas/RegulatoryPenaltySummary.schema.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "RegulatoryPenaltySummary",
4
+ "type": "object",
5
+ "required": [
6
+ "topic",
7
+ "summary",
8
+ "risk_categories",
9
+ "top_risks",
10
+ "action_suggestions",
11
+ "references",
12
+ "briefing_markdown"
13
+ ],
14
+ "properties": {
15
+ "insufficient_context": { "type": "boolean" },
16
+ "topic": { "type": "string" },
17
+ "summary": { "type": "string" },
18
+ "risk_categories": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "required": ["category", "frequency", "severity", "description", "typical_cases", "suggestions"],
23
+ "properties": {
24
+ "category": { "type": "string" },
25
+ "frequency": { "type": "string", "enum": ["high", "medium", "low"] },
26
+ "severity": { "type": "string", "enum": ["high", "medium", "low"] },
27
+ "description": { "type": "string" },
28
+ "typical_cases": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "object",
32
+ "required": ["case_name", "regulator", "date", "source_url", "excerpt"],
33
+ "properties": {
34
+ "case_name": { "type": "string" },
35
+ "regulator": { "type": "string" },
36
+ "date": { "type": "string" },
37
+ "source_url": { "type": "string" },
38
+ "excerpt": { "type": "string" }
39
+ }
40
+ }
41
+ },
42
+ "suggestions": { "type": "array", "items": { "type": "string" } }
43
+ }
44
+ }
45
+ },
46
+ "top_risks": { "type": "array", "items": { "type": "string" } },
47
+ "action_suggestions": { "type": "array", "items": { "type": "string" } },
48
+ "references": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "object",
52
+ "required": ["source", "url", "details"],
53
+ "properties": {
54
+ "source": { "type": "string" },
55
+ "url": { "type": "string" },
56
+ "details": { "type": "string" }
57
+ }
58
+ }
59
+ },
60
+ "briefing_markdown": { "type": "string" }
61
+ }
62
+ }
63
+
tsconfig.json CHANGED
@@ -9,7 +9,7 @@
9
  // See also https://aka.ms/tsconfig/module
10
  "module": "nodenext",
11
  "target": "esnext",
12
- "types": [],
13
  // For nodejs:
14
  // "lib": ["esnext"],
15
  // "types": ["node"],
 
9
  // See also https://aka.ms/tsconfig/module
10
  "module": "nodenext",
11
  "target": "esnext",
12
+ "types": ["node"],
13
  // For nodejs:
14
  // "lib": ["esnext"],
15
  // "types": ["node"],