plugin-sensitive-filter-xr 0.0.2 → 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.
- package/README.md +156 -54
- package/dist/lib/sensitive-filter.module.d.ts.map +1 -1
- package/dist/lib/sensitive-filter.module.js +2 -1
- package/dist/lib/sensitiveFilter.d.ts +6 -3
- package/dist/lib/sensitiveFilter.d.ts.map +1 -1
- package/dist/lib/sensitiveFilter.js +642 -84
- package/dist/lib/types.d.ts +149 -29
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +45 -21
- package/package.json +1 -1
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
}
|
|
@@ -168,7 +169,7 @@ function normalizeRuleDrafts(input) {
|
|
|
168
169
|
const rules = [];
|
|
169
170
|
for (const [index, draft] of input.entries()) {
|
|
170
171
|
if (!isRecord(draft)) {
|
|
171
|
-
|
|
172
|
+
continue;
|
|
172
173
|
}
|
|
173
174
|
const id = toNonEmptyString(draft['id']);
|
|
174
175
|
const pattern = toNonEmptyString(draft['pattern']);
|
|
@@ -177,44 +178,145 @@ function normalizeRuleDrafts(input) {
|
|
|
177
178
|
const severity = toNonEmptyString(draft['severity']);
|
|
178
179
|
const action = toNonEmptyString(draft['action']);
|
|
179
180
|
const replacementText = toNonEmptyString(draft['replacementText']) ?? undefined;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!type) {
|
|
184
|
-
throw new Error(`Invalid rule at index ${index}: type is required`);
|
|
185
|
-
}
|
|
186
|
-
if (!action) {
|
|
187
|
-
throw new Error(`Invalid rule at index ${index}: action is required`);
|
|
181
|
+
const hasAnyValue = Boolean(id || pattern || type || scope || severity || action || replacementText);
|
|
182
|
+
if (!hasAnyValue) {
|
|
183
|
+
continue;
|
|
188
184
|
}
|
|
189
|
-
if (action
|
|
190
|
-
|
|
185
|
+
if (!pattern || !type || !action || !scope || !severity) {
|
|
186
|
+
continue;
|
|
191
187
|
}
|
|
192
188
|
if (!['keyword', 'regex'].includes(type)) {
|
|
193
|
-
|
|
189
|
+
continue;
|
|
194
190
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw new Error(`Invalid rule at index ${index}: unsupported scope '${normalizedScope}'`);
|
|
191
|
+
if (!['input', 'output', 'both'].includes(scope)) {
|
|
192
|
+
continue;
|
|
198
193
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
throw new Error(`Invalid rule at index ${index}: unsupported severity '${normalizedSeverity}'`);
|
|
194
|
+
if (!['high', 'medium'].includes(severity)) {
|
|
195
|
+
continue;
|
|
202
196
|
}
|
|
203
197
|
if (!['block', 'rewrite'].includes(action)) {
|
|
204
|
-
|
|
198
|
+
continue;
|
|
205
199
|
}
|
|
206
200
|
rules.push({
|
|
207
201
|
id: id ?? `rule-${index + 1}`,
|
|
208
202
|
pattern,
|
|
209
203
|
type,
|
|
210
|
-
scope
|
|
211
|
-
severity
|
|
204
|
+
scope,
|
|
205
|
+
severity,
|
|
212
206
|
action,
|
|
213
207
|
replacementText,
|
|
214
208
|
});
|
|
215
209
|
}
|
|
216
210
|
return rules;
|
|
217
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
|
+
}
|
|
218
320
|
let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
219
321
|
constructor() {
|
|
220
322
|
this.meta = {
|
|
@@ -224,8 +326,8 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
224
326
|
zh_Hans: '敏感内容过滤中间件',
|
|
225
327
|
},
|
|
226
328
|
description: {
|
|
227
|
-
en_US: 'Filter sensitive content before input and after output using
|
|
228
|
-
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 提示词模式(互斥)进行输入/输出敏感内容过滤。',
|
|
229
331
|
},
|
|
230
332
|
icon: {
|
|
231
333
|
type: 'svg',
|
|
@@ -234,15 +336,37 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
234
336
|
configSchema: {
|
|
235
337
|
type: 'object',
|
|
236
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
|
+
},
|
|
237
358
|
rules: {
|
|
238
359
|
type: 'array',
|
|
360
|
+
'x-ui': {
|
|
361
|
+
span: 2,
|
|
362
|
+
},
|
|
239
363
|
title: {
|
|
240
364
|
en_US: 'Business Rules',
|
|
241
365
|
zh_Hans: '业务规则',
|
|
242
366
|
},
|
|
243
367
|
description: {
|
|
244
|
-
en_US: '
|
|
245
|
-
zh_Hans: '
|
|
368
|
+
en_US: 'Used in rule mode. Draft rows are allowed during editing. Runtime requires valid fields.',
|
|
369
|
+
zh_Hans: '规则模式使用。编辑阶段允许草稿行,运行阶段要求有效规则字段。',
|
|
246
370
|
},
|
|
247
371
|
items: {
|
|
248
372
|
type: 'object',
|
|
@@ -306,29 +430,17 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
306
430
|
},
|
|
307
431
|
replacementText: {
|
|
308
432
|
type: 'string',
|
|
309
|
-
title: { en_US: 'Replacement Text', zh_Hans: '
|
|
433
|
+
title: { en_US: 'Replacement Text', zh_Hans: '替换文本(可选)' },
|
|
310
434
|
},
|
|
311
435
|
},
|
|
312
|
-
required: ['pattern', 'type', 'action'],
|
|
313
|
-
allOf: [
|
|
314
|
-
{
|
|
315
|
-
if: {
|
|
316
|
-
properties: {
|
|
317
|
-
action: {
|
|
318
|
-
const: 'rewrite',
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
then: {
|
|
323
|
-
required: ['replacementText'],
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
],
|
|
436
|
+
required: ['pattern', 'type', 'action', 'scope', 'severity'],
|
|
327
437
|
},
|
|
328
|
-
minItems: 1,
|
|
329
438
|
},
|
|
330
439
|
generalPack: {
|
|
331
440
|
type: 'object',
|
|
441
|
+
'x-ui': {
|
|
442
|
+
span: 2,
|
|
443
|
+
},
|
|
332
444
|
title: {
|
|
333
445
|
en_US: 'General Pack',
|
|
334
446
|
zh_Hans: '通用规则包(本地开源词库)',
|
|
@@ -346,6 +458,10 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
346
458
|
profile: {
|
|
347
459
|
type: 'string',
|
|
348
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
|
+
},
|
|
349
465
|
enum: ['strict', 'balanced'],
|
|
350
466
|
'x-ui': {
|
|
351
467
|
enumLabels: {
|
|
@@ -365,80 +481,251 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
365
481
|
type: 'boolean',
|
|
366
482
|
default: false,
|
|
367
483
|
title: { en_US: 'Case Sensitive', zh_Hans: '区分大小写' },
|
|
484
|
+
'x-ui': {
|
|
485
|
+
span: 2,
|
|
486
|
+
},
|
|
368
487
|
},
|
|
369
488
|
normalize: {
|
|
370
489
|
type: 'boolean',
|
|
371
490
|
default: true,
|
|
372
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
|
+
],
|
|
373
601
|
},
|
|
374
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
|
+
],
|
|
375
618
|
},
|
|
376
619
|
};
|
|
377
620
|
}
|
|
378
|
-
createMiddleware(options,
|
|
621
|
+
async createMiddleware(options, context) {
|
|
379
622
|
const parsed = sensitiveFilterConfigSchema.safeParse(options ?? {});
|
|
380
623
|
if (!parsed.success) {
|
|
381
|
-
throw new Error(
|
|
624
|
+
throw new Error(CONFIG_PARSE_ERROR);
|
|
625
|
+
}
|
|
626
|
+
if (parsed.data.mode === 'llm') {
|
|
627
|
+
return this.createLlmModeMiddleware(parsed.data, context);
|
|
382
628
|
}
|
|
383
|
-
|
|
629
|
+
return this.createRuleModeMiddleware(parsed.data);
|
|
630
|
+
}
|
|
631
|
+
createRuleModeMiddleware(config) {
|
|
384
632
|
const caseSensitive = config.caseSensitive ?? false;
|
|
385
633
|
const normalize = config.normalize ?? true;
|
|
386
|
-
const customRules = normalizeRuleDrafts(config.rules);
|
|
634
|
+
const customRules = normalizeRuleDrafts(config.rules ?? []);
|
|
387
635
|
const generalRules = resolveGeneralPackRules(config.generalPack);
|
|
388
636
|
const allRules = [...customRules, ...generalRules];
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
...rule,
|
|
395
|
-
index,
|
|
396
|
-
normalizedPattern,
|
|
397
|
-
matchRegex: new RegExp(rule.pattern, caseSensitive ? '' : 'i'),
|
|
398
|
-
rewriteRegex: new RegExp(rule.pattern, caseSensitive ? 'g' : 'gi'),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
403
|
-
throw new Error(`Invalid regex pattern in rule '${rule.id}': ${message}`);
|
|
404
|
-
}
|
|
637
|
+
const hasEffectiveRules = allRules.length > 0;
|
|
638
|
+
let compiledRulesCache = null;
|
|
639
|
+
const getCompiledRules = () => {
|
|
640
|
+
if (compiledRulesCache) {
|
|
641
|
+
return compiledRulesCache;
|
|
405
642
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
+
};
|
|
412
668
|
let inputBlockedMessage = null;
|
|
413
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
|
+
};
|
|
414
685
|
return {
|
|
415
686
|
name: SENSITIVE_FILTER_MIDDLEWARE_NAME,
|
|
416
687
|
beforeAgent: async (state, runtime) => {
|
|
688
|
+
resetRunState();
|
|
689
|
+
if (!hasEffectiveRules) {
|
|
690
|
+
throw new Error(BUSINESS_RULES_VALIDATION_ERROR);
|
|
691
|
+
}
|
|
692
|
+
const compiledRules = getCompiledRules();
|
|
417
693
|
const safeState = state ?? {};
|
|
418
694
|
const safeRuntime = runtime ?? {};
|
|
419
|
-
inputBlockedMessage = null;
|
|
420
|
-
pendingInputRewrite = null;
|
|
421
|
-
if (compiledRules.length === 0) {
|
|
422
|
-
return undefined;
|
|
423
|
-
}
|
|
424
695
|
const inputText = extractInputText(safeState, safeRuntime);
|
|
425
696
|
const inputMatches = findMatches(inputText, 'input', compiledRules, normalize, caseSensitive);
|
|
426
697
|
const winner = pickWinningRule(inputMatches);
|
|
427
698
|
if (!winner) {
|
|
699
|
+
pushAudit({
|
|
700
|
+
phase: 'input',
|
|
701
|
+
matched: false,
|
|
702
|
+
source: 'rule',
|
|
703
|
+
errorPolicyTriggered: false,
|
|
704
|
+
});
|
|
428
705
|
return undefined;
|
|
429
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
|
+
});
|
|
430
715
|
if (winner.action === 'block') {
|
|
716
|
+
finalAction = 'block';
|
|
431
717
|
inputBlockedMessage = winner.replacementText?.trim() || DEFAULT_INPUT_BLOCK_MESSAGE;
|
|
432
718
|
return undefined;
|
|
433
719
|
}
|
|
434
|
-
|
|
435
|
-
pendingInputRewrite =
|
|
720
|
+
finalAction = 'rewrite';
|
|
721
|
+
pendingInputRewrite = rewriteTextByRule(inputText, winner, caseSensitive);
|
|
436
722
|
return undefined;
|
|
437
723
|
},
|
|
438
724
|
wrapModelCall: async (request, handler) => {
|
|
439
|
-
if (
|
|
440
|
-
|
|
725
|
+
if (!hasEffectiveRules) {
|
|
726
|
+
throw new Error(BUSINESS_RULES_VALIDATION_ERROR);
|
|
441
727
|
}
|
|
728
|
+
const compiledRules = getCompiledRules();
|
|
442
729
|
if (inputBlockedMessage) {
|
|
443
730
|
return new AIMessage(inputBlockedMessage);
|
|
444
731
|
}
|
|
@@ -449,21 +736,292 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
449
736
|
const outputMatches = findMatches(outputText, 'output', compiledRules, normalize, caseSensitive);
|
|
450
737
|
const winner = pickWinningRule(outputMatches);
|
|
451
738
|
if (!winner) {
|
|
739
|
+
pushAudit({
|
|
740
|
+
phase: 'output',
|
|
741
|
+
matched: false,
|
|
742
|
+
source: 'rule',
|
|
743
|
+
errorPolicyTriggered: false,
|
|
744
|
+
});
|
|
452
745
|
return response;
|
|
453
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
|
+
});
|
|
454
755
|
if (winner.action === 'block') {
|
|
756
|
+
finalAction = 'block';
|
|
455
757
|
const blockedOutput = winner.replacementText?.trim() || DEFAULT_OUTPUT_BLOCK_MESSAGE;
|
|
456
758
|
return replaceModelResponseText(response, blockedOutput);
|
|
457
759
|
}
|
|
760
|
+
finalAction = 'rewrite';
|
|
458
761
|
const rewrittenOutput = rewriteTextByRule(outputText, winner, caseSensitive);
|
|
459
762
|
return replaceModelResponseText(response, rewrittenOutput);
|
|
460
763
|
},
|
|
461
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));
|
|
462
1016
|
return undefined;
|
|
463
1017
|
},
|
|
464
1018
|
};
|
|
465
1019
|
}
|
|
466
1020
|
};
|
|
1021
|
+
__decorate([
|
|
1022
|
+
Inject(CommandBus),
|
|
1023
|
+
__metadata("design:type", CommandBus)
|
|
1024
|
+
], SensitiveFilterMiddleware.prototype, "commandBus", void 0);
|
|
467
1025
|
SensitiveFilterMiddleware = __decorate([
|
|
468
1026
|
Injectable(),
|
|
469
1027
|
AgentMiddlewareStrategy(SENSITIVE_FILTER_MIDDLEWARE_NAME)
|