mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
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:
parent
6a02d2a2e5
commit
9fbfabac61
|
|
@ -6,10 +6,12 @@ description: 'FastGPT V4.12.1 更新说明'
|
|||
|
||||
## 🚀 新增内容
|
||||
|
||||
1. Prompt 自动生成和优化。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
1. 工作流响应优化,主动指定响应值进入历史记录,而不是根据 key 决定。
|
||||
2. 避免工作流中,变量替换导致的死循环或深度递归风险。
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-12T22:22:18+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-08-13T16:31:28+08:00",
|
||||
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/development/openapi/share.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00",
|
||||
|
|
@ -97,12 +97,13 @@
|
|||
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00",
|
||||
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-08-12T22:22:18+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-08-13T14:29:13+08:00",
|
||||
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-10/4101.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00",
|
||||
"document/content/docs/upgrading/4-12/4120.mdx": "2025-08-12T22:45:19+08:00",
|
||||
"document/content/docs/upgrading/4-12/4121.mdx": "2025-08-13T21:31:30+08:00",
|
||||
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
|
||||
|
|
|
|||
|
|
@ -34,15 +34,75 @@ export const valToStr = (val: any) => {
|
|||
};
|
||||
|
||||
// replace {{variable}} to value
|
||||
export function replaceVariable(text: any, obj: Record<string, string | number | undefined>) {
|
||||
export function replaceVariable(
|
||||
text: any,
|
||||
obj: Record<string, string | number | undefined>,
|
||||
depth = 0
|
||||
) {
|
||||
if (typeof text !== 'string') return text;
|
||||
|
||||
for (const key in obj) {
|
||||
const val = obj[key];
|
||||
const formatVal = valToStr(val);
|
||||
text = text.replace(new RegExp(`{{(${key})}}`, 'g'), () => formatVal);
|
||||
const MAX_REPLACEMENT_DEPTH = 10;
|
||||
const processedVariables = new Set<string>();
|
||||
|
||||
// Prevent infinite recursion
|
||||
if (depth > MAX_REPLACEMENT_DEPTH) {
|
||||
return text;
|
||||
}
|
||||
return text || '';
|
||||
|
||||
// Check for circular references in variable values
|
||||
const hasCircularReference = (value: any, targetKey: string): boolean => {
|
||||
if (typeof value !== 'string') return false;
|
||||
|
||||
// Check if the value contains the target variable pattern (direct self-reference)
|
||||
const selfRefPattern = new RegExp(
|
||||
`\\{\\{${targetKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}\\}`,
|
||||
'g'
|
||||
);
|
||||
return selfRefPattern.test(value);
|
||||
};
|
||||
|
||||
let result = text;
|
||||
let hasReplacements = false;
|
||||
|
||||
// Build replacement map first to avoid modifying string during iteration
|
||||
const replacements: { pattern: string; replacement: string }[] = [];
|
||||
|
||||
for (const key in obj) {
|
||||
// Skip if already processed to avoid immediate circular reference
|
||||
if (processedVariables.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const val = obj[key];
|
||||
|
||||
// Check for direct circular reference
|
||||
if (hasCircularReference(String(val), key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const formatVal = valToStr(val);
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
replacements.push({
|
||||
pattern: `{{(${escapedKey})}}`,
|
||||
replacement: formatVal
|
||||
});
|
||||
|
||||
processedVariables.add(key);
|
||||
hasReplacements = true;
|
||||
}
|
||||
|
||||
// Apply all replacements
|
||||
replacements.forEach(({ pattern, replacement }) => {
|
||||
result = result.replace(new RegExp(pattern, 'g'), () => replacement);
|
||||
});
|
||||
|
||||
// If we made replacements and there might be nested variables, recursively process
|
||||
if (hasReplacements && /\{\{[^}]+\}\}/.test(result)) {
|
||||
result = replaceVariable(result, obj, depth + 1);
|
||||
}
|
||||
|
||||
return result || '';
|
||||
}
|
||||
|
||||
/* replace sensitive text */
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import type { WorkflowInteractiveResponseType } from '../workflow/template/syste
|
|||
import type { FlowNodeInputItemType } from '../workflow/type/io';
|
||||
import type { FlowNodeTemplateType } from '../workflow/type/node.d';
|
||||
|
||||
export type ChatSchema = {
|
||||
export type ChatSchemaType = {
|
||||
_id: string;
|
||||
chatId: string;
|
||||
userId: string;
|
||||
|
|
@ -33,6 +33,8 @@ export type ChatSchema = {
|
|||
customTitle: string;
|
||||
top: boolean;
|
||||
source: `${ChatSourceEnum}`;
|
||||
sourceName?: string;
|
||||
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
|
||||
|
|
@ -43,7 +45,7 @@ export type ChatSchema = {
|
|||
metadata?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type ChatWithAppSchema = Omit<ChatSchema, 'appId'> & {
|
||||
export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
|
||||
appId: AppSchema;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -467,27 +467,63 @@ export const formatVariableValByType = (val: any, valueType?: WorkflowIOValueTyp
|
|||
|
||||
return val;
|
||||
};
|
||||
|
||||
// replace {{$xx.xx$}} variables for text
|
||||
export function replaceEditorVariable({
|
||||
text,
|
||||
nodes,
|
||||
variables
|
||||
variables,
|
||||
depth = 0
|
||||
}: {
|
||||
text: any;
|
||||
nodes: RuntimeNodeItemType[];
|
||||
variables: Record<string, any>; // global variables
|
||||
depth?: number;
|
||||
}) {
|
||||
if (typeof text !== 'string') return text;
|
||||
if (text === '') return text;
|
||||
|
||||
const MAX_REPLACEMENT_DEPTH = 10;
|
||||
const processedVariables = new Set<string>();
|
||||
|
||||
// Prevent infinite recursion
|
||||
if (depth > MAX_REPLACEMENT_DEPTH) {
|
||||
return text;
|
||||
}
|
||||
|
||||
text = replaceVariable(text, variables);
|
||||
|
||||
// Check for circular references in variable values
|
||||
const hasCircularReference = (value: any, targetKey: string): boolean => {
|
||||
if (typeof value !== 'string') return false;
|
||||
|
||||
// Check if the value contains the target variable pattern (direct self-reference)
|
||||
const selfRefPattern = new RegExp(
|
||||
`\\{\\{\\$${targetKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\$\\}\\}`,
|
||||
'g'
|
||||
);
|
||||
return selfRefPattern.test(value);
|
||||
};
|
||||
|
||||
const variablePattern = /\{\{\$([^.]+)\.([^$]+)\$\}\}/g;
|
||||
const matches = [...text.matchAll(variablePattern)];
|
||||
if (matches.length === 0) return text;
|
||||
|
||||
matches.forEach((match) => {
|
||||
let result = text;
|
||||
let hasReplacements = false;
|
||||
|
||||
// Build replacement map first to avoid modifying string during iteration
|
||||
const replacements: Array<{ pattern: string; replacement: string }> = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const nodeId = match[1];
|
||||
const id = match[2];
|
||||
const variableKey = `${nodeId}.${id}`;
|
||||
|
||||
// Skip if already processed to avoid immediate circular reference
|
||||
if (processedVariables.has(variableKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variableVal = (() => {
|
||||
if (nodeId === VARIABLE_NODE_ID) {
|
||||
|
|
@ -505,13 +541,35 @@ export function replaceEditorVariable({
|
|||
if (input) return getReferenceVariableValue({ value: input.value, nodes, variables });
|
||||
})();
|
||||
|
||||
const formatVal = valToStr(variableVal);
|
||||
// Check for direct circular reference
|
||||
if (hasCircularReference(String(variableVal), variableKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const regex = new RegExp(`\\{\\{\\$(${nodeId}\\.${id})\\$\\}\\}`, 'g');
|
||||
text = text.replace(regex, () => formatVal);
|
||||
const formatVal = valToStr(variableVal);
|
||||
const escapedNodeId = nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
replacements.push({
|
||||
pattern: `\\{\\{\\$(${escapedNodeId}\\.${escapedId})\\$\\}\\}`,
|
||||
replacement: formatVal
|
||||
});
|
||||
|
||||
processedVariables.add(variableKey);
|
||||
hasReplacements = true;
|
||||
}
|
||||
|
||||
// Apply all replacements
|
||||
replacements.forEach(({ pattern, replacement }) => {
|
||||
result = result.replace(new RegExp(pattern, 'g'), replacement);
|
||||
});
|
||||
|
||||
return text || '';
|
||||
// If we made replacements and there might be nested variables, recursively process
|
||||
if (hasReplacements && /\{\{\$[^.]+\.[^$]+\$\}\}/.test(result)) {
|
||||
result = replaceEditorVariable({ text: result, nodes, variables, depth: depth + 1 });
|
||||
}
|
||||
|
||||
return result || '';
|
||||
}
|
||||
|
||||
export const textAdaptGptResponse = ({
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export enum UsageSourceEnum {
|
|||
official_account = 'official_account',
|
||||
pdfParse = 'pdfParse',
|
||||
mcp = 'mcp',
|
||||
evaluation = 'evaluation'
|
||||
evaluation = 'evaluation',
|
||||
optimize_prompt = 'optimize_prompt'
|
||||
}
|
||||
|
||||
export const UsageSourceMap = {
|
||||
|
|
@ -55,5 +56,8 @@ export const UsageSourceMap = {
|
|||
},
|
||||
[UsageSourceEnum.evaluation]: {
|
||||
label: i18nT('account_usage:evaluation')
|
||||
},
|
||||
[UsageSourceEnum.optimize_prompt]: {
|
||||
label: i18nT('common:support.wallet.usage.Optimize Prompt')
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { connectionMongo, getMongoModel } from '../../common/mongo';
|
||||
const { Schema } = connectionMongo;
|
||||
import { type ChatSchema as ChatType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { type ChatSchemaType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
TeamCollectionName,
|
||||
|
|
@ -83,10 +83,13 @@ const ChatSchema = new Schema({
|
|||
//For special storage
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
|
||||
initStatistics: Boolean
|
||||
});
|
||||
|
||||
try {
|
||||
ChatSchema.index({ initCharts: 1 });
|
||||
ChatSchema.index({ chatId: 1 });
|
||||
// get user history
|
||||
ChatSchema.index({ tmbId: 1, appId: 1, top: -1, updateTime: -1 });
|
||||
|
|
@ -104,4 +107,4 @@ try {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
export const MongoChat = getMongoModel<ChatType>(chatCollectionName, ChatSchema);
|
||||
export const MongoChat = getMongoModel<ChatSchemaType>(chatCollectionName, ChatSchema);
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export async function saveChat({
|
|||
});
|
||||
|
||||
try {
|
||||
const userId = outLinkUid || tmbId;
|
||||
const userId = String(outLinkUid || tmbId);
|
||||
const now = new Date();
|
||||
const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,19 +6,18 @@ import {
|
|||
VARIABLE_NODE_ID,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
DispatchNodeResponseKeyEnum,
|
||||
SseResponseEventEnum
|
||||
} from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import axios from 'axios';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import type {
|
||||
ModuleDispatchProps,
|
||||
RuntimeNodeItemType
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import {
|
||||
formatVariableValByType,
|
||||
getReferenceVariableValue,
|
||||
replaceEditorVariable,
|
||||
textAdaptGptResponse
|
||||
replaceEditorVariable
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import json5 from 'json5';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
|
|
@ -121,96 +120,6 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
|
|||
variables: allVariables
|
||||
});
|
||||
};
|
||||
/* Replace the JSON string to reduce parsing errors
|
||||
1. Replace undefined values with null
|
||||
2. Replace newline strings
|
||||
*/
|
||||
const replaceJsonBodyString = (text: string) => {
|
||||
// Check if the variable is in quotes
|
||||
const isVariableInQuotes = (text: string, variable: string) => {
|
||||
const index = text.indexOf(variable);
|
||||
if (index === -1) return false;
|
||||
|
||||
// 计算变量前面的引号数量
|
||||
const textBeforeVar = text.substring(0, index);
|
||||
const matches = textBeforeVar.match(/"/g) || [];
|
||||
|
||||
// 如果引号数量为奇数,则变量在引号内
|
||||
return matches.length % 2 === 1;
|
||||
};
|
||||
const valToStr = (val: any, isQuoted = false) => {
|
||||
if (val === undefined) return 'null';
|
||||
if (val === null) return 'null';
|
||||
|
||||
if (typeof val === 'object') return JSON.stringify(val);
|
||||
|
||||
if (typeof val === 'string') {
|
||||
if (isQuoted) {
|
||||
// Replace newlines with escaped newlines
|
||||
return val.replace(/\n/g, '\\n').replace(/(?<!\\)"/g, '\\"');
|
||||
}
|
||||
try {
|
||||
JSON.parse(val);
|
||||
return val;
|
||||
} catch (error) {
|
||||
const str = JSON.stringify(val);
|
||||
|
||||
return str.startsWith('"') && str.endsWith('"') ? str.slice(1, -1) : str;
|
||||
}
|
||||
}
|
||||
|
||||
return String(val);
|
||||
};
|
||||
|
||||
// 1. Replace {{key.key}} variables
|
||||
const regex1 = /\{\{\$([^.]+)\.([^$]+)\$\}\}/g;
|
||||
const matches1 = [...text.matchAll(regex1)];
|
||||
matches1.forEach((match) => {
|
||||
const nodeId = match[1];
|
||||
const id = match[2];
|
||||
const fullMatch = match[0];
|
||||
|
||||
// 检查变量是否在引号内
|
||||
const isInQuotes = isVariableInQuotes(text, fullMatch);
|
||||
|
||||
const variableVal = (() => {
|
||||
if (nodeId === VARIABLE_NODE_ID) {
|
||||
return variables[id];
|
||||
}
|
||||
// Find upstream node input/output
|
||||
const node = runtimeNodes.find((node) => node.nodeId === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const output = node.outputs.find((output) => output.id === id);
|
||||
if (output) return formatVariableValByType(output.value, output.valueType);
|
||||
|
||||
const input = node.inputs.find((input) => input.key === id);
|
||||
if (input)
|
||||
return getReferenceVariableValue({ value: input.value, nodes: runtimeNodes, variables });
|
||||
})();
|
||||
|
||||
const formatVal = valToStr(variableVal, isInQuotes);
|
||||
|
||||
const regex = new RegExp(`\\{\\{\\$(${nodeId}\\.${id})\\$\\}\\}`, '');
|
||||
text = text.replace(regex, () => formatVal);
|
||||
});
|
||||
|
||||
// 2. Replace {{key}} variables
|
||||
const regex2 = /{{([^}]+)}}/g;
|
||||
const matches2 = text.match(regex2) || [];
|
||||
const uniqueKeys2 = [...new Set(matches2.map((match) => match.slice(2, -2)))];
|
||||
for (const key of uniqueKeys2) {
|
||||
const fullMatch = `{{${key}}}`;
|
||||
// 检查变量是否在引号内
|
||||
const isInQuotes = isVariableInQuotes(text, fullMatch);
|
||||
|
||||
text = text.replace(new RegExp(`{{(${key})}}`, ''), () =>
|
||||
valToStr(allVariables[key], isInQuotes)
|
||||
);
|
||||
}
|
||||
|
||||
return text.replace(/(".*?")\s*:\s*undefined\b/g, '$1:null');
|
||||
};
|
||||
|
||||
httpReqUrl = replaceStringVariables(httpReqUrl);
|
||||
|
||||
|
|
@ -273,7 +182,10 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
|
|||
}
|
||||
if (!httpJsonBody) return {};
|
||||
if (httpContentType === ContentTypes.json) {
|
||||
httpJsonBody = replaceJsonBodyString(httpJsonBody);
|
||||
httpJsonBody = replaceJsonBodyString(
|
||||
{ text: httpJsonBody },
|
||||
{ variables, allVariables, runtimeNodes }
|
||||
);
|
||||
return json5.parse(httpJsonBody);
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +272,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
|
|||
Object.keys(results).length > 0 ? results : rawResponse
|
||||
};
|
||||
} catch (error) {
|
||||
addLog.error('Http request error', error);
|
||||
addLog.warn('Http request error', formatHttpError(error));
|
||||
|
||||
// @adapt
|
||||
if (node.catchError === undefined) {
|
||||
|
|
@ -391,6 +303,187 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
|
|||
}
|
||||
};
|
||||
|
||||
/* Replace the JSON string to reduce parsing errors
|
||||
1. Replace undefined values with null
|
||||
2. Replace newline strings
|
||||
*/
|
||||
export const replaceJsonBodyString = (
|
||||
{ text, depth = 0 }: { text: string; depth?: number },
|
||||
props: {
|
||||
variables: Record<string, any>;
|
||||
allVariables: Record<string, any>;
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
}
|
||||
) => {
|
||||
const { variables, allVariables, runtimeNodes } = props;
|
||||
|
||||
const MAX_REPLACEMENT_DEPTH = 10;
|
||||
const processedVariables = new Set<string>();
|
||||
|
||||
// Prevent infinite recursion
|
||||
if (depth > MAX_REPLACEMENT_DEPTH) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// Check if the variable is in quotes
|
||||
const isVariableInQuotes = (text: string, variable: string) => {
|
||||
const index = text.indexOf(variable);
|
||||
if (index === -1) return false;
|
||||
|
||||
// 计算变量前面的引号数量
|
||||
const textBeforeVar = text.substring(0, index);
|
||||
const matches = textBeforeVar.match(/"/g) || [];
|
||||
|
||||
// 如果引号数量为奇数,则变量在引号内
|
||||
return matches.length % 2 === 1;
|
||||
};
|
||||
|
||||
const valToStr = (val: any, isQuoted = false) => {
|
||||
if (val === undefined) return 'null';
|
||||
if (val === null) return 'null';
|
||||
|
||||
if (typeof val === 'object') {
|
||||
const jsonStr = JSON.stringify(val);
|
||||
if (isQuoted) {
|
||||
// Only escape quotes for JSON strings inside quotes (backslashes are already properly escaped by JSON.stringify)
|
||||
return jsonStr.replace(/"/g, '\\"');
|
||||
}
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
if (typeof val === 'string') {
|
||||
if (isQuoted) {
|
||||
const jsonStr = JSON.stringify(val);
|
||||
return jsonStr.slice(1, -1); // 移除首尾的双引号
|
||||
}
|
||||
try {
|
||||
JSON.parse(val);
|
||||
return val;
|
||||
} catch (error) {
|
||||
const str = JSON.stringify(val);
|
||||
return str.startsWith('"') && str.endsWith('"') ? str.slice(1, -1) : str;
|
||||
}
|
||||
}
|
||||
|
||||
return String(val);
|
||||
};
|
||||
|
||||
// Check for circular references in variable values
|
||||
const hasCircularReference = (value: any, targetKey: string): boolean => {
|
||||
if (typeof value !== 'string') return false;
|
||||
|
||||
// Check if the value contains the target variable pattern (direct self-reference)
|
||||
const selfRefPattern = new RegExp(
|
||||
`\\{\\{${targetKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\}\\}`,
|
||||
'g'
|
||||
);
|
||||
return selfRefPattern.test(value);
|
||||
};
|
||||
|
||||
let result = text;
|
||||
let hasReplacements = false;
|
||||
|
||||
// 1. Replace {{$nodeId.id$}} variables
|
||||
const regex1 = /\{\{\$([^.]+)\.([^$]+)\$\}\}/g;
|
||||
const matches1 = [...result.matchAll(regex1)];
|
||||
|
||||
// Build replacement map first to avoid modifying string during iteration
|
||||
const replacements1: Array<{ pattern: string; replacement: string }> = [];
|
||||
|
||||
for (const match of matches1) {
|
||||
const nodeId = match[1];
|
||||
const id = match[2];
|
||||
const fullMatch = match[0];
|
||||
const variableKey = `${nodeId}.${id}`;
|
||||
|
||||
// Skip if already processed to avoid immediate circular reference
|
||||
if (processedVariables.has(variableKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查变量是否在引号内
|
||||
const isInQuotes = isVariableInQuotes(result, fullMatch);
|
||||
|
||||
const variableVal = (() => {
|
||||
if (nodeId === VARIABLE_NODE_ID) {
|
||||
return variables[id];
|
||||
}
|
||||
// Find upstream node input/output
|
||||
const node = runtimeNodes.find((node) => node.nodeId === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const output = node.outputs.find((output) => output.id === id);
|
||||
if (output) return formatVariableValByType(output.value, output.valueType);
|
||||
|
||||
const input = node.inputs.find((input) => input.key === id);
|
||||
if (input)
|
||||
return getReferenceVariableValue({ value: input.value, nodes: runtimeNodes, variables });
|
||||
})();
|
||||
|
||||
const formatVal = valToStr(variableVal, isInQuotes);
|
||||
// Check for direct circular reference
|
||||
if (hasCircularReference(String(variableVal), variableKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const escapedPattern = `\\{\\{\\$(${nodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})\\$\\}\\}`;
|
||||
|
||||
replacements1.push({
|
||||
pattern: escapedPattern,
|
||||
replacement: formatVal
|
||||
});
|
||||
|
||||
processedVariables.add(variableKey);
|
||||
hasReplacements = true;
|
||||
}
|
||||
replacements1.forEach(({ pattern, replacement }) => {
|
||||
result = result.replace(new RegExp(pattern, 'g'), replacement);
|
||||
});
|
||||
|
||||
// 2. Replace {{key}} variables
|
||||
const regex2 = /{{([^}]+)}}/g;
|
||||
const matches2 = result.match(regex2) || [];
|
||||
const uniqueKeys2 = [...new Set(matches2.map((match) => match.slice(2, -2)))];
|
||||
// Build replacement map for simple variables
|
||||
const replacements2: Array<{ pattern: string; replacement: string }> = [];
|
||||
for (const key of uniqueKeys2) {
|
||||
if (processedVariables.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullMatch = `{{${key}}}`;
|
||||
const variableVal = allVariables[key];
|
||||
|
||||
// Check for direct circular reference
|
||||
if (hasCircularReference(variableVal, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查变量是否在引号内
|
||||
const isInQuotes = isVariableInQuotes(result, fullMatch);
|
||||
const formatVal = valToStr(variableVal, isInQuotes);
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
replacements2.push({
|
||||
pattern: `{{(${escapedKey})}}`,
|
||||
replacement: formatVal
|
||||
});
|
||||
|
||||
processedVariables.add(key);
|
||||
hasReplacements = true;
|
||||
}
|
||||
replacements2.forEach(({ pattern, replacement }) => {
|
||||
result = result.replace(new RegExp(pattern, 'g'), replacement);
|
||||
});
|
||||
|
||||
// If we made replacements and there might be nested variables, recursively process
|
||||
if (hasReplacements && /\{\{[^}]*\}\}/.test(result)) {
|
||||
result = replaceJsonBodyString({ text: result, depth: depth + 1 }, props);
|
||||
}
|
||||
|
||||
return result.replace(/(".*?")\s*:\s*undefined\b/g, '$1:null');
|
||||
};
|
||||
|
||||
async function fetchData({
|
||||
method,
|
||||
url,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const dispatchLafRequest = async (props: LafRequestProps): Promise<LafRes
|
|||
[DispatchNodeResponseKeyEnum.toolResponses]: rawResponse
|
||||
};
|
||||
} catch (error) {
|
||||
addLog.error('Http request error', error);
|
||||
addLog.warn('Http request error', formatHttpError(error));
|
||||
return {
|
||||
error: {
|
||||
[NodeOutputKeyEnum.errorText]: getErrText(error)
|
||||
|
|
|
|||
|
|
@ -450,6 +450,7 @@ export const iconPaths = {
|
|||
'model/yi': () => import('./icons/model/yi.svg'),
|
||||
more: () => import('./icons/more.svg'),
|
||||
moreLine: () => import('./icons/moreLine.svg'),
|
||||
optimizer: () => import('./icons/optimizer.svg'),
|
||||
out: () => import('./icons/out.svg'),
|
||||
paragraph: () => import('./icons/paragraph.svg'),
|
||||
'phoneTabbar/me': () => import('./icons/phoneTabbar/me.svg'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.02489 5.13636C6.02489 4.7096 6.37085 4.36364 6.79762 4.36364H11.434C11.8607 4.36364 12.2067 4.7096 12.2067 5.13636C12.2067 5.56313 11.8607 5.90909 11.434 5.90909H9.88853V12.0909H11.434C11.8607 12.0909 12.2067 12.4369 12.2067 12.8636C12.2067 13.2904 11.8607 13.6364 11.434 13.6364H6.79762C6.37085 13.6364 6.02489 13.2904 6.02489 12.8636C6.02489 12.4369 6.37085 12.0909 6.79762 12.0909H8.34307V5.90909H6.79762C6.37085 5.90909 6.02489 5.56313 6.02489 5.13636Z" fill="url(#paint0_linear_25014_10095)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.33398 12.669C2.01342 12.8153 1.63496 12.674 1.48865 12.3535C0.540947 10.2771 0.497952 7.89092 1.35999 5.78813C1.49365 5.4621 1.86631 5.30615 2.19234 5.43981C2.51837 5.57346 2.67433 5.94612 2.54067 6.27215C1.81207 8.04942 1.84846 10.0686 2.64949 11.8237C2.7958 12.1442 2.65454 12.5227 2.33398 12.669Z" fill="url(#paint1_linear_25014_10095)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.6702 12.6774C15.3495 12.5313 15.208 12.153 15.354 11.8323C16.1583 10.0664 16.1874 8.03144 15.4407 6.24392C15.3049 5.91878 15.4584 5.5451 15.7835 5.40929C16.1087 5.27347 16.4824 5.42695 16.6182 5.75209C17.5012 7.86613 17.4673 10.271 16.5153 12.3612C16.3692 12.6819 15.9909 12.8235 15.6702 12.6774Z" fill="url(#paint2_linear_25014_10095)"/>
|
||||
<path d="M3.77789 0.747892C3.85803 0.41737 4.32811 0.417369 4.40826 0.747892L4.62879 1.65735C4.6574 1.77535 4.74954 1.86749 4.86754 1.8961L5.777 2.11663C6.10752 2.19678 6.10752 2.66686 5.777 2.747L4.86754 2.96753C4.85279 2.97111 4.83845 2.97568 4.82458 2.98117C4.7275 3.01959 4.65382 3.10304 4.62879 3.20629L4.40826 4.11574C4.40325 4.1364 4.39672 4.15577 4.38885 4.17384C4.27087 4.44498 3.85302 4.42561 3.77789 4.11575L3.55736 3.20629C3.55557 3.19892 3.55353 3.19164 3.55126 3.18448C3.51712 3.07702 3.42923 2.99436 3.3186 2.96753L2.40914 2.747C2.38849 2.74199 2.36912 2.73546 2.35105 2.7276C2.07991 2.60962 2.09928 2.19177 2.40914 2.11663L3.3186 1.8961C3.4366 1.86749 3.52874 1.77535 3.55736 1.65735L3.77789 0.747892Z" fill="url(#paint3_linear_25014_10095)"/>
|
||||
<path d="M13.8233 13.8843C13.9035 13.5537 14.3736 13.5537 14.4537 13.8843L14.6742 14.7937C14.7029 14.9117 14.795 15.0039 14.913 15.0325L15.8225 15.253C16.153 15.3331 16.153 15.8032 15.8225 15.8834L14.913 16.1039C14.8982 16.1075 14.8839 16.112 14.87 16.1175C14.773 16.1559 14.6993 16.2394 14.6742 16.3427L14.4537 17.2521C14.4487 17.2728 14.4422 17.2921 14.4343 17.3102C14.3163 17.5813 13.8985 17.562 13.8233 17.2521L13.6028 16.3427C13.601 16.3353 13.599 16.328 13.5967 16.3208C13.5626 16.2134 13.4747 16.1307 13.3641 16.1039L12.4546 15.8834C12.4339 15.8784 12.4146 15.8718 12.3965 15.864C12.1254 15.746 12.1447 15.3281 12.4546 15.253L13.3641 15.0325C13.4821 15.0039 13.5742 14.9117 13.6028 14.7937L13.8233 13.8843Z" fill="url(#paint4_linear_25014_10095)"/>
|
||||
<path d="M14.5249 2.04545C14.5249 2.47222 14.1789 2.81818 13.7522 2.81818C13.3254 2.81818 12.9794 2.47222 12.9794 2.04545C12.9794 1.61869 13.3254 1.27273 13.7522 1.27273C14.1789 1.27273 14.5249 1.61869 14.5249 2.04545Z" fill="url(#paint5_linear_25014_10095)"/>
|
||||
<path d="M5.25216 15.9545C5.25216 16.3813 4.9062 16.7273 4.47943 16.7273C4.05267 16.7273 3.70671 16.3813 3.70671 15.9545C3.70671 15.5278 4.05267 15.1818 4.47943 15.1818C4.9062 15.1818 5.25216 15.5278 5.25216 15.9545Z" fill="url(#paint6_linear_25014_10095)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_25014_10095" x1="8.99999" y1="1.05195" x2="8.99999" y2="16.9481" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#499DFF"/>
|
||||
<stop offset="0.432292" stop-color="#2770FF"/>
|
||||
<stop offset="1" stop-color="#6E80FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
|
@ -57,7 +57,7 @@ const MyModal = ({
|
|||
closeOnOverlayClick={closeOnOverlayClick}
|
||||
returnFocusOnClose={false}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalOverlay zIndex={props.zIndex} />
|
||||
<ModalContent
|
||||
w={w}
|
||||
minW={['90vw', '400px']}
|
||||
|
|
@ -65,6 +65,9 @@ const MyModal = ({
|
|||
position={'relative'}
|
||||
maxH={'85vh'}
|
||||
boxShadow={'7'}
|
||||
containerProps={{
|
||||
zIndex: props.zIndex
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{!title && onClose && showCloseButton && <ModalCloseButton zIndex={1} />}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import {
|
|||
useDisclosure,
|
||||
type PlacementWithLogical,
|
||||
PopoverArrow,
|
||||
type PopoverContentProps
|
||||
type PopoverContentProps,
|
||||
Box,
|
||||
Portal
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends PopoverContentProps {
|
||||
|
|
@ -15,6 +17,7 @@ interface Props extends PopoverContentProps {
|
|||
offset?: [number, number];
|
||||
trigger?: 'hover' | 'click';
|
||||
hasArrow?: boolean;
|
||||
onBackdropClick?: () => void;
|
||||
children: (e: { onClose: () => void }) => React.ReactNode;
|
||||
onCloseFunc?: () => void;
|
||||
onOpenFunc?: () => void;
|
||||
|
|
@ -31,6 +34,7 @@ const MyPopover = ({
|
|||
onOpenFunc,
|
||||
onCloseFunc,
|
||||
closeOnBlur = false,
|
||||
onBackdropClick,
|
||||
...props
|
||||
}: Props) => {
|
||||
const firstFieldRef = React.useRef(null);
|
||||
|
|
@ -60,10 +64,17 @@ const MyPopover = ({
|
|||
autoFocus={false}
|
||||
>
|
||||
<PopoverTrigger>{Trigger}</PopoverTrigger>
|
||||
<PopoverContent {...props}>
|
||||
{hasArrow && <PopoverArrow />}
|
||||
{children({ onClose })}
|
||||
</PopoverContent>
|
||||
{isOpen && onBackdropClick && (
|
||||
<Portal>
|
||||
<Box position="fixed" zIndex={1000} inset={0} onClick={() => onBackdropClick()} />
|
||||
</Portal>
|
||||
)}
|
||||
<Portal>
|
||||
<PopoverContent zIndex={1001} {...props}>
|
||||
{hasArrow && <PopoverArrow />}
|
||||
{children({ onClose })}
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useMemo, useState, useTransition } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
|
|
@ -14,7 +14,7 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
||||
import VariableLabelPickerPlugin from './plugins/VariableLabelPickerPlugin';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import styles from './index.module.scss';
|
||||
import VariablePlugin from './plugins/VariablePlugin';
|
||||
import { VariableNode } from './plugins/VariablePlugin/node';
|
||||
|
|
@ -32,36 +32,47 @@ import VariableLabelPlugin from './plugins/VariableLabelPlugin';
|
|||
import { useDeepCompareEffect } from 'ahooks';
|
||||
import VariablePickerPlugin from './plugins/VariablePickerPlugin';
|
||||
|
||||
export type EditorProps = {
|
||||
variables?: EditorVariablePickerType[];
|
||||
variableLabels?: EditorVariableLabelPickerType[];
|
||||
value?: string;
|
||||
showOpenModal?: boolean;
|
||||
minH?: number;
|
||||
maxH?: number;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
isInvalid?: boolean;
|
||||
|
||||
ExtensionPopover?: ((e: {
|
||||
onChangeText: (text: string) => void;
|
||||
iconButtonStyle: Record<string, any>;
|
||||
}) => React.ReactNode)[];
|
||||
};
|
||||
|
||||
export default function Editor({
|
||||
minH = 200,
|
||||
maxH = 400,
|
||||
maxLength,
|
||||
showOpenModal = true,
|
||||
onOpenModal,
|
||||
variables,
|
||||
variableLabels,
|
||||
variables = [],
|
||||
variableLabels = [],
|
||||
onChange,
|
||||
onChangeText,
|
||||
onBlur,
|
||||
value,
|
||||
placeholder = '',
|
||||
bg = 'white',
|
||||
isInvalid,
|
||||
|
||||
isInvalid
|
||||
}: {
|
||||
minH?: number;
|
||||
maxH?: number;
|
||||
maxLength?: number;
|
||||
showOpenModal?: boolean;
|
||||
onOpenModal?: () => void;
|
||||
variables: EditorVariablePickerType[];
|
||||
variableLabels: EditorVariableLabelPickerType[];
|
||||
onChange?: (editorState: EditorState, editor: LexicalEditor) => void;
|
||||
onBlur?: (editor: LexicalEditor) => void;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
|
||||
isInvalid?: boolean;
|
||||
} & FormPropsType) {
|
||||
ExtensionPopover
|
||||
}: EditorProps &
|
||||
FormPropsType & {
|
||||
onOpenModal?: () => void;
|
||||
onChange: (editorState: EditorState, editor: LexicalEditor) => void;
|
||||
onChangeText?: ((text: string) => void) | undefined;
|
||||
onBlur: (editor: LexicalEditor) => void;
|
||||
}) {
|
||||
const [key, setKey] = useState(getNanoid(6));
|
||||
const [_, startSts] = useTransition();
|
||||
const [focus, setFocus] = useState(false);
|
||||
|
|
@ -81,6 +92,28 @@ export default function Editor({
|
|||
setKey(getNanoid(6));
|
||||
}, [value, variables, variableLabels]);
|
||||
|
||||
const showFullScreenIcon = useMemo(() => {
|
||||
return showOpenModal && scrollHeight > maxH;
|
||||
}, [showOpenModal, scrollHeight, maxH]);
|
||||
|
||||
const iconButtonStyle = useMemo(
|
||||
() => ({
|
||||
position: 'absolute' as const,
|
||||
bottom: 1,
|
||||
right: showFullScreenIcon ? '34px' : 2,
|
||||
zIndex: 10,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '6px',
|
||||
background: 'rgba(255, 255, 255, 0.01)',
|
||||
backdropFilter: 'blur(6.6666669845581055px)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
w: 6,
|
||||
h: 6
|
||||
}),
|
||||
[showFullScreenIcon]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="nowheel"
|
||||
|
|
@ -146,17 +179,15 @@ export default function Editor({
|
|||
<VariablePickerPlugin variables={variableLabels.length > 0 ? [] : variables} />
|
||||
<OnBlurPlugin onBlur={onBlur} />
|
||||
</LexicalComposer>
|
||||
{showOpenModal && scrollHeight > maxH && (
|
||||
<Box
|
||||
zIndex={10}
|
||||
position={'absolute'}
|
||||
bottom={-1}
|
||||
right={2}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenModal}
|
||||
>
|
||||
<MyIcon name={'common/fullScreenLight'} w={'14px'} color={'myGray.500'} />
|
||||
</Box>
|
||||
|
||||
{onChangeText &&
|
||||
ExtensionPopover?.map((Item, index) => (
|
||||
<Item key={index} iconButtonStyle={iconButtonStyle} onChangeText={onChangeText} />
|
||||
))}
|
||||
{showFullScreenIcon && (
|
||||
<Flex onClick={onOpenModal} {...iconButtonStyle} right={2}>
|
||||
<MyIcon name={'common/fullScreenLight'} w={'1rem'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,46 +1,29 @@
|
|||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box, Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { editorStateToText } from './utils';
|
||||
import type { EditorProps } from './Editor';
|
||||
import Editor from './Editor';
|
||||
import MyModal from '../../MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { EditorState, LexicalEditor } from 'lexical';
|
||||
import type { FormPropsType } from './type.d';
|
||||
import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type.d';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const PromptEditor = ({
|
||||
showOpenModal = true,
|
||||
variables = [],
|
||||
variableLabels = [],
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
minH,
|
||||
maxH,
|
||||
maxLength,
|
||||
placeholder,
|
||||
title,
|
||||
isInvalid,
|
||||
isDisabled,
|
||||
...props
|
||||
}: {
|
||||
showOpenModal?: boolean;
|
||||
variables?: EditorVariablePickerType[];
|
||||
variableLabels?: EditorVariableLabelPickerType[];
|
||||
value?: string;
|
||||
onChange?: (text: string) => void;
|
||||
onBlur?: (text: string) => void;
|
||||
minH?: number;
|
||||
maxH?: number;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
|
||||
isInvalid?: boolean;
|
||||
isDisabled?: boolean;
|
||||
} & FormPropsType) => {
|
||||
}: FormPropsType &
|
||||
EditorProps & {
|
||||
title?: string;
|
||||
isDisabled?: boolean;
|
||||
onChange?: (text: string) => void;
|
||||
onBlur?: (text: string) => void;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -69,19 +52,13 @@ const PromptEditor = ({
|
|||
<>
|
||||
<Box position="relative">
|
||||
<Editor
|
||||
{...props}
|
||||
showOpenModal={showOpenModal}
|
||||
onOpenModal={onOpen}
|
||||
variables={variables}
|
||||
variableLabels={variableLabels}
|
||||
minH={minH}
|
||||
maxH={maxH}
|
||||
maxLength={maxLength}
|
||||
value={formattedValue}
|
||||
onChange={onChangeInput}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlurInput}
|
||||
placeholder={placeholder}
|
||||
isInvalid={isInvalid}
|
||||
{...props}
|
||||
/>
|
||||
{isDisabled && (
|
||||
<Box
|
||||
|
|
@ -106,16 +83,14 @@ const PromptEditor = ({
|
|||
>
|
||||
<ModalBody>
|
||||
<Editor
|
||||
{...props}
|
||||
minH={400}
|
||||
maxH={400}
|
||||
maxLength={maxLength}
|
||||
showOpenModal={false}
|
||||
variables={variables}
|
||||
variableLabels={variableLabels}
|
||||
value={value}
|
||||
onChange={onChangeInput}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlurInput}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"AutoOptimize": "Automatic optimization",
|
||||
"Click_to_delete_this_field": "Click to delete this field",
|
||||
"Filed_is_deprecated": "This field is deprecated",
|
||||
"Index": "Index",
|
||||
|
|
@ -12,6 +13,13 @@
|
|||
"MCP_tools_url_is_empty": "The MCP address cannot be empty",
|
||||
"MCP_tools_url_placeholder": "After filling in the MCP address, click Analysis",
|
||||
"No_selected_dataset": "No selected dataset",
|
||||
"Optimizer_CloseConfirm": "Confirm to close",
|
||||
"Optimizer_CloseConfirmText": "Optimization results have been generated, confirming that closing will lose the current result. Will it continue?",
|
||||
"Optimizer_EmptyPrompt": "Please enter optimization requirements",
|
||||
"Optimizer_Generating": "Generating...",
|
||||
"Optimizer_Placeholder": "How do you want to write or optimize prompt words?",
|
||||
"Optimizer_Reoptimize": "Re-optimize",
|
||||
"Optimizer_Replace": "replace",
|
||||
"Role_setting": "Permission",
|
||||
"Run": "Execute",
|
||||
"Search_dataset": "Search dataset",
|
||||
|
|
|
|||
|
|
@ -1213,6 +1213,7 @@
|
|||
"support.wallet.usage.Bill Module": "Billing Module",
|
||||
"support.wallet.usage.Duration": "Duration (seconds)",
|
||||
"support.wallet.usage.Module name": "Module Name",
|
||||
"support.wallet.usage.Optimize Prompt": "Prompt word optimization",
|
||||
"support.wallet.usage.Source": "Source",
|
||||
"support.wallet.usage.Text Length": "Text Length",
|
||||
"support.wallet.usage.Time": "Generation Time",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"AutoOptimize": "自动优化",
|
||||
"Click_to_delete_this_field": "点击删除该字段",
|
||||
"Filed_is_deprecated": "该字段已弃用",
|
||||
"Index": "索引",
|
||||
|
|
@ -12,6 +13,13 @@
|
|||
"MCP_tools_url_is_empty": "MCP 地址不能为空",
|
||||
"MCP_tools_url_placeholder": "填入 MCP 地址后,点击解析",
|
||||
"No_selected_dataset": "未选择知识库",
|
||||
"Optimizer_CloseConfirm": "确认关闭",
|
||||
"Optimizer_CloseConfirmText": "已经生成了优化结果,确认关闭将丢失当前结果,是否继续?",
|
||||
"Optimizer_EmptyPrompt": "请输入优化要求",
|
||||
"Optimizer_Generating": "生成中…",
|
||||
"Optimizer_Placeholder": "你希望如何编写或优化提示词?",
|
||||
"Optimizer_Reoptimize": "重新优化",
|
||||
"Optimizer_Replace": "替换",
|
||||
"Role_setting": "权限设置",
|
||||
"Run": "运行",
|
||||
"Search_dataset": "搜索知识库",
|
||||
|
|
|
|||
|
|
@ -1214,6 +1214,7 @@
|
|||
"support.wallet.usage.Bill Module": "扣费模块",
|
||||
"support.wallet.usage.Duration": "时长(秒)",
|
||||
"support.wallet.usage.Module name": "模块名",
|
||||
"support.wallet.usage.Optimize Prompt": "提示词优化",
|
||||
"support.wallet.usage.Source": "来源",
|
||||
"support.wallet.usage.Text Length": "文本长度",
|
||||
"support.wallet.usage.Time": "生成时间",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"AutoOptimize": "自動優化",
|
||||
"Click_to_delete_this_field": "點擊刪除該字段",
|
||||
"Filed_is_deprecated": "該字段已棄用",
|
||||
"Index": "索引",
|
||||
|
|
@ -12,6 +13,13 @@
|
|||
"MCP_tools_url_is_empty": "MCP 地址不能為空",
|
||||
"MCP_tools_url_placeholder": "填入 MCP 地址後,點擊解析",
|
||||
"No_selected_dataset": "未選擇知識庫",
|
||||
"Optimizer_CloseConfirm": "確認關閉",
|
||||
"Optimizer_CloseConfirmText": "已經生成了優化結果,確認關閉將丟失當前結果,是否繼續?",
|
||||
"Optimizer_EmptyPrompt": "請輸入優化要求",
|
||||
"Optimizer_Generating": "生成中…",
|
||||
"Optimizer_Placeholder": "你希望如何編寫或優化提示詞?",
|
||||
"Optimizer_Reoptimize": "重新優化",
|
||||
"Optimizer_Replace": "替換",
|
||||
"Role_setting": "權限設定",
|
||||
"Run": "執行",
|
||||
"Search_dataset": "搜尋知識庫",
|
||||
|
|
|
|||
|
|
@ -1212,6 +1212,7 @@
|
|||
"support.wallet.usage.Bill Module": "計費模組",
|
||||
"support.wallet.usage.Duration": "時長(秒)",
|
||||
"support.wallet.usage.Module name": "模組名稱",
|
||||
"support.wallet.usage.Optimize Prompt": "提示詞優化",
|
||||
"support.wallet.usage.Source": "來源",
|
||||
"support.wallet.usage.Text Length": "文字長度",
|
||||
"support.wallet.usage.Time": "產生時間",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,326 @@
|
|||
import { useMemo, useRef, useState } from 'react';
|
||||
import type { FlexProps } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Textarea, useDisclosure } from '@chakra-ui/react';
|
||||
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useLocalStorageState } from 'ahooks';
|
||||
import AIModelSelector from '../../../Select/AIModelSelector';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { onOptimizePrompt } from '@/web/common/api/fetch';
|
||||
|
||||
export type OptimizerPromptProps = {
|
||||
onChangeText: (text: string) => void;
|
||||
defaultPrompt?: string;
|
||||
};
|
||||
|
||||
export type OnOptimizePromptProps = {
|
||||
originalPrompt?: string;
|
||||
input: string;
|
||||
model: string;
|
||||
onResult: (result: string) => void;
|
||||
abortController?: AbortController;
|
||||
};
|
||||
|
||||
const OptimizerPopover = ({
|
||||
onChangeText,
|
||||
iconButtonStyle,
|
||||
defaultPrompt
|
||||
}: OptimizerPromptProps & {
|
||||
iconButtonStyle?: FlexProps;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { llmModelList, defaultModels } = useSystemStore();
|
||||
|
||||
const [optimizerInput, setOptimizerInput] = useState('');
|
||||
const [optimizedResult, setOptimizedResult] = useState('');
|
||||
const [selectedModel = '', setSelectedModel] = useLocalStorageState<string>(
|
||||
'prompt-editor-selected-model',
|
||||
{
|
||||
defaultValue: defaultModels.llm?.model || ''
|
||||
}
|
||||
);
|
||||
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
||||
const { isOpen: isConfirmOpen, onOpen: onOpenConfirm, onClose: onCloseConfirm } = useDisclosure();
|
||||
|
||||
const closePopoverRef = useRef<() => void>();
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
return llmModelList.map((model) => {
|
||||
// const provider = getModelProvider(model.model)
|
||||
return {
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar
|
||||
src={model.avatar || HUGGING_FACE_ICON}
|
||||
fallbackSrc={HUGGING_FACE_ICON}
|
||||
mr={1.5}
|
||||
w={5}
|
||||
/>
|
||||
<Box fontWeight={'normal'} fontSize={'14px'} color={'myGray.900'}>
|
||||
{model.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
value: model.model
|
||||
};
|
||||
});
|
||||
}, [llmModelList]);
|
||||
|
||||
const isEmptyOptimizerInput = useMemo(() => {
|
||||
return !optimizerInput.trim();
|
||||
}, [optimizerInput]);
|
||||
|
||||
const { runAsync: handleSendOptimization, loading } = useRequest2(async (isAuto?: boolean) => {
|
||||
if (isEmptyOptimizerInput && !isAuto) return;
|
||||
|
||||
setOptimizedResult('');
|
||||
setOptimizerInput('');
|
||||
const controller = new AbortController();
|
||||
setAbortController(controller);
|
||||
|
||||
await onOptimizePrompt({
|
||||
originalPrompt: defaultPrompt,
|
||||
input: optimizerInput,
|
||||
model: selectedModel,
|
||||
onResult: (result: string) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setOptimizedResult((prev) => prev + result);
|
||||
}
|
||||
},
|
||||
abortController: controller
|
||||
});
|
||||
|
||||
setAbortController(null);
|
||||
});
|
||||
|
||||
const handleStopRequest = () => {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
setAbortController(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
||||
e.preventDefault();
|
||||
if (!loading) {
|
||||
handleSendOptimization();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyPopover
|
||||
Trigger={
|
||||
<Flex {...iconButtonStyle}>
|
||||
<MyIcon name={'optimizer'} w={'18px'} />
|
||||
</Flex>
|
||||
}
|
||||
trigger="click"
|
||||
placement={'auto'}
|
||||
w="482px"
|
||||
onBackdropClick={() => {
|
||||
if (optimizedResult) {
|
||||
onOpenConfirm();
|
||||
} else {
|
||||
closePopoverRef.current?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ onClose }) => {
|
||||
closePopoverRef.current = onClose;
|
||||
return (
|
||||
<Box p={optimizedResult ? 8 : 4}>
|
||||
{/* Result */}
|
||||
{optimizedResult && (
|
||||
<Box
|
||||
px={'10px'}
|
||||
maxHeight={'300px'}
|
||||
overflowY={'auto'}
|
||||
fontSize={'14px'}
|
||||
color={'gray.700'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-word'}
|
||||
mb={4}
|
||||
>
|
||||
{optimizedResult}
|
||||
</Box>
|
||||
)}
|
||||
{/* Button */}
|
||||
<Flex mb={3} alignItems={'center'} gap={3}>
|
||||
{!loading && (
|
||||
<>
|
||||
{!optimizedResult && !!defaultPrompt && (
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
size={'sm'}
|
||||
color={'myGray.600'}
|
||||
onClick={() => handleSendOptimization(true)}
|
||||
>
|
||||
{t('app:AutoOptimize')}
|
||||
</Button>
|
||||
)}
|
||||
{optimizedResult && (
|
||||
<>
|
||||
<Button
|
||||
variant={'primaryGhost'}
|
||||
size={'sm'}
|
||||
px={2}
|
||||
border={'0.5px solid'}
|
||||
color={'primary.600'}
|
||||
onClick={() => {
|
||||
onChangeText?.(optimizedResult);
|
||||
setOptimizedResult('');
|
||||
setOptimizerInput('');
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('app:Optimizer_Replace')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
size={'sm'}
|
||||
fontSize={'12px'}
|
||||
onClick={() => {
|
||||
setOptimizedResult('');
|
||||
handleSendOptimization();
|
||||
}}
|
||||
>
|
||||
{t('app:Optimizer_Reoptimize')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box flex={1} />
|
||||
{modelOptions && modelOptions.length > 0 && (
|
||||
<AIModelSelector
|
||||
borderColor={'transparent'}
|
||||
_hover={{
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.400'
|
||||
}}
|
||||
size={'sm'}
|
||||
value={selectedModel}
|
||||
list={modelOptions}
|
||||
onChange={setSelectedModel}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Input */}
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
gap={2}
|
||||
border={'1px solid'}
|
||||
borderColor={'gray.200'}
|
||||
borderRadius={'md'}
|
||||
p={2}
|
||||
mb={3}
|
||||
_focusWithin={{ borderColor: 'primary.600' }}
|
||||
>
|
||||
<MyIcon name={'optimizer'} alignSelf={'flex-start'} mt={0.5} w={5} />
|
||||
<Textarea
|
||||
placeholder={t('app:Optimizer_Placeholder')}
|
||||
resize={'none'}
|
||||
rows={1}
|
||||
minHeight={'24px'}
|
||||
lineHeight={'24px'}
|
||||
maxHeight={'96px'}
|
||||
overflowY={'hidden'}
|
||||
border={'none'}
|
||||
_focus={{
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
p={0}
|
||||
borderRadius={'none'}
|
||||
value={optimizerInput}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
isDisabled={loading}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
setOptimizerInput(e.target.value);
|
||||
|
||||
textarea.style.height = '24px';
|
||||
const maxHeight = 96;
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
|
||||
if (textarea.scrollHeight > maxHeight) {
|
||||
textarea.style.overflowY = 'auto';
|
||||
} else {
|
||||
textarea.style.overflowY = 'hidden';
|
||||
}
|
||||
}}
|
||||
flex={1}
|
||||
/>
|
||||
<MyIcon
|
||||
name={loading ? 'stop' : 'core/chat/sendLight'}
|
||||
w={'1rem'}
|
||||
alignSelf={'flex-end'}
|
||||
mb={1}
|
||||
color={loading || !isEmptyOptimizerInput ? 'primary.600' : 'gray.400'}
|
||||
cursor={loading || !isEmptyOptimizerInput ? 'pointer' : 'not-allowed'}
|
||||
onClick={() => {
|
||||
if (loading) {
|
||||
handleStopRequest();
|
||||
} else {
|
||||
void handleSendOptimization();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
</MyPopover>
|
||||
|
||||
<MyModal
|
||||
isOpen={isConfirmOpen}
|
||||
onClose={onCloseConfirm}
|
||||
title={t('app:Optimizer_CloseConfirm')}
|
||||
iconSrc={'common/confirm/deleteTip'}
|
||||
size="md"
|
||||
zIndex={2000}
|
||||
>
|
||||
<Box p={4}>
|
||||
<Box fontSize={'sm'} color={'myGray.700'} mb={4}>
|
||||
{t('app:Optimizer_CloseConfirmText')}
|
||||
</Box>
|
||||
<Flex justifyContent={'flex-end'} gap={3}>
|
||||
<Button variant={'whiteBase'} onClick={onCloseConfirm}>
|
||||
{t('common:Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'dangerFill'}
|
||||
onClick={() => {
|
||||
setOptimizedResult('');
|
||||
setOptimizerInput('');
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
setAbortController(null);
|
||||
}
|
||||
onCloseConfirm();
|
||||
closePopoverRef.current?.();
|
||||
}}
|
||||
>
|
||||
{t('app:Optimizer_CloseConfirm')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</MyModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptimizerPopover;
|
||||
|
|
@ -73,6 +73,7 @@ const InputRender = (props: InputRenderProps) => {
|
|||
maxLength={props.maxLength}
|
||||
minH={100}
|
||||
maxH={300}
|
||||
ExtensionPopover={props.ExtensionPopover}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
import type { InputTypeEnum } from './constant';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import type { EditorProps } from '@fastgpt/web/components/common/Textarea/PromptEditor/Editor';
|
||||
|
||||
type CommonRenderProps = {
|
||||
placeholder?: string;
|
||||
|
|
@ -18,14 +19,16 @@ type CommonRenderProps = {
|
|||
} & Omit<BoxProps, 'onChange' | 'list' | 'value'>;
|
||||
|
||||
type SpecificProps =
|
||||
| {
|
||||
| ({
|
||||
// input & textarea
|
||||
inputType: InputTypeEnum.input | InputTypeEnum.textarea;
|
||||
variables?: EditorVariablePickerType[];
|
||||
variableLabels?: EditorVariableLabelPickerType[];
|
||||
title?: string;
|
||||
maxLength?: number;
|
||||
}
|
||||
} & {
|
||||
ExtensionPopover?: EditorProps['ExtensionPopover'];
|
||||
})
|
||||
| {
|
||||
// numberInput
|
||||
inputType: InputTypeEnum.numberInput;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useMemo, useTransition } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useTransition } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
|
|
@ -31,6 +31,8 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
|||
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
import ToolSelect from './components/ToolSelect';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
|
||||
|
|
@ -69,6 +71,7 @@ const EditForm = ({
|
|||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const selectDatasets = useMemo(() => appForm?.dataset?.datasets, [appForm]);
|
||||
const [, startTst] = useTransition();
|
||||
const { llmModelList, defaultModels } = useSystemStore();
|
||||
|
||||
const {
|
||||
isOpen: isOpenDatasetSelect,
|
||||
|
|
@ -126,6 +129,27 @@ const EditForm = ({
|
|||
}
|
||||
}, [selectedModel, setAppForm]);
|
||||
|
||||
const OptimizerPopverComponent = useCallback(
|
||||
({ iconButtonStyle }: { iconButtonStyle: Record<string, any> }) => {
|
||||
return (
|
||||
<OptimizerPopover
|
||||
iconButtonStyle={iconButtonStyle}
|
||||
defaultPrompt={appForm.aiSettings.systemPrompt}
|
||||
onChangeText={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
aiSettings: {
|
||||
...state.aiSettings,
|
||||
systemPrompt: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[appForm.aiSettings.systemPrompt, setAppForm]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
|
|
@ -196,6 +220,7 @@ const EditForm = ({
|
|||
variables={formatVariables}
|
||||
placeholder={t('common:core.app.tip.systemPromptTip')}
|
||||
title={t('common:core.ai.Prompt')}
|
||||
ExtensionPopover={[OptimizerPopverComponent]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { getEditorVariables } from '@/pageComponents/app/detail/WorkflowComponen
|
|||
import { InputTypeEnum } from '@/components/core/app/formRender/constant';
|
||||
import { llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
|
||||
import { getWebDefaultLLMModel } from '@/web/common/system/utils';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
|
||||
|
||||
const CommonInputForm = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -80,6 +82,22 @@ const CommonInputForm = ({ item, nodeId }: RenderInputProps) => {
|
|||
return item.value;
|
||||
}, [inputType, item.value, defaultModel, handleChange]);
|
||||
|
||||
const canOptimizePrompt = item.key === NodeInputKeyEnum.aiSystemPrompt;
|
||||
const OptimizerPopverComponent = useCallback(
|
||||
({ iconButtonStyle }: { iconButtonStyle: Record<string, any> }) => {
|
||||
return (
|
||||
<OptimizerPopover
|
||||
iconButtonStyle={iconButtonStyle}
|
||||
defaultPrompt={item.value}
|
||||
onChangeText={(e) => {
|
||||
handleChange(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[item.value, handleChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<InputRender
|
||||
inputType={inputType}
|
||||
|
|
@ -93,6 +111,7 @@ const CommonInputForm = ({ item, nodeId }: RenderInputProps) => {
|
|||
max={item.max}
|
||||
list={item.list}
|
||||
modelList={modelList}
|
||||
ExtensionPopover={canOptimizePrompt ? [OptimizerPopverComponent] : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { MongoAppChatLog } from '@fastgpt/service/core/app/logs/chatLogsSchema';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { addLog } from '@fastgpt/service/common/system/log';
|
||||
import type { ChatSchemaType } from '@fastgpt/global/core/chat/type';
|
||||
|
||||
export type SyncAppChatLogQuery = {};
|
||||
|
||||
export type SyncAppChatLogBody = {
|
||||
batchSize?: number;
|
||||
};
|
||||
|
||||
export type SyncAppChatLogResponse = {};
|
||||
|
||||
/*
|
||||
将 chats 表全部扫一遍,来获取统计数据
|
||||
*/
|
||||
async function handler(
|
||||
req: ApiRequestProps<SyncAppChatLogBody, SyncAppChatLogQuery>,
|
||||
res: ApiResponseType<SyncAppChatLogResponse>
|
||||
) {
|
||||
await authCert({ req, authRoot: true });
|
||||
|
||||
const { batchSize = 10 } = req.body;
|
||||
|
||||
console.log('开始同步AppChatLog数据...');
|
||||
console.log(`批处理大小: ${batchSize}`);
|
||||
|
||||
let success = 0;
|
||||
const total = await MongoChat.countDocuments({});
|
||||
console.log(`总共需要处理的chat记录数: ${total}`);
|
||||
|
||||
res.json({
|
||||
data: '同步任务已开始,可在日志中看到进度'
|
||||
});
|
||||
|
||||
while (true) {
|
||||
console.log(`对话同步处理进度: ${success}/${total}`);
|
||||
|
||||
try {
|
||||
const chats = await MongoChat.find({
|
||||
initStatistics: { $exists: false }
|
||||
})
|
||||
.sort({ _id: -1 })
|
||||
.limit(batchSize)
|
||||
.lean();
|
||||
|
||||
if (chats.length === 0) break;
|
||||
|
||||
const result = await Promise.allSettled(chats.map((chat) => processChatRecord(chat)));
|
||||
success += result.filter((r) => r.status === 'fulfilled').length;
|
||||
} catch (error) {
|
||||
addLog.error('处理chat记录失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('同步对话完成');
|
||||
}
|
||||
|
||||
async function processChatRecord(chat: ChatSchemaType) {
|
||||
async function calculateChatItemStats(chatId: string) {
|
||||
const chatItems = await MongoChatItem.find({ chatId }).lean();
|
||||
|
||||
let chatItemCount = chatItems.length;
|
||||
let errorCount = 0;
|
||||
let totalPoints = 0;
|
||||
let goodFeedbackCount = 0;
|
||||
let badFeedbackCount = 0;
|
||||
let totalResponseTime = 0;
|
||||
|
||||
for (const item of chatItems) {
|
||||
const itemData = item as any;
|
||||
|
||||
if (itemData.userGoodFeedback && itemData.userGoodFeedback.trim() !== '') {
|
||||
goodFeedbackCount++;
|
||||
}
|
||||
if (itemData.userBadFeedback && itemData.userBadFeedback.trim() !== '') {
|
||||
badFeedbackCount++;
|
||||
}
|
||||
|
||||
if (itemData.durationSeconds) {
|
||||
totalResponseTime += itemData.durationSeconds;
|
||||
} else if (
|
||||
itemData[DispatchNodeResponseKeyEnum.nodeResponse] &&
|
||||
Array.isArray(itemData[DispatchNodeResponseKeyEnum.nodeResponse])
|
||||
) {
|
||||
for (const response of itemData[DispatchNodeResponseKeyEnum.nodeResponse]) {
|
||||
if (response.runningTime) {
|
||||
totalResponseTime += response.runningTime / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
itemData[DispatchNodeResponseKeyEnum.nodeResponse] &&
|
||||
Array.isArray(itemData[DispatchNodeResponseKeyEnum.nodeResponse])
|
||||
) {
|
||||
for (const response of itemData[DispatchNodeResponseKeyEnum.nodeResponse]) {
|
||||
if (response.errorText) {
|
||||
errorCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.totalPoints) {
|
||||
totalPoints += response.totalPoints;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chatItemCount,
|
||||
errorCount,
|
||||
totalPoints,
|
||||
goodFeedbackCount,
|
||||
badFeedbackCount,
|
||||
totalResponseTime
|
||||
};
|
||||
}
|
||||
|
||||
async function checkIsFirstChat(chat: any): Promise<boolean> {
|
||||
const earliestChat = await MongoChat.findOne(
|
||||
{
|
||||
userId: chat.userId,
|
||||
appId: chat.appId
|
||||
},
|
||||
{},
|
||||
{ sort: { createTime: 1 } }
|
||||
).lean();
|
||||
|
||||
return earliestChat?._id.toString() === chat._id.toString();
|
||||
}
|
||||
|
||||
const chatItemStats = await calculateChatItemStats(chat.chatId);
|
||||
const isFirstChat = await checkIsFirstChat(chat);
|
||||
|
||||
const chatLogData = {
|
||||
appId: chat.appId,
|
||||
teamId: chat.teamId,
|
||||
chatId: chat.chatId,
|
||||
userId: String(chat.outLinkUid || chat.tmbId),
|
||||
source: chat.source,
|
||||
sourceName: chat.sourceName,
|
||||
createTime: chat.createTime,
|
||||
updateTime: chat.updateTime,
|
||||
chatItemCount: chatItemStats.chatItemCount,
|
||||
errorCount: chatItemStats.errorCount,
|
||||
totalPoints: chatItemStats.totalPoints,
|
||||
goodFeedbackCount: chatItemStats.goodFeedbackCount,
|
||||
badFeedbackCount: chatItemStats.badFeedbackCount,
|
||||
totalResponseTime: chatItemStats.totalResponseTime,
|
||||
isFirstChat
|
||||
};
|
||||
|
||||
await MongoAppChatLog.updateOne(
|
||||
{ appId: chat.appId, chatId: chat.chatId },
|
||||
{ $set: chatLogData },
|
||||
{ upsert: true }
|
||||
);
|
||||
await MongoChat.updateOne({ _id: chat._id }, { $set: { initStatistics: true } });
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { responseWrite } from '@fastgpt/service/common/response';
|
||||
import { sseErrRes } from '@fastgpt/service/common/response';
|
||||
import { createChatCompletion } from '@fastgpt/service/core/ai/config';
|
||||
import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { loadRequestMessages } from '@fastgpt/service/core/chat/utils';
|
||||
import { llmCompletionsBodyFormat, parseLLMStreamResponse } from '@fastgpt/service/core/ai/utils';
|
||||
import { countGptMessagesTokens } from '@fastgpt/service/common/string/tiktoken/index';
|
||||
import { formatModelChars2Points } from '@fastgpt/service/support/wallet/usage/utils';
|
||||
import { createUsage } from '@fastgpt/service/support/wallet/usage/controller';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import { addLog } from '@fastgpt/service/common/system/log';
|
||||
|
||||
type OptimizePromptBody = {
|
||||
originalPrompt: string;
|
||||
optimizerInput: string;
|
||||
model: string;
|
||||
};
|
||||
|
||||
const getPromptOptimizerSystemPrompt = () => {
|
||||
return `# Role
|
||||
Prompt工程师
|
||||
|
||||
## Skills
|
||||
- 了解LLM的技术原理和局限性,包括它的训练数据、构建方式等,以便更好地设计Prompt
|
||||
- 具有丰富的自然语言处理经验,能够设计出符合语法、语义的高质量Prompt
|
||||
- 迭代优化能力强,能通过不断调整和测试Prompt的表现,持续改进Prompt质量
|
||||
- 能结合具体业务需求设计Prompt,使LLM生成的内容符合业务要求
|
||||
- 擅长分析用户需求,设计结构清晰、逻辑严谨的Prompt框架
|
||||
|
||||
## Goals
|
||||
- 分析用户的Prompt,理解其核心需求和意图
|
||||
- 设计一个结构清晰、符合逻辑的Prompt框架
|
||||
- 生成高质量的结构化Prompt
|
||||
- 提供针对性的优化建议
|
||||
|
||||
## Constrains
|
||||
- 确保所有内容符合各个学科的最佳实践
|
||||
- 在任何情况下都不要跳出角色
|
||||
- 不要胡说八道和编造事实
|
||||
- 保持专业性和准确性
|
||||
- 输出必须包含优化建议部分
|
||||
|
||||
## Suggestions
|
||||
- 深入分析用户原始Prompt的核心意图,避免表面理解
|
||||
- 采用结构化思维,确保各个部分逻辑清晰且相互呼应
|
||||
- 优先考虑实用性,生成的Prompt应该能够直接使用
|
||||
- 注重细节完善,每个部分都要有具体且有价值的内容
|
||||
- 保持专业水准,确保输出的Prompt符合行业最佳实践
|
||||
- **特别注意**:Suggestions部分应该专注于角色内在的工作方法,而不是与用户互动的策略`;
|
||||
};
|
||||
|
||||
const getPromptOptimizerUserPrompt = (originalPrompt: string, optimizerInput: string) => {
|
||||
return `请严格遵循用户的优化需求:
|
||||
<OptimizerInput>
|
||||
${optimizerInput}
|
||||
</OptimizerInput>
|
||||
|
||||
分析并优化以下Prompt,将其转化为结构化的高质量Prompt:
|
||||
<OriginalPrompt>
|
||||
${originalPrompt}
|
||||
</OriginalPrompt>
|
||||
|
||||
## 注意事项:
|
||||
- 直接输出优化后的Prompt,不要添加解释性文字,不要用代码块包围
|
||||
- 每个部分都要有具体内容,不要使用占位符
|
||||
- **数量要求**:Skills、Goals、Constrains、Workflow、Suggestions各部分需要5个要点,OutputFormat需要3个要点
|
||||
- **Suggestions是给角色的内在工作方法论**,专注于角色自身的技能提升和工作优化方法,避免涉及与用户互动的建议
|
||||
- **必须包含完整结构**:确保包含Role、Background、Attention、Profile、Skills、Goals、Constrains、Workflow、OutputFormat、Suggestions、Initialization等所有部分
|
||||
- 保持内容的逻辑性和连贯性,各部分之间要相互呼应`;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<OptimizePromptBody>, res: ApiResponseType) {
|
||||
try {
|
||||
const { originalPrompt, optimizerInput, model } = req.body;
|
||||
|
||||
const { teamId, tmbId } = await authCert({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: getPromptOptimizerSystemPrompt()
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: getPromptOptimizerUserPrompt(originalPrompt, optimizerInput)
|
||||
}
|
||||
];
|
||||
|
||||
const requestMessages = await loadRequestMessages({
|
||||
messages,
|
||||
useVision: false
|
||||
});
|
||||
|
||||
const { response, isStreamResponse } = await createChatCompletion({
|
||||
body: llmCompletionsBodyFormat(
|
||||
{
|
||||
model,
|
||||
messages: requestMessages,
|
||||
temperature: 0.1,
|
||||
max_tokens: 2000,
|
||||
stream: true
|
||||
},
|
||||
model
|
||||
)
|
||||
});
|
||||
|
||||
const { inputTokens, outputTokens } = await (async () => {
|
||||
if (isStreamResponse) {
|
||||
const { parsePart, getResponseData } = parseLLMStreamResponse();
|
||||
|
||||
let optimizedText = '';
|
||||
|
||||
for await (const part of response) {
|
||||
const { responseContent } = parsePart({
|
||||
part,
|
||||
parseThinkTag: true,
|
||||
retainDatasetCite: false
|
||||
});
|
||||
|
||||
if (responseContent) {
|
||||
optimizedText += responseContent;
|
||||
responseWrite({
|
||||
res,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: responseContent
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { content: answer, usage } = getResponseData();
|
||||
return {
|
||||
content: answer,
|
||||
inputTokens: usage?.prompt_tokens || (await countGptMessagesTokens(requestMessages)),
|
||||
outputTokens:
|
||||
usage?.completion_tokens ||
|
||||
(await countGptMessagesTokens([{ role: 'assistant', content: optimizedText }]))
|
||||
};
|
||||
} else {
|
||||
const usage = response.usage;
|
||||
const content = response.choices?.[0]?.message?.content || '';
|
||||
|
||||
responseWrite({
|
||||
res,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
content,
|
||||
inputTokens: usage?.prompt_tokens || (await countGptMessagesTokens(requestMessages)),
|
||||
outputTokens:
|
||||
usage?.completion_tokens ||
|
||||
(await countGptMessagesTokens([{ role: 'assistant', content: content }]))
|
||||
};
|
||||
}
|
||||
})();
|
||||
responseWrite({
|
||||
res,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: '[DONE]'
|
||||
});
|
||||
|
||||
const { totalPoints, modelName } = formatModelChars2Points({
|
||||
model,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
modelType: ModelTypeEnum.llm
|
||||
});
|
||||
|
||||
createUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
appName: i18nT('common:support.wallet.usage.Optimize Prompt'),
|
||||
totalPoints,
|
||||
source: UsageSourceEnum.optimize_prompt,
|
||||
list: [
|
||||
{
|
||||
moduleName: i18nT('common:support.wallet.usage.Optimize Prompt'),
|
||||
amount: totalPoints,
|
||||
model: modelName,
|
||||
inputTokens,
|
||||
outputTokens
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error: any) {
|
||||
addLog.error('Optimize prompt error', error);
|
||||
sseErrRes(res, error);
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { type ChatHistoryItemResType, type ChatSchema } from '@fastgpt/global/core/chat/type';
|
||||
import { type ChatHistoryItemResType, type ChatSchemaType } from '@fastgpt/global/core/chat/type';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { type AuthModeType } from '@fastgpt/service/support/permission/type';
|
||||
import { authOutLink } from './outLink';
|
||||
|
|
@ -51,7 +51,7 @@ export async function authChatCrud({
|
|||
teamId: string;
|
||||
tmbId: string;
|
||||
uid: string;
|
||||
chat?: ChatSchema;
|
||||
chat?: ChatSchemaType;
|
||||
responseDetail: boolean;
|
||||
showNodeStatus: boolean;
|
||||
showRawSource: boolean;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { ChatSchema } from '@fastgpt/global/core/chat/type';
|
||||
import type { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { ChatModelType } from '@/constants/model';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
|
|||
import { useSystemStore } from '../system/useSystemStore';
|
||||
import { formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
import type { OnOptimizePromptProps } from '@/components/common/PromptEditor/OptimizerPopover';
|
||||
|
||||
type StreamFetchProps = {
|
||||
url?: string;
|
||||
|
|
@ -272,3 +273,27 @@ export const streamFetch = ({
|
|||
failedFinish(err);
|
||||
}
|
||||
});
|
||||
|
||||
export const onOptimizePrompt = async ({
|
||||
originalPrompt,
|
||||
model,
|
||||
input,
|
||||
onResult,
|
||||
abortController
|
||||
}: OnOptimizePromptProps) => {
|
||||
const controller = abortController || new AbortController();
|
||||
await streamFetch({
|
||||
url: '/api/core/ai/optimizePrompt',
|
||||
data: {
|
||||
originalPrompt,
|
||||
optimizerInput: input,
|
||||
model
|
||||
},
|
||||
onMessage: ({ event, text }) => {
|
||||
if (event === SseResponseEventEnum.answer && text) {
|
||||
onResult(text);
|
||||
}
|
||||
},
|
||||
abortCtrl: controller
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export const useSystemStore = create<State>()(
|
|||
return null;
|
||||
},
|
||||
|
||||
gitStar: 20000,
|
||||
gitStar: 25000,
|
||||
async loadGitStar() {
|
||||
if (!get().feConfigs?.show_git) return;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"]
|
||||
}
|
||||
Loading…
Reference in New Issue