mirror of
https://github.com/labring/FastGPT.git
synced 2026-03-11 01:02:08 +08:00
V4.14.5.1 dev (#6290)
* chore: cherry pick some commits from v4.14.6-dev (#6287) * fix: custom domain limitation (#6265) * fix: system secret (#6259) * fix: system secret * chore: update docs * chore: docs * fix password variable & datetime picker (#6276) * fix password variable & datetime picker * doc * chore: cherry pick some commits from v4.14.6-dev (#6287) * fix: custom domain limitation (#6265) * fix: system secret (#6259) * fix: system secret * chore: update docs * chore: docs * doc * chore: docs --------- Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: Finley Ge <finleyge@fastgpt.io> * perf: extname computed (#6285) * perf: extname computed * chore: handle hash or query flags --------- Co-authored-by: Finley Ge <finleyge@fastgpt.io> * chore: docs (#6291) --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Archer <545436317@qq.com>
This commit is contained in:
@@ -119,6 +119,7 @@ description: FastGPT 文档目录
|
||||
- [/docs/upgrading/4-14/4143](/docs/upgrading/4-14/4143)
|
||||
- [/docs/upgrading/4-14/4144](/docs/upgrading/4-14/4144)
|
||||
- [/docs/upgrading/4-14/4145](/docs/upgrading/4-14/4145)
|
||||
- [/docs/upgrading/4-14/41451](/docs/upgrading/4-14/41451)
|
||||
- [/docs/upgrading/4-14/4146](/docs/upgrading/4-14/4146)
|
||||
- [/docs/upgrading/4-8/40](/docs/upgrading/4-8/40)
|
||||
- [/docs/upgrading/4-8/41](/docs/upgrading/4-8/41)
|
||||
|
||||
48
document/content/docs/upgrading/4-14/41451.mdx
Normal file
48
document/content/docs/upgrading/4-14/41451.mdx
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 'V4.14.5.1'
|
||||
description: 'FastGPT V4.14.5.1 更新说明'
|
||||
---
|
||||
|
||||
|
||||
## 🚀 新增内容
|
||||
|
||||
1. Markdown 表格支持导出 csv。
|
||||
|
||||
### 1. 更新镜像:
|
||||
|
||||
- 更新 FastGPT 镜像tag: v4.14.5.1
|
||||
- 更新 FastGPT 商业版镜像tag: v4.14.5.1
|
||||
- 更新 fastgpt-plugin 镜像 tag: v0.4.0
|
||||
- mcp_server 无需更新
|
||||
- Sandbox 无需更新
|
||||
- AIProxy 无需更新
|
||||
- mongo 无需更新
|
||||
|
||||
### 2. 执行升级脚本
|
||||
|
||||
从任意终端,发起 1 个 HTTP 请求。其中 `{{rootkey}}` 替换成环境变量里的 `rootkey`;`{{host}}` 替换成**FastGPT 域名**。
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'https://{{host}}/api/admin/initv41451' \
|
||||
--header 'rootkey: {{rootkey}}' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
1. 迁移系统工具的系统密钥配置
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
1. 工作流触摸板移动时,遇到输入框后会被强制阻拦。
|
||||
2. 工作流粘贴节点,精确按鼠标位置粘贴。
|
||||
3. 精确移除请求 LLM 时多余的系统字段,避免部分模型接口报错。
|
||||
4. 使用 path.extname 从 URL 获取文件扩展名
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
1. 系统工具工具集设置系统密钥后,子工具无法读取到设置的系统密钥
|
||||
2. 密码类型的全局变量,必填规则校验错误。
|
||||
3. 时间类型的全局变量,选择月份被遮挡。
|
||||
4. 手动复制弹窗,换行丢失。
|
||||
5. 未传入文件上传类型变量,对话接口报错。
|
||||
|
||||
## 插件
|
||||
@@ -7,6 +7,29 @@ description: 'FastGPT V4.14.6 更新说明'
|
||||
## 🚀 新增内容
|
||||
|
||||
1. Markdown 表格支持导出 csv。
|
||||
2. 系统工具可配置自定义的分类属性
|
||||
|
||||
### 1. 更新镜像:
|
||||
|
||||
- 更新 FastGPT 镜像tag: v4.14.6
|
||||
- 更新 FastGPT 商业版镜像tag: v4.14.6
|
||||
- 更新 fastgpt-plugin 镜像 tag: v0.4.0
|
||||
- mcp_server 无需更新
|
||||
- Sandbox 无需更新
|
||||
- AIProxy 无需更新
|
||||
- mongo 5.x 版本修改成 5.0.32 版本,解决 CVE-2025-14847 漏洞。直接修改镜像 tag 成 `5.0.32`。
|
||||
|
||||
### 2. 执行升级脚本
|
||||
|
||||
从任意终端,发起 1 个 HTTP 请求。其中 `{{rootkey}}` 替换成环境变量里的 `rootkey`;`{{host}}` 替换成**FastGPT 域名**。
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'https://{{host}}/api/admin/initv4146' \
|
||||
--header 'rootkey: {{rootkey}}' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
1. 迁移系统工具的系统密钥配置
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
@@ -16,4 +39,6 @@ description: 'FastGPT V4.14.6 更新说明'
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
1. 系统工具工具集设置系统密钥后,子工具无法读取到设置的系统密钥
|
||||
|
||||
## 插件
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"document/content/docs/protocol/terms.en.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/protocol/terms.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
|
||||
"document/content/docs/toc.mdx": "2026-01-11T21:09:27+08:00",
|
||||
"document/content/docs/toc.mdx": "2026-01-19T15:56:02+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-09-08T20:07:20+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
@@ -122,8 +122,9 @@
|
||||
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
|
||||
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
|
||||
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
|
||||
"document/content/docs/upgrading/4-14/4145.mdx": "2026-01-13T19:11:02+08:00",
|
||||
"document/content/docs/upgrading/4-14/4146.mdx": "2026-01-12T21:07:44+08:00",
|
||||
"document/content/docs/upgrading/4-14/4145.mdx": "2026-01-18T23:59:15+08:00",
|
||||
"document/content/docs/upgrading/4-14/41451.mdx": "2026-01-19T16:10:06+08:00",
|
||||
"document/content/docs/upgrading/4-14/4146.mdx": "2026-01-19T15:56:02+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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import path from 'path';
|
||||
|
||||
/* check string is a web link */
|
||||
export function strIsLink(str?: string) {
|
||||
@@ -193,13 +194,14 @@ export const sliceStrStartEnd = (str: string, start: number, end: number) => {
|
||||
=> pdf
|
||||
*/
|
||||
export const parseFileExtensionFromUrl = (url = '') => {
|
||||
// Remove query params
|
||||
const urlWithoutQuery = url.split('?')[0];
|
||||
// Get file name
|
||||
const fileName = urlWithoutQuery.split('/').pop() || '';
|
||||
// Get file extension
|
||||
const extension = fileName.split('.').pop();
|
||||
return (extension || '').toLowerCase();
|
||||
// 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 => {
|
||||
|
||||
@@ -633,7 +633,6 @@ export const refreshSystemTools = async (): Promise<AppToolTemplateItemType[]> =
|
||||
defaultInstalled: dbPluginConfig?.defaultInstalled ?? false,
|
||||
inputList: item?.secretInputConfig,
|
||||
hasSystemSecret: !!dbPluginConfig?.inputListVal,
|
||||
|
||||
originCost: dbPluginConfig?.originCost ?? 0,
|
||||
currentCost: dbPluginConfig?.currentCost ?? 0,
|
||||
systemKeyCost: dbPluginConfig?.systemKeyCost ?? 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import { Box, Card, Flex, useOutsideClick } from '@chakra-ui/react';
|
||||
import { Box, Card, Flex, Portal, useOutsideClick } from '@chakra-ui/react';
|
||||
import { format } from 'date-fns';
|
||||
import type { Matcher } from 'react-day-picker';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
@@ -22,33 +22,60 @@ const DateTimePicker = ({
|
||||
selectedDateTime?: Date;
|
||||
disabled?: Matcher[];
|
||||
} & Omit<BoxProps, 'onChange'>) => {
|
||||
const OutRangeRef = useRef(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
||||
selectedDateTime || defaultDate
|
||||
);
|
||||
const [showSelected, setShowSelected] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDate(selectedDateTime);
|
||||
}, [selectedDateTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showSelected && containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
if (popPosition === 'top') {
|
||||
setPosition({
|
||||
top: rect.top - 4,
|
||||
left: rect.left
|
||||
});
|
||||
} else {
|
||||
setPosition({
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [showSelected, popPosition]);
|
||||
|
||||
// 点击外部关闭
|
||||
useEffect(() => {
|
||||
if (!showSelected) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (
|
||||
containerRef.current?.contains(e.target as Node) ||
|
||||
popoverRef.current?.contains(e.target as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setShowSelected(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [showSelected]);
|
||||
|
||||
const formatSelected = useMemo(() => {
|
||||
if (selectedDate) {
|
||||
const dateStr = format(selectedDate, 'y/MM/dd');
|
||||
return dateStr;
|
||||
return format(selectedDate, 'y/MM/dd');
|
||||
}
|
||||
return '';
|
||||
}, [selectedDate]);
|
||||
|
||||
useOutsideClick({
|
||||
ref: OutRangeRef,
|
||||
handler: () => {
|
||||
setShowSelected(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position={'relative'} ref={OutRangeRef}>
|
||||
<Box position={'relative'} ref={containerRef}>
|
||||
<Flex
|
||||
border={'base'}
|
||||
px={3}
|
||||
@@ -68,32 +95,33 @@ const DateTimePicker = ({
|
||||
<MyIcon ml={2} name={'date'} w={'16px'} color={'myGray.600'} />
|
||||
</Flex>
|
||||
{showSelected && (
|
||||
<Card
|
||||
position={'absolute'}
|
||||
zIndex={10}
|
||||
css={{
|
||||
'--rdp-background-color': '#d6e8ff',
|
||||
'--rdp-accent-color': '#0000ff'
|
||||
}}
|
||||
{...(popPosition === 'top'
|
||||
? {
|
||||
bottom: '40px'
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<DayPicker
|
||||
locale={zhCN}
|
||||
mode="single"
|
||||
defaultMonth={selectedDate}
|
||||
selected={selectedDate}
|
||||
disabled={disabled}
|
||||
onSelect={(date) => {
|
||||
setSelectedDate(date);
|
||||
onChange?.(date);
|
||||
setShowSelected(false);
|
||||
<Portal>
|
||||
<Card
|
||||
ref={popoverRef}
|
||||
position={'fixed'}
|
||||
top={popPosition === 'top' ? 'auto' : `${position.top}px`}
|
||||
bottom={popPosition === 'top' ? `${window.innerHeight - position.top}px` : 'auto'}
|
||||
left={`${position.left}px`}
|
||||
zIndex={1500}
|
||||
css={{
|
||||
'--rdp-background-color': '#d6e8ff',
|
||||
'--rdp-accent-color': '#0000ff'
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
>
|
||||
<DayPicker
|
||||
locale={zhCN}
|
||||
mode="single"
|
||||
defaultMonth={selectedDate}
|
||||
selected={selectedDate}
|
||||
disabled={disabled}
|
||||
onSelect={(date) => {
|
||||
setSelectedDate(date);
|
||||
onChange?.(date);
|
||||
setShowSelected(false);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Portal>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.14.5",
|
||||
"version": "4.14.5.1",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "npm run build:workers && next dev",
|
||||
|
||||
@@ -66,14 +66,17 @@ const LabelAndFormRender = ({
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return true;
|
||||
if (inputType === InputTypeEnum.password && props.minLength) {
|
||||
if (!value || typeof value !== 'object' || !value.value) return false;
|
||||
if (value.value.length < props.minLength) {
|
||||
if (!required) return true;
|
||||
// 密码类型特殊处理:已加密的密码格式为 { value: '', secret: 'xxx' }
|
||||
if (inputType === InputTypeEnum.password) {
|
||||
const hasValue = value && typeof value === 'object' && (value.value || value.secret);
|
||||
if (!hasValue) return false;
|
||||
// 有 minLength 要求且正在输入新值时,检查长度
|
||||
if (props.minLength && value.value && value.value.length < props.minLength) {
|
||||
return t(`common:min_length`, { minLenth: props.minLength });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!required) return true;
|
||||
|
||||
if (inputType === InputTypeEnum.fileSelect) {
|
||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||
|
||||
@@ -485,7 +485,7 @@ const InputTypeConfig = ({
|
||||
setValue('timeRangeStart', date);
|
||||
}}
|
||||
popPosition="top"
|
||||
timeGranularity={timeGranularity}
|
||||
timeGranularity={timeGranularity || 'day'}
|
||||
maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined}
|
||||
/>
|
||||
</Box>
|
||||
@@ -499,7 +499,7 @@ const InputTypeConfig = ({
|
||||
setValue('timeRangeEnd', date);
|
||||
}}
|
||||
popPosition="top"
|
||||
timeGranularity={timeGranularity}
|
||||
timeGranularity={timeGranularity || 'day'}
|
||||
minDate={timeRangeStart ? new Date(timeRangeStart) : undefined}
|
||||
/>
|
||||
</Box>
|
||||
@@ -623,7 +623,7 @@ const InputTypeConfig = ({
|
||||
setValue('defaultValue', date);
|
||||
}}
|
||||
popPosition="top"
|
||||
timeGranularity={timeGranularity}
|
||||
timeGranularity={timeGranularity || 'day'}
|
||||
minDate={timeRangeStart ? new Date(timeRangeStart) : undefined}
|
||||
maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined}
|
||||
/>
|
||||
@@ -640,7 +640,7 @@ const InputTypeConfig = ({
|
||||
setValue('defaultValue', [date, timeRangeEndDefault]);
|
||||
}}
|
||||
popPosition="top"
|
||||
timeGranularity={timeGranularity}
|
||||
timeGranularity={timeGranularity || 'day'}
|
||||
minDate={timeRangeStart ? new Date(timeRangeStart) : undefined}
|
||||
maxDate={
|
||||
timeRangeEndDefault && timeRangeEnd
|
||||
@@ -668,7 +668,7 @@ const InputTypeConfig = ({
|
||||
setValue('defaultValue', [timeRangeStartDefault, date]);
|
||||
}}
|
||||
popPosition="top"
|
||||
timeGranularity={timeGranularity}
|
||||
timeGranularity={timeGranularity || 'day'}
|
||||
minDate={
|
||||
timeRangeStartDefault && timeRangeStart
|
||||
? new Date(
|
||||
|
||||
@@ -237,7 +237,7 @@ const Reference = ({
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
isDisabled: isEmptyItem,
|
||||
minW: '240px',
|
||||
w: '240px',
|
||||
_hover: {
|
||||
borderColor: 'blue.300'
|
||||
}
|
||||
|
||||
@@ -74,11 +74,11 @@ const CustomDomain = () => {
|
||||
const [editDomain, setEditDomain] = useState<CustomDomainType | undefined>(undefined);
|
||||
|
||||
// 检查用户是否有 advanced 套餐
|
||||
const isAdvancedPlan = useMemo(() => {
|
||||
const isSupportCustomDomain = useMemo(() => {
|
||||
const plan = teamPlanStatus?.standard;
|
||||
if (!plan) return false;
|
||||
|
||||
return plan.customDomain && plan.customDomain > 0;
|
||||
return !!(plan.customDomain && plan.customDomain > 0);
|
||||
}, [teamPlanStatus?.standard]);
|
||||
|
||||
return (
|
||||
@@ -100,7 +100,7 @@ const CustomDomain = () => {
|
||||
<Button
|
||||
variant="whitePrimaryOutline"
|
||||
onClick={onOpenCreateModal}
|
||||
isDisabled={!isAdvancedPlan}
|
||||
isDisabled={isSupportCustomDomain}
|
||||
>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
@@ -185,7 +185,7 @@ const CustomDomain = () => {
|
||||
>
|
||||
<EmptyTip
|
||||
text={
|
||||
!isAdvancedPlan && (
|
||||
isSupportCustomDomain && (
|
||||
<Flex flexDir="column" alignItems="center">
|
||||
<Box>{t('account:upgrade_to_use_custom_domain')}</Box>
|
||||
<Button
|
||||
|
||||
101
projects/app/src/pages/api/admin/initv41451.ts
Normal file
101
projects/app/src/pages/api/admin/initv41451.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoSystemTool } from '@fastgpt/service/core/plugin/tool/systemToolSchema';
|
||||
import type { AnyBulkWriteOperation } from '@fastgpt/service/common/mongo';
|
||||
import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type';
|
||||
|
||||
export type ResponseType = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 4.14.5.1 版本数据初始化脚本
|
||||
* 1. 迁移 system tool 数据:如果工具集配置了系统密钥,则需要给其子工具都写入 InputListVal 字段
|
||||
*/
|
||||
|
||||
const migrateSystemSecret = async () => {
|
||||
// 1. find all system tools
|
||||
const tools = await MongoSystemTool.find(
|
||||
{
|
||||
pluginId: {
|
||||
$regex: /systemTool-/
|
||||
}
|
||||
},
|
||||
{
|
||||
pluginId: 1,
|
||||
inputListVal: 1
|
||||
}
|
||||
).lean();
|
||||
|
||||
// 2. 构建工具集和子工具的映射关系
|
||||
const toolSetMap = new Map<string, (typeof tools)[0]>(); // 工具集 ID -> 工具集对象
|
||||
const childToolsMap = new Map<string, typeof tools>(); // 工具集 ID -> 子工具列表
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.pluginId.includes('/')) {
|
||||
// 这是一个子工具
|
||||
const toolSetId = tool.pluginId.split('/')[0];
|
||||
if (!childToolsMap.has(toolSetId)) {
|
||||
childToolsMap.set(toolSetId, []);
|
||||
}
|
||||
childToolsMap.get(toolSetId)!.push(tool);
|
||||
} else {
|
||||
// 这是一个工具集
|
||||
toolSetMap.set(tool.pluginId, tool);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建批量更新操作
|
||||
const ops: AnyBulkWriteOperation<SystemPluginToolCollectionType>[] = [];
|
||||
|
||||
for (const [toolSetId, toolSet] of toolSetMap.entries()) {
|
||||
// 只处理配置了系统密钥的工具集
|
||||
if (toolSet.inputListVal) {
|
||||
const childTools = childToolsMap.get(toolSetId) || [];
|
||||
|
||||
// 为每个子工具添加更新操作
|
||||
for (const childTool of childTools) {
|
||||
ops.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: childTool._id
|
||||
},
|
||||
update: {
|
||||
$set: {
|
||||
inputListVal: toolSet.inputListVal
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 执行批量更新
|
||||
if (ops.length > 0) {
|
||||
await MongoSystemTool.bulkWrite(ops);
|
||||
console.log(`Updated ${ops.length} child tools with system secrets`);
|
||||
}
|
||||
|
||||
return ops.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* 主处理函数
|
||||
*/
|
||||
async function handler(
|
||||
req: ApiRequestProps,
|
||||
_res: ApiResponseType<ResponseType>
|
||||
): Promise<ResponseType> {
|
||||
await authCert({ req, authRoot: true });
|
||||
|
||||
// 执行系统工具密钥迁移
|
||||
const updatedCount = await migrateSystemSecret();
|
||||
|
||||
return {
|
||||
message: `Completed v4.14.6 initialization: Updated ${updatedCount} child tools with system secrets`
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -68,7 +68,8 @@ async function handler(
|
||||
{ pluginId: tool.pluginId },
|
||||
{
|
||||
pluginId: tool.pluginId,
|
||||
systemKeyCost: tool.systemKeyCost
|
||||
systemKeyCost: tool.systemKeyCost,
|
||||
inputListVal: updateFields.inputListVal
|
||||
},
|
||||
{ upsert: true, session }
|
||||
);
|
||||
|
||||
191
test/cases/global/common/string/string.test.ts
Normal file
191
test/cases/global/common/string/string.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
describe('parseFileExtensionFromUrl', () => {
|
||||
it('should parse extension from simple URL', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('https://example.com/document.docx')).toBe('docx');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/image.jpg')).toBe('jpg');
|
||||
});
|
||||
|
||||
it('should parse extension from URL with query parameters', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.pdf?download=true')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('https://example.com/image.png?size=large&quality=high')).toBe(
|
||||
'png'
|
||||
);
|
||||
expect(parseFileExtensionFromUrl('http://example.com/doc.txt?v=1.2.3&auth=token123')).toBe(
|
||||
'txt'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not handle hash in URL (returns extension with hash)', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.pdf#page=1')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('https://example.com/image.jpg#section')).toBe('jpg');
|
||||
});
|
||||
|
||||
it('should parse extension from URL with query but not hash', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.pdf?download=true#page=1')).toBe(
|
||||
'pdf'
|
||||
);
|
||||
expect(parseFileExtensionFromUrl('https://example.com/image.png?v=1#top')).toBe('png');
|
||||
});
|
||||
|
||||
it('should parse extension from nested path URL', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/path/to/file.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('https://cdn.example.com/assets/images/photo.jpg')).toBe(
|
||||
'jpg'
|
||||
);
|
||||
expect(parseFileExtensionFromUrl('http://example.com/a/b/c/d/e/document.docx')).toBe('docx');
|
||||
});
|
||||
|
||||
it('should return lowercase extension', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.PDF')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/image.JPG')).toBe('jpg');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/document.DOCX')).toBe('docx');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/MixedCase.TxT')).toBe('txt');
|
||||
});
|
||||
|
||||
it('should parse extension from relative URLs', () => {
|
||||
expect(parseFileExtensionFromUrl('/static/file.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('./images/photo.png')).toBe('png');
|
||||
expect(parseFileExtensionFromUrl('../documents/report.xlsx')).toBe('xlsx');
|
||||
});
|
||||
|
||||
it('should handle S3-style URLs', () => {
|
||||
expect(parseFileExtensionFromUrl('chat/image.png')).toBe('png');
|
||||
expect(parseFileExtensionFromUrl('dataset/document.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('uploads/2024/01/file.txt')).toBe('txt');
|
||||
});
|
||||
|
||||
it('should handle file with multiple dots', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/my.file.name.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/archive.tar.gz')).toBe('gz');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/config.backup.json')).toBe('json');
|
||||
});
|
||||
|
||||
it('should handle special characters in URL', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file%20name.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file-name.jpg')).toBe('jpg');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file_name.png')).toBe('png');
|
||||
});
|
||||
|
||||
it('should return empty string for URL without extension', () => {
|
||||
// Note: path.extname returns '' when no extension exists
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/path/to/file')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/download')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle URL ending with slash', () => {
|
||||
// Note: path.extname may treat domain extension as file extension for root URLs
|
||||
expect(parseFileExtensionFromUrl('http://example.com/')).toBe('com');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/path/')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/path/to/folder/')).toBe('');
|
||||
});
|
||||
|
||||
it('should return domain extension for root URL', () => {
|
||||
// Note: The function treats domain as filename when no path exists
|
||||
expect(parseFileExtensionFromUrl('http://example.com')).toBe('com');
|
||||
expect(parseFileExtensionFromUrl('https://example.com')).toBe('com');
|
||||
});
|
||||
|
||||
it('should handle empty string input', () => {
|
||||
expect(parseFileExtensionFromUrl('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle undefined input', () => {
|
||||
expect(parseFileExtensionFromUrl()).toBe('');
|
||||
});
|
||||
|
||||
it('should handle URLs with port numbers', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com:8080/file.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('https://localhost:3000/image.png')).toBe('png');
|
||||
});
|
||||
|
||||
it('should handle data URLs correctly', () => {
|
||||
// Note: path.extname doesn't handle data URLs specially, returns empty
|
||||
expect(parseFileExtensionFromUrl('data:image/png;base64,iVBORw0KGgo=')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('data:application/pdf;base64,JVBERi0=')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle common file extensions', () => {
|
||||
// Documents
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.docx')).toBe('docx');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.xlsx')).toBe('xlsx');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.pptx')).toBe('pptx');
|
||||
|
||||
// Images
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.jpg')).toBe('jpg');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.jpeg')).toBe('jpeg');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.png')).toBe('png');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.gif')).toBe('gif');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.webp')).toBe('webp');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.svg')).toBe('svg');
|
||||
|
||||
// Text
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.txt')).toBe('txt');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.md')).toBe('md');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.csv')).toBe('csv');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.json')).toBe('json');
|
||||
|
||||
// Archives
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.zip')).toBe('zip');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.tar')).toBe('tar');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.gz')).toBe('gz');
|
||||
|
||||
// Code
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.js')).toBe('js');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.ts')).toBe('ts');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/file.py')).toBe('py');
|
||||
});
|
||||
|
||||
it('should handle Chinese characters in URL', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/文件.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/测试/图片.jpg')).toBe('jpg');
|
||||
});
|
||||
|
||||
it('should handle filename with only dot prefix (hidden files)', () => {
|
||||
// Note: path.extname treats the entire name after dot as extension for files starting with dot
|
||||
expect(parseFileExtensionFromUrl('http://example.com/.gitignore')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/.env')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/path/.htaccess')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle filename starting with dot and having extension', () => {
|
||||
expect(parseFileExtensionFromUrl('http://example.com/.config.json')).toBe('json');
|
||||
expect(parseFileExtensionFromUrl('http://example.com/.eslintrc.js')).toBe('js');
|
||||
});
|
||||
|
||||
it('should handle direct filename input (not URL)', () => {
|
||||
// Test simple filenames
|
||||
expect(parseFileExtensionFromUrl('document.pdf')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('image.jpg')).toBe('jpg');
|
||||
expect(parseFileExtensionFromUrl('data.json')).toBe('json');
|
||||
expect(parseFileExtensionFromUrl('script.js')).toBe('js');
|
||||
expect(parseFileExtensionFromUrl('style.css')).toBe('css');
|
||||
|
||||
// Test filenames with multiple dots
|
||||
expect(parseFileExtensionFromUrl('archive.tar.gz')).toBe('gz');
|
||||
expect(parseFileExtensionFromUrl('config.backup.json')).toBe('json');
|
||||
|
||||
// Test filenames with uppercase extensions
|
||||
expect(parseFileExtensionFromUrl('FILE.PDF')).toBe('pdf');
|
||||
expect(parseFileExtensionFromUrl('Image.JPG')).toBe('jpg');
|
||||
|
||||
// Test filename without extension
|
||||
expect(parseFileExtensionFromUrl('README')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('Makefile')).toBe('');
|
||||
|
||||
// Test hidden files (starting with dot)
|
||||
expect(parseFileExtensionFromUrl('.gitignore')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('.env')).toBe('');
|
||||
expect(parseFileExtensionFromUrl('.eslintrc.js')).toBe('js');
|
||||
|
||||
// Test filenames with special characters
|
||||
expect(parseFileExtensionFromUrl('my-file.txt')).toBe('txt');
|
||||
expect(parseFileExtensionFromUrl('my_file.txt')).toBe('txt');
|
||||
expect(parseFileExtensionFromUrl('file name.txt')).toBe('txt');
|
||||
expect(parseFileExtensionFromUrl('文件.pdf')).toBe('pdf');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user