mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-08 01:08:43 +08:00
7506a147e6
* batch node (#6732) * batch node * docs: add local code quality standards and style guides for automated review * refactor: remove enforced minimum for parallel concurrency, simplify edge handling in task runtime context, and fix loop output mapping * feat: auto-infer and sync valueType for parallel loop input and output based on referenced array source * fix: refactor parallelRun output type synchronization and improve sub-workflow error handling in dispatch service * feat: enforce parallel concurrency limits and validate against workflow loop constraints * feat: implement retry mechanism for parallel workflow tasks with usage tracking per attempt * fix review * perf: use function * refactor: abstract nested node logic into useNestedNode hook and update parallelRun icon/service logic * fix: type import * refactor: update ParallelRunStatusEnum and i18n labels for improved status clarity * feat: parallel run details and input/output display to chat response modal and service dispatch * fix: config limit error * refactor: optimize parallel run task execution, fix point accumulation, and improve error handling for sub-workflows * fix: include totalPoints in parallel task results * refactor: centralize nested input injection and point safety utilities for workflow dispatchers * test: add unit tests for safePoints utility function * refactor: update parallel workflow runtime types and clean up docstring placement in dispatch utils * fix: include all runtime nodes in parallel execution to ensure variable reference accessibility * refactor: update pushSubWorkflowUsage signature to use object parameter for improved consistency --------- Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com> * feat(s3): add proxy transfer mode with tokenized upload/download (#6729) * feat(s3): add proxy transfer mode with tokenized upload/download * wip: switch to proxy mode for upload progress * fix: office mime types * fix(s3): upload MIME validation, multer whitelist, API error status - Treat AVI/MPEG mime aliases (incl. video/mp1s vs video/mpeg) as matching - Optional allowedExtensions on multer for dataset images and localFile - Map S3/business errors to 4xx in jsonRes where appropriate - Align presign max size with team plan; fix dataset import size UX - Add upload validation tests Made-with: Cursor * fix: show clear message when upload frequency limit is exceeded - Reject ERROR_ENUM.uploadFileIntervalLimit from authFrequencyLimit instead of Mongo doc - Add i18n for upload_file_interval_limit (zh-CN/en/zh-Hant) Made-with: Cursor * fix file token validation and upload mime checks * fix: test * fix(s3): treat m4a audio/mp4 and audio/x-m4a as equivalent - Add MIME equivalence group for AAC/M4A container mismatch (mime-types vs file-type) - Add upload validation test for minimal ftyp/M4A buffer - Test env: keep FILE_TOKEN_KEY in vitest test.env and test/setup.ts (drop loadTestEnv file) Made-with: Cursor * fix(chat): 调试区文件类型与编辑态一致,并修复 accept 在 WebKit 下不更新 - ChatTest: 用 getAppChatConfig + getGuideModule 合并画布引导节点与 chatConfig - useChatTest: 依赖 fileSelectConfig 序列化与 chatConfig,避免深层变更未触发预览更新 - useSelectFile: 用 useCallback + input key 替代 useMemoizedFn,确保 accept 变更后重建 input Made-with: Cursor * fix: invalid request * feat: prompt inject (#6757) * feat: resume chat stream (#6722) * fix: openapi schema issue while creating openapi json * feat: resume chat stream * wip: chat status and read status * feat: sync chat side bar status * fix: allow reassignment of variables in chatTest handler Made-with: Cursor * feat(chat): stream resume hardening, resume modules in @fastgpt/service, stale generating cron - Move stream resume mirror + resumeStatus into packages/service; update API imports - chatTest: ensurePendingChatRoundItems, default responseChatItemId; zod default import for client - useChatTest + HomeChatWindow: enableAutoResume and sync init chatGenerateStatus - ChatContext: safe no-op defaults without provider - Cron: clean MongoChat stuck in generating >30min; timer lock cleanStaleGeneratingChat Made-with: Cursor * fix(chat): address stream-resume PR review (zod/mongoose enum, legacy status, upsert, UI race) - Zod: use z.nativeEnum(ChatGenerateStatusEnum); mongoose chatGenerateStatus enum as [0,1,2] only - Init APIs: default missing chatGenerateStatus to done before read/unread logic - ensurePendingChatRoundItems: unique index + upsert; rename ChatGenerateStatusEnum - ChatBox auto-resume: guard by chatId; sidebar sync via targetChatId - Tests: chat history/feedback APIs pass with schema fixes Made-with: Cursor * fix(chat): expose resume at /api/v2/chat/resume; openapi + review tidy - Move handler from v1/stream to v2/chat/resume (pairs with v2 completions + Redis mirror) - Update fetch, OpenAPI AIPath, comments; remove slim projects/app global chat api - getHistoryStatus default chatGenerateStatus; team init + chatTest notes; ChatItem tweak Made-with: Cursor * fix(chat): fix resume JSON parse catch shadowing; drop unused resumeChatStream Made-with: Cursor * docs(chat): comment closed+stream mirror write path in workflow dispatch Made-with: Cursor * refactor: unify resumable stream mirroring * fix: keep v1 chat completions out of resume flow * refactor: make prepared chat rounds transactional * fix: handle resume stream terminal errors * fix: rerank max token * feat(workflow): extend variable update node with Number/Boolean/Array operations (#6752) * feat(workflow): extend variable update node with Number/Boolean/Array ops * feat: math operator icons and refactor variable update renderers for improved layout and consistency * chore(workflow): clean up variable update types and restore icon cleanup * feat: add test * fix:md_ascii_bug (#6755) * md_ascii_bug * md_ascii_bug * md_ascii_bug * md_ascii_bug * md_ascii_bug * perf: test --------- Co-authored-by: archer <545436317@qq.com> * doc * del dataset * perf: date auto coerce * doc * add test * perf: channel setting * doc * fix: chat resume stream (#6759) * refactor(api): move stream resume to /api/core/chat/resume Relocate resume handler from pages/api/v2 to pages/api/core, update OpenAPI paths, frontend streamResumeFetch URL, tests, and comments. Made-with: Cursor * fix: remove stray conflict markers; use z.nativeEnum for chatGenerateStatus Made-with: Cursor * fix: use enum instead of nativeEnum * fix(chat): address resume review suggestions * fix(chat): require sse when resuming generating chats * revert(chat): keep chatitem dataId index non-unique * fix: ts * fix doc * fix(chat): gate stream resume mirror by header (#6760) * fix: remove stray conflict markers; use z.nativeEnum for chatGenerateStatus Made-with: Cursor * fix: use enum instead of nativeEnum * fix(chat): address resume review suggestions * fix(chat): require sse when resuming generating chats * feat(chat): gate stream resume mirror by header * refactor(chat): decouple resume mirror header parsing * perf: dataset queue * fix: multipleselect * perf: workflow bug * doc * doc * perf: deploy yml;fix: child nodes watch * adapt embedding model defaultconfig * install shell * add mcp zod check * feat: http tool zod schema * Feat/batch UI (#6763) * feat: aggregate parallel run results into task-specific virtual nodes and update UI to support i18n arguments for module names * style: update workflow node card padding and table styling for improved layout consistency * feat: implement parallel run workflow node with documentation and i18n support * style(modal): WholeResponseModal UI and layout styling * chore: improve chat resume UX (#6764) * fix: remove stray conflict markers; use z.nativeEnum for chatGenerateStatus Made-with: Cursor * fix: use enum instead of nativeEnum * fix(chat): address resume review suggestions * fix(chat): require sse when resuming generating chats * feat(chat): gate stream resume mirror by header * refactor(chat): decouple resume mirror header parsing * feat: improve stream resume fallback * feat: block duplicate chat generation * feat: polish resume unavailable recovery * test: stabilize resume stream timeout * fix: harden resume wait flow * fix: get mcp tool raw schema * style: update UI styling and layout for LLM request detail and response modals * perf: http tool * fix: test * fix: http raw schema * fix: test * deploy yml * deploy yml --------- Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com> Co-authored-by: Ryo <whoeverimf5@gmail.com> Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
275 lines
7.4 KiB
TypeScript
275 lines
7.4 KiB
TypeScript
import crypto from 'crypto';
|
||
import { customAlphabet } from 'nanoid';
|
||
import path from 'path';
|
||
import { getErrText } from '../error/utils';
|
||
|
||
export const checkStrOversize = (str: string, size = 1e8) => {
|
||
if (str.length > size) {
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/* check string is a web link */
|
||
export function strIsLink(str?: string) {
|
||
if (!str) return false;
|
||
if (/^((http|https)?:\/\/|www\.|\/)[^\s/$.?#].[^\s]*$/i.test(str)) return true;
|
||
return false;
|
||
}
|
||
|
||
/* hash string */
|
||
export const hashStr = (str: string) => {
|
||
return crypto.createHash('sha256').update(str).digest('hex');
|
||
};
|
||
|
||
/* simple text, remove chinese space and extra \n */
|
||
export const simpleText = (text = '') => {
|
||
text = text.trim();
|
||
text = text.replace(/([\u4e00-\u9fa5])[\s&&[^\n]]+([\u4e00-\u9fa5])/g, '$1$2');
|
||
text = text.replace(/\r\n|\r/g, '\n');
|
||
text = text.replace(/\n{3,}/g, '\n\n');
|
||
text = text.replace(/[\s&&[^\n]]{2,}/g, ' ');
|
||
text = text.replace(/[\x00-\x08]/g, ' ');
|
||
|
||
return text;
|
||
};
|
||
|
||
export const valToStr = (val: any) => {
|
||
if (val === undefined) return '';
|
||
if (val === null) return 'null';
|
||
|
||
if (typeof val === 'object') {
|
||
try {
|
||
const start = Date.now();
|
||
const res = JSON.stringify(val);
|
||
|
||
if (Date.now() - start > 1000) {
|
||
console.warn('Slow JSON.stringify', {
|
||
duration: Date.now() - start,
|
||
valLength: res.length
|
||
});
|
||
}
|
||
|
||
return res;
|
||
} catch (error) {
|
||
console.error('Failed to stringify value', { error });
|
||
return `Failed to stringify value: ${getErrText(error)}`;
|
||
}
|
||
}
|
||
|
||
return String(val);
|
||
};
|
||
|
||
// replace {{variable}} to value
|
||
export function replaceVariable(
|
||
text: any,
|
||
obj: Record<string, string | number | undefined>,
|
||
depth = 0
|
||
) {
|
||
if (typeof text !== 'string') return text;
|
||
if (checkStrOversize(text)) {
|
||
throw new Error('Text length exceeds 100,000,000 characters.');
|
||
}
|
||
|
||
const MAX_REPLACEMENT_DEPTH = 10;
|
||
const processedVariables = new Set<string>();
|
||
|
||
// Prevent infinite recursion
|
||
if (depth > MAX_REPLACEMENT_DEPTH) {
|
||
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 */
|
||
export const replaceSensitiveText = (text: string) => {
|
||
// 1. http link
|
||
text = text.replace(/(?<=https?:\/\/)[^\s]+/g, 'xxx');
|
||
// 2. nx-xxx 全部替换成xxx
|
||
text = text.replace(/ns-[\w-]+/g, 'xxx');
|
||
|
||
return text;
|
||
};
|
||
|
||
/* Make sure the first letter is definitely lowercase */
|
||
export const getNanoid = (size = 16) => {
|
||
const firstChar = customAlphabet('abcdefghijklmnopqrstuvwxyz', 1)();
|
||
|
||
if (size === 1) return firstChar;
|
||
|
||
const randomsStr = customAlphabet(
|
||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
|
||
size - 1
|
||
)();
|
||
|
||
return `${firstChar}${randomsStr}`;
|
||
};
|
||
export const customNanoid = (str: string, size: number) => customAlphabet(str, size)();
|
||
|
||
/* Custom text to reg, need to replace special chats */
|
||
export const replaceRegChars = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
||
/* slice json str */
|
||
export const sliceJsonStr = (str: string) => {
|
||
str = str.trim();
|
||
|
||
// Find first opening bracket
|
||
let start = -1;
|
||
let openChar = '';
|
||
|
||
for (let i = 0; i < str.length; i++) {
|
||
if (str[i] === '{' || str[i] === '[') {
|
||
start = i;
|
||
openChar = str[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (start === -1) return str;
|
||
|
||
// Find matching closing bracket from the end
|
||
const closeChar = openChar === '{' ? '}' : ']';
|
||
|
||
for (let i = str.length - 1; i >= start; i--) {
|
||
const ch = str[i];
|
||
|
||
if (ch === closeChar) {
|
||
return str.slice(start, i + 1);
|
||
}
|
||
}
|
||
|
||
return str;
|
||
};
|
||
|
||
export const sliceStrStartEnd = (str: string | null = '', start: number, end: number) => {
|
||
if (!str) return '';
|
||
|
||
const overSize = str.length > start + end;
|
||
|
||
if (!overSize) return str;
|
||
|
||
const startContent = str.slice(0, start);
|
||
const endContent = overSize ? str.slice(-end) : '';
|
||
|
||
return `${startContent}${overSize ? `\n\n...[hide ${str.length - start - end} chars]...\n\n` : ''}${endContent}`;
|
||
};
|
||
|
||
/*
|
||
Parse file extension from url
|
||
Test:
|
||
1. https://xxx.com/file.pdf?token=123
|
||
=> pdf
|
||
2. https://xxx.com/file.pdf
|
||
=> pdf
|
||
*/
|
||
export const parseFileExtensionFromUrl = (url = '') => {
|
||
// Prefer explicit filename in query params for proxy links:
|
||
// e.g. /api/system/file/download/<token>?filename=image.jpg
|
||
try {
|
||
const parsedUrl = new URL(url, 'http://localhost');
|
||
const queryFilename =
|
||
parsedUrl.searchParams.get('filename') || parsedUrl.searchParams.get('name');
|
||
if (queryFilename) {
|
||
const extFromQuery = path.extname(decodeURIComponent(queryFilename));
|
||
if (extFromQuery.startsWith('.')) {
|
||
return extFromQuery.slice(1).toLowerCase();
|
||
}
|
||
}
|
||
} catch {
|
||
// noop
|
||
// fallback to legacy parser below
|
||
}
|
||
|
||
// Remove query params and hash first
|
||
const urlWithoutQuery = url.split('?')[0].split('#')[0];
|
||
const extension = path.extname(urlWithoutQuery);
|
||
// path.extname returns '.ext' or ''
|
||
if (extension.startsWith('.')) {
|
||
return extension.slice(1).toLowerCase();
|
||
}
|
||
return '';
|
||
};
|
||
|
||
export const formatNumberWithUnit = (num: number, locale: string = 'zh-CN'): string => {
|
||
if (num === 0) return '0';
|
||
if (!num || isNaN(num)) return '-';
|
||
const absNum = Math.abs(num);
|
||
const isNegative = num < 0;
|
||
const prefix = isNegative ? '-' : '';
|
||
|
||
if (locale === 'zh-CN') {
|
||
if (absNum >= 10000) {
|
||
const value = absNum / 10000;
|
||
const formatted = Number(value.toFixed(2)).toString();
|
||
return `${prefix}${formatted}万`;
|
||
}
|
||
return num.toLocaleString(locale);
|
||
} else {
|
||
if (absNum >= 1000000) {
|
||
const value = absNum / 1000000;
|
||
const formatted = Number(value.toFixed(2)).toString();
|
||
return `${prefix}${formatted}M`;
|
||
}
|
||
if (absNum >= 1000) {
|
||
const value = absNum / 1000;
|
||
const formatted = Number(value.toFixed(2)).toString();
|
||
return `${prefix}${formatted}K`;
|
||
}
|
||
return num.toLocaleString(locale);
|
||
}
|
||
};
|