Files
FastGPT/packages/global/common/string/tools.ts
T
Archer 7506a147e6 V4.14.x (#6751)
* 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>
2026-04-17 23:28:43 +08:00

275 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
};