plugin-sensitive-filter-xr 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,17 @@
1
- import { __decorate } from "tslib";
1
+ import { __decorate, __metadata } from "tslib";
2
2
  import { z as z4 } from 'zod/v4';
3
3
  import { AIMessage, HumanMessage } from '@langchain/core/messages';
4
- import { Injectable } from '@nestjs/common';
5
- import { AgentMiddlewareStrategy, } from '@xpert-ai/plugin-sdk';
6
- import { SensitiveFilterIcon, resolveGeneralPackRules, sensitiveFilterConfigSchema, } from './types.js';
4
+ import { Inject, Injectable } from '@nestjs/common';
5
+ import { CommandBus } from '@nestjs/cqrs';
6
+ import { AgentMiddlewareStrategy, CreateModelClientCommand, WrapWorkflowNodeExecutionCommand, } from '@xpert-ai/plugin-sdk';
7
+ import { SensitiveFilterIcon, llmDecisionSchema, resolveGeneralPackRules, sensitiveFilterConfigSchema, } from './types.js';
7
8
  const SENSITIVE_FILTER_MIDDLEWARE_NAME = 'SensitiveFilterMiddleware';
8
9
  const DEFAULT_INPUT_BLOCK_MESSAGE = '输入内容触发敏感策略,已拦截。';
9
10
  const DEFAULT_OUTPUT_BLOCK_MESSAGE = '输出内容触发敏感策略,已拦截。';
10
11
  const DEFAULT_REWRITE_TEXT = '[已过滤]';
12
+ const CONFIG_PARSE_ERROR = '敏感词过滤配置格式不正确,请检查填写内容。';
13
+ const BUSINESS_RULES_VALIDATION_ERROR = '请至少配置 1 条有效业务规则(pattern/type/action/scope/severity),或启用通用规则包。';
14
+ const LLM_MODE_VALIDATION_ERROR = '请完善 LLM 过滤配置:需填写过滤模型、生效范围、判定提示词、失败处理策略。';
11
15
  function isRecord(value) {
12
16
  return typeof value === 'object' && value !== null;
13
17
  }
@@ -62,9 +66,6 @@ function extractInputText(state, runtime) {
62
66
  }
63
67
  return '';
64
68
  }
65
- function escapeRegExp(value) {
66
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
- }
68
69
  function getSeverityWeight(severity) {
69
70
  return severity === 'high' ? 2 : 1;
70
71
  }
@@ -166,7 +167,7 @@ function rewriteModelRequestInput(request, rewrittenText) {
166
167
  }
167
168
  function normalizeRuleDrafts(input) {
168
169
  const rules = [];
169
- for (const draft of input) {
170
+ for (const [index, draft] of input.entries()) {
170
171
  if (!isRecord(draft)) {
171
172
  continue;
172
173
  }
@@ -181,7 +182,7 @@ function normalizeRuleDrafts(input) {
181
182
  if (!hasAnyValue) {
182
183
  continue;
183
184
  }
184
- if (!id || !pattern || !type || !scope || !severity || !action) {
185
+ if (!pattern || !type || !action || !scope || !severity) {
185
186
  continue;
186
187
  }
187
188
  if (!['keyword', 'regex'].includes(type)) {
@@ -197,7 +198,7 @@ function normalizeRuleDrafts(input) {
197
198
  continue;
198
199
  }
199
200
  rules.push({
200
- id,
201
+ id: id ?? `rule-${index + 1}`,
201
202
  pattern,
202
203
  type,
203
204
  scope,
@@ -208,6 +209,114 @@ function normalizeRuleDrafts(input) {
208
209
  }
209
210
  return rules;
210
211
  }
212
+ function modeIncludesScope(scope, phase) {
213
+ return scope === 'both' || scope === phase;
214
+ }
215
+ function parseLlmDecision(raw, rewriteFallbackText) {
216
+ let payload = raw;
217
+ if (typeof payload === 'string') {
218
+ try {
219
+ payload = JSON.parse(payload);
220
+ }
221
+ catch {
222
+ throw new Error('LLM decision is not valid JSON string');
223
+ }
224
+ }
225
+ if (isRecord(payload) && !('matched' in payload) && 'content' in payload) {
226
+ const content = extractPrimitiveText(payload['content']).trim();
227
+ if (!content) {
228
+ throw new Error('LLM decision content is empty');
229
+ }
230
+ try {
231
+ payload = JSON.parse(content);
232
+ }
233
+ catch {
234
+ throw new Error('LLM decision content is not valid JSON');
235
+ }
236
+ }
237
+ const parsed = llmDecisionSchema.safeParse(payload);
238
+ if (!parsed.success) {
239
+ throw new Error(`Invalid LLM decision: ${z4.prettifyError(parsed.error)}`);
240
+ }
241
+ const decision = parsed.data;
242
+ const reason = toNonEmptyString(decision.reason) ?? undefined;
243
+ const categories = Array.isArray(decision.categories) ? decision.categories.filter(Boolean) : undefined;
244
+ if (!decision.matched) {
245
+ return {
246
+ matched: false,
247
+ reason,
248
+ categories,
249
+ };
250
+ }
251
+ if (decision.action === 'block') {
252
+ return {
253
+ matched: true,
254
+ action: 'block',
255
+ reason,
256
+ categories,
257
+ };
258
+ }
259
+ return {
260
+ matched: true,
261
+ action: 'rewrite',
262
+ replacementText: toNonEmptyString(decision.replacementText) ?? rewriteFallbackText,
263
+ reason,
264
+ categories,
265
+ };
266
+ }
267
+ function resolveRuntimeLlmConfig(config) {
268
+ if (!isRecord(config)) {
269
+ throw new Error(LLM_MODE_VALIDATION_ERROR);
270
+ }
271
+ const model = isRecord(config.model) ? config.model : null;
272
+ const scope = toNonEmptyString(config.scope);
273
+ const systemPrompt = toNonEmptyString(config.systemPrompt);
274
+ const onLlmError = toNonEmptyString(config.onLlmError);
275
+ if (!model || !scope || !systemPrompt || !onLlmError) {
276
+ throw new Error(LLM_MODE_VALIDATION_ERROR);
277
+ }
278
+ const outputMethodRaw = toNonEmptyString(config.outputMethod);
279
+ const outputMethod = ['functionCalling', 'jsonMode', 'jsonSchema'].includes(outputMethodRaw ?? '')
280
+ ? outputMethodRaw
281
+ : 'jsonSchema';
282
+ const errorRewriteText = toNonEmptyString(config.errorRewriteText) ?? undefined;
283
+ if (onLlmError === 'rewrite' && !errorRewriteText) {
284
+ throw new Error('请完善 LLM 过滤配置:当失败处理为改写时,必须填写失败改写文本。');
285
+ }
286
+ const timeoutMs = typeof config.timeoutMs === 'number' && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0
287
+ ? Math.min(Math.floor(config.timeoutMs), 120000)
288
+ : undefined;
289
+ return {
290
+ model,
291
+ scope,
292
+ systemPrompt,
293
+ outputMethod,
294
+ onLlmError,
295
+ errorRewriteText,
296
+ blockMessage: toNonEmptyString(config.blockMessage) ?? undefined,
297
+ rewriteFallbackText: toNonEmptyString(config.rewriteFallbackText) ?? DEFAULT_REWRITE_TEXT,
298
+ timeoutMs,
299
+ };
300
+ }
301
+ function withTimeout(promise, timeoutMs) {
302
+ if (!timeoutMs || timeoutMs <= 0) {
303
+ return promise;
304
+ }
305
+ return new Promise((resolve, reject) => {
306
+ const timer = setTimeout(() => {
307
+ reject(new Error(`LLM filter timeout after ${timeoutMs}ms`));
308
+ }, timeoutMs);
309
+ promise
310
+ .then((result) => {
311
+ clearTimeout(timer);
312
+ resolve(result);
313
+ })
314
+ .catch((error) => {
315
+ clearTimeout(timer);
316
+ reject(error);
317
+ });
318
+ });
319
+ }
211
320
  let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
212
321
  constructor() {
213
322
  this.meta = {
@@ -217,8 +326,8 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
217
326
  zh_Hans: '敏感内容过滤中间件',
218
327
  },
219
328
  description: {
220
- en_US: 'Filter sensitive content before input and after output using business rules and optional local general lexicon.',
221
- zh_Hans: '基于业务规则进行输入/输出过滤,并可选启用本地开源通用词库(中英双语)作为兜底。',
329
+ en_US: 'Filter sensitive content before input and after output using rule mode or LLM prompt mode (mutually exclusive).',
330
+ zh_Hans: '支持规则模式或 LLM 提示词模式(互斥)进行输入/输出敏感内容过滤。',
222
331
  },
223
332
  icon: {
224
333
  type: 'svg',
@@ -227,15 +336,37 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
227
336
  configSchema: {
228
337
  type: 'object',
229
338
  properties: {
339
+ mode: {
340
+ type: 'string',
341
+ title: {
342
+ en_US: 'Filter Mode',
343
+ zh_Hans: '过滤模式',
344
+ },
345
+ description: {
346
+ en_US: 'Choose exactly one mode: Rule or LLM.',
347
+ zh_Hans: '二选一:规则模式或 LLM 模式。',
348
+ },
349
+ enum: ['rule', 'llm'],
350
+ default: 'rule',
351
+ 'x-ui': {
352
+ enumLabels: {
353
+ rule: { en_US: 'Rule Mode', zh_Hans: '规则模式' },
354
+ llm: { en_US: 'LLM Mode', zh_Hans: 'LLM 模式' },
355
+ },
356
+ },
357
+ },
230
358
  rules: {
231
359
  type: 'array',
360
+ 'x-ui': {
361
+ span: 2,
362
+ },
232
363
  title: {
233
364
  en_US: 'Business Rules',
234
365
  zh_Hans: '业务规则',
235
366
  },
236
367
  description: {
237
- en_US: 'Add at least one complete business rule. If you only need baseline protection, enable General Pack below.',
238
- zh_Hans: '建议至少添加一条完整业务规则;若仅需基础兜底,可直接启用下方“通用规则包”。',
368
+ en_US: 'Used in rule mode. Draft rows are allowed during editing. Runtime requires valid fields.',
369
+ zh_Hans: '规则模式使用。编辑阶段允许草稿行,运行阶段要求有效规则字段。',
239
370
  },
240
371
  items: {
241
372
  type: 'object',
@@ -243,6 +374,10 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
243
374
  id: {
244
375
  type: 'string',
245
376
  title: { en_US: 'Rule ID', zh_Hans: '规则标识' },
377
+ description: {
378
+ en_US: 'Optional. Auto-generated when empty.',
379
+ zh_Hans: '可选。留空时系统自动生成。',
380
+ },
246
381
  },
247
382
  pattern: {
248
383
  type: 'string',
@@ -298,10 +433,14 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
298
433
  title: { en_US: 'Replacement Text', zh_Hans: '替换文本(可选)' },
299
434
  },
300
435
  },
436
+ required: ['pattern', 'type', 'action', 'scope', 'severity'],
301
437
  },
302
438
  },
303
439
  generalPack: {
304
440
  type: 'object',
441
+ 'x-ui': {
442
+ span: 2,
443
+ },
305
444
  title: {
306
445
  en_US: 'General Pack',
307
446
  zh_Hans: '通用规则包(本地开源词库)',
@@ -319,6 +458,10 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
319
458
  profile: {
320
459
  type: 'string',
321
460
  title: { en_US: 'Profile', zh_Hans: '策略档位' },
461
+ description: {
462
+ en_US: 'Strict blocks on hit with broader lexicon; Balanced rewrites on hit with smaller lexicon.',
463
+ zh_Hans: '严格:词库范围更大且命中后拦截;平衡:词库范围较小且命中后改写。',
464
+ },
322
465
  enum: ['strict', 'balanced'],
323
466
  'x-ui': {
324
467
  enumLabels: {
@@ -338,80 +481,251 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
338
481
  type: 'boolean',
339
482
  default: false,
340
483
  title: { en_US: 'Case Sensitive', zh_Hans: '区分大小写' },
484
+ 'x-ui': {
485
+ span: 2,
486
+ },
341
487
  },
342
488
  normalize: {
343
489
  type: 'boolean',
344
490
  default: true,
345
491
  title: { en_US: 'Normalize Text', zh_Hans: '文本标准化' },
492
+ 'x-ui': {
493
+ span: 2,
494
+ },
495
+ },
496
+ llm: {
497
+ type: 'object',
498
+ 'x-ui': {
499
+ span: 2,
500
+ },
501
+ title: {
502
+ en_US: 'LLM Filter Config',
503
+ zh_Hans: 'LLM 过滤配置',
504
+ },
505
+ description: {
506
+ en_US: 'Used only when mode=llm.',
507
+ zh_Hans: '仅在 mode=llm 时生效。',
508
+ },
509
+ properties: {
510
+ model: {
511
+ type: 'object',
512
+ title: {
513
+ en_US: 'Filter Model',
514
+ zh_Hans: '过滤模型',
515
+ },
516
+ 'x-ui': {
517
+ component: 'ai-model-select',
518
+ span: 2,
519
+ inputs: {
520
+ modelType: 'llm',
521
+ hiddenLabel: true,
522
+ },
523
+ },
524
+ },
525
+ scope: {
526
+ type: 'string',
527
+ title: { en_US: 'Scope', zh_Hans: '生效范围' },
528
+ enum: ['input', 'output', 'both'],
529
+ 'x-ui': {
530
+ enumLabels: {
531
+ input: { en_US: 'Input', zh_Hans: '仅输入' },
532
+ output: { en_US: 'Output', zh_Hans: '仅输出' },
533
+ both: { en_US: 'Both', zh_Hans: '输入和输出' },
534
+ },
535
+ },
536
+ },
537
+ systemPrompt: {
538
+ type: 'string',
539
+ title: { en_US: 'System Prompt', zh_Hans: '判定提示词' },
540
+ 'x-ui': {
541
+ component: 'textarea',
542
+ span: 2,
543
+ },
544
+ },
545
+ outputMethod: {
546
+ type: 'string',
547
+ title: { en_US: 'Output Method', zh_Hans: '结构化输出方式' },
548
+ enum: ['functionCalling', 'jsonMode', 'jsonSchema'],
549
+ default: 'jsonSchema',
550
+ 'x-ui': {
551
+ enumLabels: {
552
+ functionCalling: { en_US: 'Function Calling', zh_Hans: '函数调用' },
553
+ jsonMode: { en_US: 'JSON Mode', zh_Hans: 'JSON模式' },
554
+ jsonSchema: { en_US: 'JSON Schema', zh_Hans: 'JSON架构' },
555
+ },
556
+ },
557
+ },
558
+ onLlmError: {
559
+ type: 'string',
560
+ title: { en_US: 'On LLM Error', zh_Hans: 'LLM失败处理' },
561
+ enum: ['block', 'rewrite'],
562
+ 'x-ui': {
563
+ enumLabels: {
564
+ block: { en_US: 'Block', zh_Hans: '拦截' },
565
+ rewrite: { en_US: 'Rewrite', zh_Hans: '改写' },
566
+ },
567
+ },
568
+ },
569
+ errorRewriteText: {
570
+ type: 'string',
571
+ title: { en_US: 'Error Rewrite Text', zh_Hans: '失败改写文本' },
572
+ },
573
+ blockMessage: {
574
+ type: 'string',
575
+ title: { en_US: 'Block Message', zh_Hans: '拦截提示文本' },
576
+ },
577
+ rewriteFallbackText: {
578
+ type: 'string',
579
+ title: { en_US: 'Rewrite Fallback Text', zh_Hans: '改写兜底文本' },
580
+ },
581
+ timeoutMs: {
582
+ type: 'number',
583
+ title: { en_US: 'Timeout (ms)', zh_Hans: '超时毫秒' },
584
+ },
585
+ },
586
+ required: ['model', 'scope', 'systemPrompt', 'onLlmError'],
587
+ allOf: [
588
+ {
589
+ if: {
590
+ properties: {
591
+ onLlmError: {
592
+ const: 'rewrite',
593
+ },
594
+ },
595
+ },
596
+ then: {
597
+ required: ['errorRewriteText'],
598
+ },
599
+ },
600
+ ],
346
601
  },
347
602
  },
603
+ required: ['mode'],
604
+ allOf: [
605
+ {
606
+ if: {
607
+ properties: {
608
+ mode: {
609
+ const: 'llm',
610
+ },
611
+ },
612
+ },
613
+ then: {
614
+ required: ['llm'],
615
+ },
616
+ },
617
+ ],
348
618
  },
349
619
  };
350
620
  }
351
- createMiddleware(options, _context) {
621
+ async createMiddleware(options, context) {
352
622
  const parsed = sensitiveFilterConfigSchema.safeParse(options ?? {});
353
623
  if (!parsed.success) {
354
- throw new Error(`Invalid sensitive filter middleware options: ${z4.prettifyError(parsed.error)}`);
624
+ throw new Error(CONFIG_PARSE_ERROR);
355
625
  }
356
- const config = parsed.data;
626
+ if (parsed.data.mode === 'llm') {
627
+ return this.createLlmModeMiddleware(parsed.data, context);
628
+ }
629
+ return this.createRuleModeMiddleware(parsed.data);
630
+ }
631
+ createRuleModeMiddleware(config) {
357
632
  const caseSensitive = config.caseSensitive ?? false;
358
633
  const normalize = config.normalize ?? true;
359
634
  const customRules = normalizeRuleDrafts(config.rules ?? []);
360
635
  const generalRules = resolveGeneralPackRules(config.generalPack);
361
636
  const allRules = [...customRules, ...generalRules];
362
- const compiledRules = allRules.map((rule, index) => {
363
- const normalizedPattern = rule.type === 'keyword' ? normalizeForMatching(rule.pattern, normalize, caseSensitive) : rule.pattern;
364
- if (rule.type === 'regex') {
365
- try {
366
- return {
367
- ...rule,
368
- index,
369
- normalizedPattern,
370
- matchRegex: new RegExp(rule.pattern, caseSensitive ? '' : 'i'),
371
- rewriteRegex: new RegExp(rule.pattern, caseSensitive ? 'g' : 'gi'),
372
- };
373
- }
374
- catch (error) {
375
- const message = error instanceof Error ? error.message : String(error);
376
- throw new Error(`Invalid regex pattern in rule '${rule.id}': ${message}`);
377
- }
637
+ const hasEffectiveRules = allRules.length > 0;
638
+ let compiledRulesCache = null;
639
+ const getCompiledRules = () => {
640
+ if (compiledRulesCache) {
641
+ return compiledRulesCache;
378
642
  }
379
- return {
380
- ...rule,
381
- index,
382
- normalizedPattern,
383
- };
384
- });
643
+ compiledRulesCache = allRules.map((rule, index) => {
644
+ const normalizedPattern = rule.type === 'keyword' ? normalizeForMatching(rule.pattern, normalize, caseSensitive) : rule.pattern;
645
+ if (rule.type === 'regex') {
646
+ try {
647
+ return {
648
+ ...rule,
649
+ index,
650
+ normalizedPattern,
651
+ matchRegex: new RegExp(rule.pattern, caseSensitive ? '' : 'i'),
652
+ rewriteRegex: new RegExp(rule.pattern, caseSensitive ? 'g' : 'gi'),
653
+ };
654
+ }
655
+ catch (error) {
656
+ const message = error instanceof Error ? error.message : String(error);
657
+ throw new Error(`请完善规则配置:规则「${rule.id}」的正则表达式不合法(${message})。`);
658
+ }
659
+ }
660
+ return {
661
+ ...rule,
662
+ index,
663
+ normalizedPattern,
664
+ };
665
+ });
666
+ return compiledRulesCache;
667
+ };
385
668
  let inputBlockedMessage = null;
386
669
  let pendingInputRewrite = null;
670
+ let finalAction = 'pass';
671
+ let auditEntries = [];
672
+ const resetRunState = () => {
673
+ inputBlockedMessage = null;
674
+ pendingInputRewrite = null;
675
+ finalAction = 'pass';
676
+ auditEntries = [];
677
+ };
678
+ const pushAudit = (entry) => {
679
+ auditEntries.push({
680
+ ...entry,
681
+ timestamp: new Date().toISOString(),
682
+ mode: 'rule',
683
+ });
684
+ };
387
685
  return {
388
686
  name: SENSITIVE_FILTER_MIDDLEWARE_NAME,
389
687
  beforeAgent: async (state, runtime) => {
688
+ resetRunState();
689
+ if (!hasEffectiveRules) {
690
+ throw new Error(BUSINESS_RULES_VALIDATION_ERROR);
691
+ }
692
+ const compiledRules = getCompiledRules();
390
693
  const safeState = state ?? {};
391
694
  const safeRuntime = runtime ?? {};
392
- inputBlockedMessage = null;
393
- pendingInputRewrite = null;
394
- if (compiledRules.length === 0) {
395
- return undefined;
396
- }
397
695
  const inputText = extractInputText(safeState, safeRuntime);
398
696
  const inputMatches = findMatches(inputText, 'input', compiledRules, normalize, caseSensitive);
399
697
  const winner = pickWinningRule(inputMatches);
400
698
  if (!winner) {
699
+ pushAudit({
700
+ phase: 'input',
701
+ matched: false,
702
+ source: 'rule',
703
+ errorPolicyTriggered: false,
704
+ });
401
705
  return undefined;
402
706
  }
707
+ pushAudit({
708
+ phase: 'input',
709
+ matched: true,
710
+ source: 'rule',
711
+ action: winner.action,
712
+ reason: `rule:${winner.id}`,
713
+ errorPolicyTriggered: false,
714
+ });
403
715
  if (winner.action === 'block') {
716
+ finalAction = 'block';
404
717
  inputBlockedMessage = winner.replacementText?.trim() || DEFAULT_INPUT_BLOCK_MESSAGE;
405
718
  return undefined;
406
719
  }
407
- const rewrittenInput = rewriteTextByRule(inputText, winner, caseSensitive);
408
- pendingInputRewrite = rewrittenInput;
720
+ finalAction = 'rewrite';
721
+ pendingInputRewrite = rewriteTextByRule(inputText, winner, caseSensitive);
409
722
  return undefined;
410
723
  },
411
724
  wrapModelCall: async (request, handler) => {
412
- if (compiledRules.length === 0) {
413
- return handler(request);
725
+ if (!hasEffectiveRules) {
726
+ throw new Error(BUSINESS_RULES_VALIDATION_ERROR);
414
727
  }
728
+ const compiledRules = getCompiledRules();
415
729
  if (inputBlockedMessage) {
416
730
  return new AIMessage(inputBlockedMessage);
417
731
  }
@@ -422,21 +736,292 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
422
736
  const outputMatches = findMatches(outputText, 'output', compiledRules, normalize, caseSensitive);
423
737
  const winner = pickWinningRule(outputMatches);
424
738
  if (!winner) {
739
+ pushAudit({
740
+ phase: 'output',
741
+ matched: false,
742
+ source: 'rule',
743
+ errorPolicyTriggered: false,
744
+ });
425
745
  return response;
426
746
  }
747
+ pushAudit({
748
+ phase: 'output',
749
+ matched: true,
750
+ source: 'rule',
751
+ action: winner.action,
752
+ reason: `rule:${winner.id}`,
753
+ errorPolicyTriggered: false,
754
+ });
427
755
  if (winner.action === 'block') {
756
+ finalAction = 'block';
428
757
  const blockedOutput = winner.replacementText?.trim() || DEFAULT_OUTPUT_BLOCK_MESSAGE;
429
758
  return replaceModelResponseText(response, blockedOutput);
430
759
  }
760
+ finalAction = 'rewrite';
431
761
  const rewrittenOutput = rewriteTextByRule(outputText, winner, caseSensitive);
432
762
  return replaceModelResponseText(response, rewrittenOutput);
433
763
  },
434
764
  afterAgent: async () => {
765
+ console.log('[SensitiveFilterMiddleware][audit]', JSON.stringify({
766
+ mode: 'rule',
767
+ finalAction,
768
+ records: auditEntries,
769
+ }, null, 2));
770
+ return undefined;
771
+ },
772
+ };
773
+ }
774
+ createLlmModeMiddleware(config, context) {
775
+ const llmDraftConfig = config.llm;
776
+ let resolvedLlmConfig = null;
777
+ let modelPromise = null;
778
+ let structuredModelPromise = null;
779
+ const getLlmConfig = () => {
780
+ if (!resolvedLlmConfig) {
781
+ resolvedLlmConfig = resolveRuntimeLlmConfig(llmDraftConfig);
782
+ }
783
+ return resolvedLlmConfig;
784
+ };
785
+ const ensureModel = async () => {
786
+ const llmConfig = getLlmConfig();
787
+ if (!modelPromise) {
788
+ modelPromise = this.commandBus.execute(new CreateModelClientCommand(llmConfig.model, {
789
+ usageCallback: (event) => {
790
+ console.log('[SensitiveFilterMiddleware][LLM] Model Usage:', event);
791
+ },
792
+ }));
793
+ }
794
+ return modelPromise;
795
+ };
796
+ const ensureStructuredModel = async () => {
797
+ const llmConfig = getLlmConfig();
798
+ if (!structuredModelPromise) {
799
+ structuredModelPromise = (async () => {
800
+ const model = await ensureModel();
801
+ return model.withStructuredOutput?.(llmDecisionSchema, {
802
+ method: llmConfig.outputMethod,
803
+ }) ?? null;
804
+ })();
805
+ }
806
+ return structuredModelPromise;
807
+ };
808
+ let inputBlockedMessage = null;
809
+ let pendingInputRewrite = null;
810
+ let finalAction = 'pass';
811
+ let auditEntries = [];
812
+ const resetRunState = () => {
813
+ inputBlockedMessage = null;
814
+ pendingInputRewrite = null;
815
+ finalAction = 'pass';
816
+ auditEntries = [];
817
+ };
818
+ const pushAudit = (entry) => {
819
+ auditEntries.push({
820
+ ...entry,
821
+ timestamp: new Date().toISOString(),
822
+ mode: 'llm',
823
+ });
824
+ };
825
+ const buildEvaluationMessages = (phase, text, llmConfig) => {
826
+ return [
827
+ { role: 'system', content: llmConfig.systemPrompt },
828
+ {
829
+ role: 'user',
830
+ content: `phase=${phase}\n` +
831
+ '请严格基于给定文本进行敏感判定,并只返回约定结构。\n' +
832
+ `text:\n${text}`,
833
+ },
834
+ ];
835
+ };
836
+ const invokeAndTrack = async (phase, text, runtime, llmConfig) => {
837
+ const invokeCore = async () => {
838
+ const messages = buildEvaluationMessages(phase, text, llmConfig);
839
+ const [model, structuredModel] = await Promise.all([ensureModel(), ensureStructuredModel()]);
840
+ if (structuredModel) {
841
+ return withTimeout(structuredModel.invoke(messages), llmConfig.timeoutMs);
842
+ }
843
+ return withTimeout(model.invoke(messages), llmConfig.timeoutMs);
844
+ };
845
+ const parseCore = async () => {
846
+ const raw = await invokeCore();
847
+ return parseLlmDecision(raw, llmConfig.rewriteFallbackText);
848
+ };
849
+ const configurable = (runtime?.configurable ?? {});
850
+ const { thread_id, checkpoint_ns, checkpoint_id, subscriber, executionId } = configurable;
851
+ if (!thread_id || !executionId) {
852
+ return parseCore();
853
+ }
854
+ const tracked = await this.commandBus.execute(new WrapWorkflowNodeExecutionCommand(async () => {
855
+ const decision = await parseCore();
856
+ return {
857
+ state: decision,
858
+ output: decision,
859
+ };
860
+ }, {
861
+ execution: {
862
+ category: 'workflow',
863
+ type: 'middleware',
864
+ inputs: {
865
+ phase,
866
+ text,
867
+ },
868
+ parentId: executionId,
869
+ threadId: thread_id,
870
+ checkpointNs: checkpoint_ns,
871
+ checkpointId: checkpoint_id,
872
+ agentKey: context.node.key,
873
+ title: context.node.title,
874
+ },
875
+ subscriber,
876
+ }));
877
+ return tracked;
878
+ };
879
+ const resolveOnErrorDecision = (llmConfig, error) => {
880
+ const reason = `llm-error:${error instanceof Error ? error.message : String(error)}`;
881
+ if (llmConfig.onLlmError === 'block') {
882
+ return {
883
+ matched: true,
884
+ action: 'block',
885
+ reason,
886
+ };
887
+ }
888
+ return {
889
+ matched: true,
890
+ action: 'rewrite',
891
+ replacementText: llmConfig.errorRewriteText ?? llmConfig.rewriteFallbackText,
892
+ reason,
893
+ };
894
+ };
895
+ const resolveBlockMessage = (llmConfig, phase) => {
896
+ return llmConfig.blockMessage ??
897
+ (phase === 'input' ? DEFAULT_INPUT_BLOCK_MESSAGE : DEFAULT_OUTPUT_BLOCK_MESSAGE);
898
+ };
899
+ return {
900
+ name: SENSITIVE_FILTER_MIDDLEWARE_NAME,
901
+ beforeAgent: async (state, runtime) => {
902
+ resetRunState();
903
+ const llmConfig = getLlmConfig();
904
+ if (!modeIncludesScope(llmConfig.scope, 'input')) {
905
+ pushAudit({
906
+ phase: 'input',
907
+ matched: false,
908
+ source: 'llm',
909
+ reason: 'scope-skip',
910
+ errorPolicyTriggered: false,
911
+ });
912
+ return undefined;
913
+ }
914
+ const inputText = extractInputText(state ?? {}, runtime ?? {});
915
+ if (!inputText) {
916
+ pushAudit({
917
+ phase: 'input',
918
+ matched: false,
919
+ source: 'llm',
920
+ reason: 'empty-input',
921
+ errorPolicyTriggered: false,
922
+ });
923
+ return undefined;
924
+ }
925
+ let decision;
926
+ let fromErrorPolicy = false;
927
+ try {
928
+ decision = await invokeAndTrack('input', inputText, runtime, llmConfig);
929
+ }
930
+ catch (error) {
931
+ decision = resolveOnErrorDecision(llmConfig, error);
932
+ fromErrorPolicy = true;
933
+ }
934
+ pushAudit({
935
+ phase: 'input',
936
+ matched: decision.matched,
937
+ source: fromErrorPolicy ? 'error-policy' : 'llm',
938
+ action: decision.action,
939
+ reason: decision.reason,
940
+ errorPolicyTriggered: fromErrorPolicy,
941
+ });
942
+ if (!decision.matched || !decision.action) {
943
+ return undefined;
944
+ }
945
+ if (decision.action === 'block') {
946
+ finalAction = 'block';
947
+ inputBlockedMessage = resolveBlockMessage(llmConfig, 'input');
948
+ return undefined;
949
+ }
950
+ finalAction = 'rewrite';
951
+ pendingInputRewrite = toNonEmptyString(decision.replacementText) ?? llmConfig.rewriteFallbackText;
952
+ return undefined;
953
+ },
954
+ wrapModelCall: async (request, handler) => {
955
+ const llmConfig = getLlmConfig();
956
+ if (inputBlockedMessage) {
957
+ return new AIMessage(inputBlockedMessage);
958
+ }
959
+ const modelRequest = pendingInputRewrite ? rewriteModelRequestInput(request, pendingInputRewrite) : request;
960
+ pendingInputRewrite = null;
961
+ const response = await handler(modelRequest);
962
+ if (!modeIncludesScope(llmConfig.scope, 'output')) {
963
+ pushAudit({
964
+ phase: 'output',
965
+ matched: false,
966
+ source: 'llm',
967
+ reason: 'scope-skip',
968
+ errorPolicyTriggered: false,
969
+ });
970
+ return response;
971
+ }
972
+ const outputText = extractModelResponseText(response);
973
+ if (!outputText) {
974
+ pushAudit({
975
+ phase: 'output',
976
+ matched: false,
977
+ source: 'llm',
978
+ reason: 'empty-output',
979
+ errorPolicyTriggered: false,
980
+ });
981
+ return response;
982
+ }
983
+ let decision;
984
+ let fromErrorPolicy = false;
985
+ try {
986
+ decision = await invokeAndTrack('output', outputText, request?.runtime, llmConfig);
987
+ }
988
+ catch (error) {
989
+ decision = resolveOnErrorDecision(llmConfig, error);
990
+ fromErrorPolicy = true;
991
+ }
992
+ pushAudit({
993
+ phase: 'output',
994
+ matched: decision.matched,
995
+ source: fromErrorPolicy ? 'error-policy' : 'llm',
996
+ action: decision.action,
997
+ reason: decision.reason,
998
+ errorPolicyTriggered: fromErrorPolicy,
999
+ });
1000
+ if (!decision.matched || !decision.action) {
1001
+ return response;
1002
+ }
1003
+ if (decision.action === 'block') {
1004
+ finalAction = 'block';
1005
+ return replaceModelResponseText(response, resolveBlockMessage(llmConfig, 'output'));
1006
+ }
1007
+ finalAction = 'rewrite';
1008
+ return replaceModelResponseText(response, toNonEmptyString(decision.replacementText) ?? llmConfig.rewriteFallbackText);
1009
+ },
1010
+ afterAgent: async () => {
1011
+ console.log('[SensitiveFilterMiddleware][audit]', JSON.stringify({
1012
+ mode: 'llm',
1013
+ finalAction,
1014
+ records: auditEntries,
1015
+ }, null, 2));
435
1016
  return undefined;
436
1017
  },
437
1018
  };
438
1019
  }
439
1020
  };
1021
+ __decorate([
1022
+ Inject(CommandBus),
1023
+ __metadata("design:type", CommandBus)
1024
+ ], SensitiveFilterMiddleware.prototype, "commandBus", void 0);
440
1025
  SensitiveFilterMiddleware = __decorate([
441
1026
  Injectable(),
442
1027
  AgentMiddlewareStrategy(SENSITIVE_FILTER_MIDDLEWARE_NAME)