perf: variabel replace;Feat: prompt optimizer code (#5453)

* feat: add prompt optimizer (#5444)

* feat: add prompt optimizer

* fix

* perf: variabel replace

* perf: prompt optimizer code

* feat: init charts shell

* perf: user error remove

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-08-14 15:48:22 +08:00
committed by GitHub
parent 6a02d2a2e5
commit 9fbfabac61
35 changed files with 1968 additions and 202 deletions

View File

@@ -0,0 +1,632 @@
import { describe, it, expect } from 'vitest';
import { replaceJsonBodyString } from '@fastgpt/service/core/workflow/dispatch/tools/http468';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
describe('replaceJsonBodyString', () => {
// Mock runtime nodes for testing
const mockRuntimeNodes = [
{
nodeId: 'node1',
outputs: [
{ id: 'output1', value: 'Hello World', valueType: 'string' },
{ id: 'output2', value: 42, valueType: 'number' },
{ id: 'output3', value: true, valueType: 'boolean' },
{ id: 'output4', value: { nested: 'value' }, valueType: 'object' },
{ id: 'output5', value: [1, 2, 3], valueType: 'array' }
],
inputs: []
}
] as unknown as RuntimeNodeItemType[];
const mockVariables = {
userName: 'John Doe',
userAge: 30,
isActive: true,
userProfile: { name: 'John', email: 'john@example.com' },
tags: ['developer', 'tester']
};
const mockAllVariables = {
...mockVariables,
systemVar: 'system_value',
nullVar: null,
undefinedVar: undefined,
emptyString: '',
zeroNumber: 0,
falseBool: false
};
const mockProps = {
variables: mockVariables,
allVariables: mockAllVariables,
runtimeNodes: mockRuntimeNodes
};
describe('Basic variable replacement functionality', () => {
it('should correctly replace string variables', () => {
const input = '{"name": "{{userName}}", "greeting": "Hello {{userName}}"}';
const expected = '{"name": "John Doe", "greeting": "Hello John Doe"}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
it('should correctly replace number variables', () => {
const input =
'{"age": {{userAge}}, "doubled": {{userAge}}, "calculation": "{{userAge}} years old"}';
const expected = '{"age": 30, "doubled": 30, "calculation": "30 years old"}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
it('should correctly replace boolean variables', () => {
const input =
'{"active": {{isActive}}, "inactive": {{falseBool}}, "status": "User is {{isActive}}"}';
const expected = '{"active": true, "inactive": false, "status": "User is true"}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
it('should correctly replace object variables', () => {
const input = '{"profile": {{userProfile}}, "info": "Profile: {{userProfile}}"}';
const expected =
'{"profile": {"name":"John","email":"john@example.com"}, "info": "Profile: {"name":"John","email":"john@example.com"}"}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
it('should correctly replace array variables and node output variables', () => {
const input =
'{"tags": {{tags}}, "nodeOutput": "{{$node1.output1$}}", "numbers": {{$node1.output5$}}}';
const expected =
'{"tags": ["developer","tester"], "nodeOutput": "Hello World", "numbers": [1,2,3]}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
});
describe('Security and boundary testing', () => {
it('should prevent circular references', () => {
const maliciousProps = {
...mockProps,
allVariables: {
...mockAllVariables,
selfRef: '{{selfRef}}', // Self reference
circularA: '{{circularB}}',
circularB: '{{circularA}}'
}
};
const input = '{"self": "{{selfRef}}", "circular": "{{circularA}}"}';
// Should keep circular references unreplaced
const result = replaceJsonBodyString({ text: input }, maliciousProps);
expect(result).toContain('{{selfRef}}');
expect(result).toContain('{{circularB}}');
});
it('should prevent excessive nesting depth', () => {
const deepNestingProps = {
...mockProps,
allVariables: {
...mockAllVariables,
level1: '{{level2}}',
level2: '{{level3}}',
level3: '{{level4}}',
level4: '{{level5}}',
level5: '{{level6}}',
level6: '{{level7}}',
level7: '{{level8}}',
level8: '{{level9}}',
level9: '{{level10}}',
level10: '{{level11}}',
level11: '{{level12}}',
level12: 'final_value'
}
};
const input = '{"deep": "{{level1}}"}';
// Should handle but limit depth
const result = replaceJsonBodyString({ text: input }, deepNestingProps);
expect(result).toBeDefined();
expect(result).toBe('{"deep": "{{level12}}"}');
// Should eventually resolve to a valid value
});
it('should handle large number of variables performance test', () => {
const manyVariables: Record<string, any> = {};
let input = '{';
// Create 1000 variables
for (let i = 0; i < 1000; i++) {
manyVariables[`var${i}`] = `value${i}`;
input += `"var${i}": "{{var${i}}}",`;
}
input = input.slice(0, -1) + '}'; // Remove last comma
const largeProps = {
...mockProps,
allVariables: { ...mockAllVariables, ...manyVariables }
};
const result = replaceJsonBodyString({ text: input }, largeProps);
expect(result).toBeDefined();
expect(result).toContain('value0');
expect(result).toContain('value999');
// Verify performance and correctness
});
it('should prevent regex attacks', () => {
const maliciousProps = {
...mockProps,
allVariables: {
...mockAllVariables,
// Contains special regex characters
maliciousVar: '.*+?^${}()|[]\\',
regexAttack: '(.*){999999}', // Potential regex attack
specialChars: '\\n\\r\\t"\'`<>&'
}
};
const input =
'{"malicious": "{{maliciousVar}}", "attack": "{{regexAttack}}", "special": "{{specialChars}}"}';
const result = replaceJsonBodyString({ text: input }, maliciousProps);
expect(result).toBeDefined();
expect(typeof result).toBe('string');
// Should properly escape special characters
expect(result).toContain('.*+?^${}()|[]\\\\');
});
it('should handle quote injection and escape characters', () => {
const maliciousProps = {
...mockProps,
allVariables: {
...mockAllVariables,
quotedVar: '"malicious"value"',
escapedVar: '\\"escaped\\"',
jsonInjection: '", "injected": "hacked',
scriptTag: '<script>alert("xss")</script>',
newlineAttack: 'line1\\nline2\\r\\nline3'
}
};
const input =
'{"quoted": "{{quotedVar}}", "escaped": "{{escapedVar}}", "injection": "{{jsonInjection}}", "script": "{{scriptTag}}", "newlines": "{{newlineAttack}}"}';
const result = replaceJsonBodyString({ text: input }, maliciousProps);
expect(result).toBeDefined();
expect(typeof result).toBe('string');
// Should properly escape quotes
expect(result).toContain('\\"malicious\\"value\\"');
// Should handle newlines
expect(result).toContain('line1\\\\nline2\\\\r\\\\nline3');
// Should generate valid JSON
try {
JSON.parse(result);
} catch (e) {
// If parsing fails, there might be injection attacks or escaping issues
// Further validation needed here
expect(e).toBeDefined();
}
});
});
describe('Edge cases', () => {
it('should handle empty string', () => {
const result = replaceJsonBodyString({ text: '' }, mockProps);
expect(result).toBe('');
});
it('should handle static content without variables', () => {
const input = '{"static": "value", "number": 123}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(input);
});
it('should handle null and undefined values', () => {
const input = '{"nullVar": {{nullVar}}, "undefinedVar": {{undefinedVar}}}';
const expected = '{"nullVar": null, "undefinedVar": null}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
it('should handle non-existent variables', () => {
const input = '{"missing": "{{nonExistentVar}}", "static": "value"}';
const expected = '{"missing": "null", "static": "value"}';
const result = replaceJsonBodyString({ text: input }, mockProps);
expect(result).toBe(expected);
});
});
describe('Complex robustness and compatibility tests', () => {
// Test Case 1: Nested JSON with mixed variable types and complex escape scenarios
it('Complex Test 1: Deep nested JSON with mixed data types, escape characters and Unicode', () => {
const complexProps = {
...mockProps,
allVariables: {
...mockAllVariables,
unicodeText: '你好世界 🌍 Hello\nWorld',
complexObject: {
nested: {
array: [{ key: 'value"with"quotes' }, null, undefined],
special: 'tab\there\nnewline\rcarriage\\"quotes\\\'single'
}
},
htmlContent: '<div class="test" data-value=\'mixed"quotes\'>Content & more</div>',
jsonLikeString: '{"fake": "json", "number": 123, "bool": true}',
emptyValues: { empty: '', zero: 0, false: false, null: null }
}
};
const input = `{
"metadata": {
"title": "{{unicodeText}}",
"description": "Testing \\"complex\\" scenarios",
"nested": {{complexObject}},
"htmlSnippet": "{{htmlContent}}"
},
"payload": {
"jsonString": "{{jsonLikeString}}",
"emptyData": {{emptyValues}},
"nodeOutput": "{{$node1.output4$}}",
"arrayData": {{$node1.output5$}}
},
"validation": {
"hasNewlines": "Text with\\nembedded\\tcharacters: {{unicodeText}}",
"quoteMixing": "\\"Outer quotes\\" and '{{htmlContent}}' content"
}
}`;
const result = replaceJsonBodyString({ text: input }, complexProps);
// Verify it's valid JSON after replacement
expect(() => JSON.parse(result)).not.toThrow();
// Verify complex nested structures are preserved
const parsed = JSON.parse(result);
expect(parsed.metadata.nested.nested.array).toHaveLength(3);
expect(parsed.payload.emptyData.zero).toBe(0);
expect(parsed.payload.emptyData.false).toBe(false);
expect(parsed.payload.emptyData.null).toBe(null);
// Verify Unicode and special characters are handled
expect(parsed.metadata.title).toContain('你好世界');
expect(parsed.metadata.title).toContain('🌍');
});
// Test Case 2: Variable substitution in complex nested structures with edge cases
it('Complex Test 2: Complex variable interpolation and data type handling', () => {
const edgeCaseProps = {
...mockProps,
allVariables: {
...mockAllVariables,
leadingTrailingSpaces: ' spaced content ',
numberAsString: '12345',
boolAsString: 'true',
arrayAsString: '[1,2,3]',
objectAsString: '{"key":"value"}',
commaInValue: 'value,with,commas',
colonInValue: 'key:value:pair',
braceInValue: 'value{with}braces',
safeJsonString: 'safe_content_123'
}
};
const input = `{
"testDataTypes": {
"data": "{{leadingTrailingSpaces}}",
"number": {{numberAsString}},
"bool": {{boolAsString}},
"array": {{arrayAsString}},
"object": {{objectAsString}}
},
"testStringNumbers": {
"stringAsNumber": "Value is {{numberAsString}}",
"boolAsString": "Boolean: {{boolAsString}}",
"safeContent": "{{safeJsonString}}"
},
"testSpecialChars": {
"commas": "{{commaInValue}}",
"colons": "{{colonInValue}}",
"braces": "{{braceInValue}}"
},
"testMixedQuoting": {
"inQuotes": "{{arrayAsString}}",
"withoutQuotes": {{arrayAsString}},
"safeQuote": "Content: {{safeJsonString}}"
}
}`;
const result = replaceJsonBodyString({ text: input }, edgeCaseProps);
// Should produce valid JSON
expect(() => JSON.parse(result)).not.toThrow();
const parsed = JSON.parse(result);
// Verify trimming and data type handling
expect(parsed.testDataTypes.data).toBe(' spaced content ');
expect(parsed.testDataTypes.number).toBe(12345);
expect(parsed.testDataTypes.bool).toBe(true);
// Verify arrays and objects are properly embedded
expect(Array.isArray(parsed.testDataTypes.array)).toBe(true);
expect(typeof parsed.testDataTypes.object).toBe('object');
// Verify special characters in values don't break JSON
expect(parsed.testSpecialChars.commas).toContain(',');
expect(parsed.testSpecialChars.colons).toContain(':');
expect(parsed.testSpecialChars.braces).toContain('{');
// Verify string interpolation works correctly
expect(parsed.testStringNumbers.stringAsNumber).toContain('12345');
expect(parsed.testMixedQuoting.safeQuote).toContain('safe_content_123');
});
// Test Case 3: Extreme recursion and circular reference prevention
it('Complex Test 3: Advanced circular reference patterns and recursion limits', () => {
const recursionProps = {
...mockProps,
allVariables: {
...mockAllVariables,
// Complex circular patterns
chainA: '{{chainB}}',
chainB: '{{chainC}}',
chainC: '{{chainD}}',
chainD: '{{chainE}}',
chainE: '{{chainA}}', // Creates cycle
// Indirect self-reference
selfIndirect: 'prefix-{{selfIndirect}}-suffix',
// Node reference cycles
nodeRef1: '{{$node1.output1$}} -> {{nodeRef2}}',
nodeRef2: '{{nodeRef1}}',
// Deep nested variable chains
level1: '{{level2}}-{{level3}}',
level2: '{{level4}}-{{level5}}',
level3: '{{level6}}-{{level7}}',
level4: '{{level8}}-{{level9}}',
level5: '{{level10}}-{{level11}}',
level6: '{{level12}}-final',
level7: 'end',
level8: 'deep8',
level9: 'deep9',
level10: 'deep10',
level11: 'deep11',
level12: 'deep12'
}
};
const input = `{
"circularTests": {
"simpleChain": "{{chainA}}",
"selfReference": "{{selfIndirect}}",
"nodeCircular": "{{nodeRef1}}",
"deepNesting": "{{level1}}"
},
"mixedCircular": {
"valid": "{{userName}}",
"circular": "{{chainB}}",
"nodeData": "{{$node1.output1$}}",
"combined": "Valid: {{userName}}, Circular: {{chainC}}"
}
}`;
const result = replaceJsonBodyString({ text: input }, recursionProps);
// Should not crash or hang
expect(result).toBeDefined();
expect(typeof result).toBe('string');
// Should contain unreplaced circular references
expect(result).toMatch(/\{\{chain[A-E]\}\}/);
expect(result).toContain('{{selfIndirect}}');
// Valid variables should still be replaced
expect(result).toContain('John Doe');
expect(result).toContain('Hello World');
// Should handle deep nesting up to limit
const parsed = JSON.parse(result);
expect(parsed.circularTests.deepNesting).toBeDefined();
});
// Test Case 4: Large payload stress test with performance validation
it('Complex Test 4: Large payload stress test with varied data patterns', () => {
const stressTestVariables: Record<string, any> = {};
// Generate large number of variables with different patterns
for (let i = 0; i < 500; i++) {
stressTestVariables[`str_${i}`] = `string_value_${i}_with_special_chars_"'\\\\n\\\\t`;
stressTestVariables[`num_${i}`] = Math.random() * 1000000;
stressTestVariables[`bool_${i}`] = i % 2 === 0;
stressTestVariables[`obj_${i}`] = {
id: i,
data: `nested_${i}`,
list: [i, i + 1, i + 2],
nested: { deep: `value_${i}` }
};
stressTestVariables[`arr_${i}`] = Array.from(
{ length: (i % 10) + 1 },
(_, j) => `item_${i}_${j}`
);
}
// Add some problematic variables
stressTestVariables['largeText'] = 'x'.repeat(10000);
stressTestVariables['jsonBomb'] = JSON.stringify({ large: 'x'.repeat(1000) });
stressTestVariables['specialChars'] = '\\n\\r\\t\\"\\\\\'`~!@#$%^&*()[]{}|;:,.<>?/+=';
const stressProps = {
...mockProps,
allVariables: { ...mockAllVariables, ...stressTestVariables }
};
// Create large JSON structure
let input = '{\n';
for (let i = 0; i < 200; i++) {
input += ` "section_${i}": {\n`;
input += ` "str": "{{str_${i}}}",\n`;
input += ` "num": {{num_${i}}},\n`;
input += ` "bool": {{bool_${i}}},\n`;
input += ` "obj": {{obj_${i}}},\n`;
input += ` "arr": {{arr_${i}}},\n`;
input += ` "mixed": "String {{str_${i}}} with number {{num_${i}}}"\n`;
input += ` }${i < 199 ? ',' : ''}\n`;
}
input += '}';
const startTime = Date.now();
const result = replaceJsonBodyString({ text: input }, stressProps);
const endTime = Date.now();
// Performance check - should complete within reasonable time
expect(endTime - startTime).toBeLessThan(5000); // 5 seconds max
// Should produce valid JSON
expect(() => JSON.parse(result)).not.toThrow();
const parsed = JSON.parse(result);
// Verify structure integrity
expect(Object.keys(parsed)).toHaveLength(200);
expect(parsed.section_0.str).toContain('string_value_0');
expect(typeof parsed.section_0.num).toBe('number');
expect(typeof parsed.section_0.bool).toBe('boolean');
expect(Array.isArray(parsed.section_0.arr)).toBe(true);
});
// Test Case 5: Security and injection attack prevention
it('Complex Test 5: Comprehensive security and injection attack prevention', () => {
const securityProps = {
...mockProps,
allVariables: {
...mockAllVariables,
// SQL injection patterns
sqlInjection: "'; DROP TABLE users; --",
sqlInjection2: "' OR '1'='1",
// JSON injection attempts
jsonBreak: '", "hacked": true, "original": "',
jsonBreak2: '}}, "injected": {"evil": "payload"}, "fake": {{',
// XSS attempts
xssScript: '<script>alert("XSS")</script>',
xssOnload: '<img src=x onerror=alert("XSS")>',
// Template injection attempts
templateInject: '{{constructor.constructor("alert(1)")()}}',
prototypePolute: '__proto__.polluted',
// Path traversal
pathTraversal: '../../../etc/passwd',
pathTraversal2: '..\\..\\..\\windows\\system32\\config\\sam',
// Unicode and encoding attacks
unicodeAttack: '\u0000\u0001\u0002\u003c\u003e',
utf8Attack: '\uFEFF\uFFFE\uFFFF',
// Control characters
controlChars: '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0E\x0F',
// Regex DoS attempts
regexDos: '(' + 'a'.repeat(10000) + ')*',
regexDos2: '(a+)+$',
// Large payload attack
payloadBomb: 'A'.repeat(100000),
// Mixed attack vectors
mixedAttack: '"><script>alert(/XSS/)</script><"{{malicious}}">'
}
};
const input = `{
"sqlTests": {
"query1": "{{sqlInjection}}",
"query2": "SELECT * FROM users WHERE name='{{sqlInjection2}}'"
},
"jsonTests": {
"break1": "{{jsonBreak}}",
"break2": "{{jsonBreak2}}",
"safe": "normal_value"
},
"xssTests": {
"script": "{{xssScript}}",
"onload": "{{xssOnload}}",
"content": "<div>{{xssScript}}</div>"
},
"templateTests": {
"inject": "{{templateInject}}",
"prototype": "{{prototypePolute}}"
},
"pathTests": {
"traversal1": "{{pathTraversal}}",
"traversal2": "{{pathTraversal2}}"
},
"encodingTests": {
"unicode": "{{unicodeAttack}}",
"utf8": "{{utf8Attack}}",
"control": "{{controlChars}}"
},
"performanceTests": {
"regex1": "{{regexDos}}",
"regex2": "{{regexDos2}}",
"large": "{{payloadBomb}}"
},
"mixedAttack": "{{mixedAttack}}"
}`;
const startTime = Date.now();
const result = replaceJsonBodyString({ text: input }, securityProps);
const endTime = Date.now();
// Should not take excessive time (DoS protection)
expect(endTime - startTime).toBeLessThan(10000); // 10 seconds max
// Should produce valid JSON despite malicious input
expect(() => JSON.parse(result)).not.toThrow();
const parsed = JSON.parse(result);
// Verify dangerous content is properly handled
expect(parsed.sqlTests.query1).toBe("'; DROP TABLE users; --");
expect(parsed.xssTests.script).toBe('<script>alert("XSS")</script>'); // Content is preserved as-is in JSON string
// The function properly escapes strings even with injection attempts
expect(typeof parsed.jsonTests.break1).toBe('string'); // Should be a string, not break JSON structure
expect(parsed.jsonTests.break1).toBe('", "hacked": true, "original": "'); // The injection content is escaped
// Verify the JSON structure wasn't broken by injection attempts
expect(Object.keys(parsed)).toEqual([
'sqlTests',
'jsonTests',
'xssTests',
'templateTests',
'pathTests',
'encodingTests',
'performanceTests',
'mixedAttack'
]);
// Verify large payloads are handled
expect(parsed.performanceTests.large).toHaveLength(100000);
// Verify no code execution occurred (template injection variable was replaced with null because it doesn't exist)
expect(typeof parsed.templateTests.inject).toBe('string');
expect(parsed.templateTests.inject).toBe('null'); // Non-existent variables become "null"
});
});
});

29
test/tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "..",
"paths": {
"@fastgpt/*": ["packages/*"],
"@test/*": ["test/*"],
"@/*": ["projects/app/src/*"]
},
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": false,
"types": ["vitest/globals", "node"]
},
"include": [
"**/*.ts",
"**/*.tsx",
"../packages/**/*.ts",
"../packages/**/*.tsx",
"../packages/**/*.d.ts",
"../projects/app/src/**/*.ts",
"../projects/app/src/**/*.tsx"
],
"exclude": ["node_modules", "../node_modules", "dist", "../dist"]
}