Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
8784b622
authored
Jun 10, 2026
by
Archer
Committed by
GitHub
Jun 10, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: svg (#7094)
* fix: svg * doc * doc
parent
a81d6a83
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
304 additions
and
7 deletions
+304
-7
document/content/self-host/upgrading/4-15/41504.mdx
+106
-0
document/data/doc-last-modified.json
+4
-4
packages/web/components/common/Icon/index.tsx
+14
-1
packages/web/components/common/Icon/svgScope.ts
+88
-0
pnpm-lock.yaml
+3
-0
pro
+1
-1
projects/app/src/pages/api/core/chat/outLink/init.ts
+5
-1
projects/app/test/components/common/Icon/svgScope.test.ts
+83
-0
No files found.
document/content/self-host/upgrading/4-15/41504.mdx
View file @
8784b622
...
@@ -59,3 +59,109 @@ description: 'FastGPT V4.15.0-beta4 更新说明'
...
@@ -59,3 +59,109 @@ description: 'FastGPT V4.15.0-beta4 更新说明'
1. 插件服务从旧 `runtime` 结构调整为 pnpm workspace monorepo,拆分为 HTTP 服务入口、领域模型、用例、API adapter、基础设施、SDK 和 CLI。
1. 插件服务从旧 `runtime` 结构调整为 pnpm workspace monorepo,拆分为 HTTP 服务入口、领域模型、用例、API adapter、基础设施、SDK 和 CLI。
2. 将 app API 接口全部用 zod schema 编写并生成文档。
2. 将 app API 接口全部用 zod schema 编写并生成文档。
3. 及时处理 worker 内图片,不再存留 base64,降低内存消耗。
3. 及时处理 worker 内图片,不再存留 base64,降低内存消耗。
## FAQ
如果出现 Mongo sync index error,并且是提示:
```shell
modelName: 'chat_item_responses',
error: MongoServerError: Index build failed: a45ca5d9-8429-4602-80bc-312a22b92687: Collection fastgpt.chat_item_responses ( f2a417fe-6e7c-4ec2-8947-d8003755462f ) :: caused by :: E11000 duplicate key error collection: fastgpt.chat_item_responses index: appId_1_chatId_1_chatItemDataId_1_data.id_1 dup key: { appId: ObjectId('69c9110790de745f57e93919'), chatId: "jzt1JCcaoy4kC67hdhQlzUob", chatItemDataId: "xa0f1AMFr9CWj9uVC9TqGtc0", data.id: "jTUTS9j67N4CyZ17" }
```
可以进入 mongo shell,执行以下命令来清楚重复的数据。这里仅删除对话中每个节点的运行详情结果,无风险,可能是过去某次出现异常导致重复。
```shell
use fastgpt
const dryRun = true; // 确认后改成 false
const batchSize = 500;
const col = db.chat_item_responses;
let duplicateGroups = 0;
let duplicateDocs = 0;
let removableDocs = 0;
let deletedDocs = 0;
let pendingIds = [];
function flushDeletes() {
if (pendingIds.length === 0) return;
const ids = pendingIds;
pendingIds = [];
if (dryRun) {
print(`[dry-run] would delete ${ids.length} old docs`);
return;
}
const res = col.deleteMany({ _id: { $in: ids } });
deletedDocs += res.deletedCount;
print(`[delete] deleted ${res.deletedCount} old docs, total=${deletedDocs}`);
}
const cursor = col.aggregate(
[
{
$group: {
_id: {
appId: "$appId",
chatId: "$chatId",
chatItemDataId: "$chatItemDataId",
dataId: "$data.id"
},
count: { $sum: 1 }
}
},
{ $match: { count: { $gt: 1 } } },
{ $sort: { count: -1 } }
],
{ allowDiskUse: true }
);
while (cursor.hasNext()) {
const group = cursor.next();
duplicateGroups += 1;
duplicateDocs += group.count;
removableDocs += group.count - 1;
const key = group._id;
const filter = {
appId: key.appId,
chatId: key.chatId,
chatItemDataId: key.chatItemDataId,
"data.id": key.dataId
};
const docs = col
.find(filter, { _id: 1, time: 1 })
.sort({ _id: -1 })
.toArray();
const keepId = docs[0]?._id;
const removeIds = docs.slice(1).map((doc) => doc._id);
printjson({
group: duplicateGroups,
count: group.count,
keepId,
removeIds
});
pendingIds.push(...removeIds);
if (pendingIds.length >= batchSize) {
flushDeletes();
}
}
flushDeletes();
printjson({
dryRun,
duplicateGroups,
duplicateDocs,
removableDocs,
deletedDocs
});
```
document/data/doc-last-modified.json
View file @
8784b622
...
@@ -147,8 +147,8 @@
...
@@ -147,8 +147,8 @@
"content/plugin/model-presets.mdx"
:
"2026-06-04T16:10:15+08:00"
,
"content/plugin/model-presets.mdx"
:
"2026-06-04T16:10:15+08:00"
,
"content/plugin/system-tool-development.en.mdx"
:
"2026-06-09T16:03:58+08:00"
,
"content/plugin/system-tool-development.en.mdx"
:
"2026-06-09T16:03:58+08:00"
,
"content/plugin/system-tool-development.mdx"
:
"2026-06-09T16:03:58+08:00"
,
"content/plugin/system-tool-development.mdx"
:
"2026-06-09T16:03:58+08:00"
,
"content/self-host/config/env.en.mdx"
:
"2026-0
5-27T12:17:46
+08:00"
,
"content/self-host/config/env.en.mdx"
:
"2026-0
6-10T19:02:59
+08:00"
,
"content/self-host/config/env.mdx"
:
"2026-0
5-27T12:17:46
+08:00"
,
"content/self-host/config/env.mdx"
:
"2026-0
6-10T19:02:59
+08:00"
,
"content/self-host/config/json.en.mdx"
:
"2026-05-25T11:21:30+08:00"
,
"content/self-host/config/json.en.mdx"
:
"2026-05-25T11:21:30+08:00"
,
"content/self-host/config/json.mdx"
:
"2026-05-25T11:21:30+08:00"
,
"content/self-host/config/json.mdx"
:
"2026-05-25T11:21:30+08:00"
,
"content/self-host/config/model/intro.en.mdx"
:
"2026-06-04T16:10:15+08:00"
,
"content/self-host/config/model/intro.en.mdx"
:
"2026-06-04T16:10:15+08:00"
,
...
@@ -272,8 +272,8 @@
...
@@ -272,8 +272,8 @@
"content/self-host/upgrading/4-15/41502.mdx"
:
"2026-05-25T11:21:30+08:00"
,
"content/self-host/upgrading/4-15/41502.mdx"
:
"2026-05-25T11:21:30+08:00"
,
"content/self-host/upgrading/4-15/41503.en.mdx"
:
"2026-05-28T16:21:09+08:00"
,
"content/self-host/upgrading/4-15/41503.en.mdx"
:
"2026-05-28T16:21:09+08:00"
,
"content/self-host/upgrading/4-15/41503.mdx"
:
"2026-05-28T16:21:09+08:00"
,
"content/self-host/upgrading/4-15/41503.mdx"
:
"2026-05-28T16:21:09+08:00"
,
"content/self-host/upgrading/4-15/41504.en.mdx"
:
"2026-06-
07T17:54:48
+08:00"
,
"content/self-host/upgrading/4-15/41504.en.mdx"
:
"2026-06-
10T19:02:59
+08:00"
,
"content/self-host/upgrading/4-15/41504.mdx"
:
"2026-06-10T1
7:33:23
+08:00"
,
"content/self-host/upgrading/4-15/41504.mdx"
:
"2026-06-10T1
9:02:59
+08:00"
,
"content/self-host/upgrading/outdated/40.en.mdx"
:
"2026-04-26T21:08:47+08:00"
,
"content/self-host/upgrading/outdated/40.en.mdx"
:
"2026-04-26T21:08:47+08:00"
,
"content/self-host/upgrading/outdated/40.mdx"
:
"2026-04-26T21:08:47+08:00"
,
"content/self-host/upgrading/outdated/40.mdx"
:
"2026-04-26T21:08:47+08:00"
,
"content/self-host/upgrading/outdated/41.en.mdx"
:
"2026-04-26T21:08:47+08:00"
,
"content/self-host/upgrading/outdated/41.en.mdx"
:
"2026-04-26T21:08:47+08:00"
,
...
...
packages/web/components/common/Icon/index.tsx
View file @
8784b622
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
use
Id
,
useLayoutEffect
,
use
State
}
from
'react'
;
import
type
{
IconProps
}
from
'@chakra-ui/react'
;
import
type
{
IconProps
}
from
'@chakra-ui/react'
;
import
{
Box
,
Icon
}
from
'@chakra-ui/react'
;
import
{
Box
,
Icon
}
from
'@chakra-ui/react'
;
import
{
iconPaths
}
from
'./constants'
;
import
{
iconPaths
}
from
'./constants'
;
import
type
{
IconNameType
}
from
'./type'
;
import
type
{
IconNameType
}
from
'./type'
;
import
{
scopeSvgElementIds
}
from
'./svgScope'
;
const
iconCache
:
Record
<
string
,
any
>
=
{};
const
iconCache
:
Record
<
string
,
any
>
=
{};
const
useBrowserLayoutEffect
=
typeof
window
===
'undefined'
?
useEffect
:
useLayoutEffect
;
const
MyIcon
=
({
name
,
w
=
'auto'
,
h
=
'auto'
,
...
props
}:
{
name
:
IconNameType
}
&
IconProps
)
=>
{
const
MyIcon
=
({
name
,
w
=
'auto'
,
h
=
'auto'
,
...
props
}:
{
name
:
IconNameType
}
&
IconProps
)
=>
{
const
[,
setUpdate
]
=
useState
(
0
);
const
[,
setUpdate
]
=
useState
(
0
);
const
scopeId
=
useId
().
replace
(
/:/g
,
''
);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
iconCache
[
name
])
{
if
(
iconCache
[
name
])
{
...
@@ -26,6 +29,15 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
...
@@ -26,6 +29,15 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
const
IconComponent
=
iconCache
[
name
];
const
IconComponent
=
iconCache
[
name
];
useBrowserLayoutEffect
(()
=>
{
const
svg
=
document
.
querySelector
<
SVGSVGElement
>
(
`svg[data-fastgpt-icon-instance="
${
scopeId
}
"]`
);
if
(
!
svg
)
return
;
scopeSvgElementIds
(
svg
,
scopeId
,
`
${
scopeId
}
-
${
name
}
`
);
},
[
IconComponent
,
name
,
scopeId
]);
return
!!
IconComponent
?
(
return
!!
IconComponent
?
(
<
Icon
<
Icon
{
...
IconComponent
}
{
...
IconComponent
}
...
@@ -35,6 +47,7 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
...
@@ -35,6 +47,7 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconNameType
verticalAlign=
{
'top'
}
verticalAlign=
{
'top'
}
fill=
{
'currentcolor'
}
fill=
{
'currentcolor'
}
{
...
props
}
{
...
props
}
data
-
fastgpt
-
icon
-
instance=
{
scopeId
}
/>
/>
)
:
(
)
:
(
<
Box
w=
{
w
}
h=
{
'1px'
}
/>
<
Box
w=
{
w
}
h=
{
'1px'
}
/>
...
...
packages/web/components/common/Icon/svgScope.ts
0 → 100644
View file @
8784b622
const
svgUrlReferenceReg
=
/url
\(([
'"
]?)
#
([^
)'"
]
+
)\1\)
/g
;
const
hashOnlyReferenceReg
=
/^#
(
.+
)
$/
;
const
spaceSeparatedReferenceAttrs
=
new
Set
([
'aria-labelledby'
,
'aria-describedby'
]);
const
toScopedSvgId
=
(
scopeId
:
string
,
id
:
string
)
=>
{
const
prefix
=
`
${
scopeId
}
__`
;
return
id
.
startsWith
(
prefix
)
?
id
:
`
${
prefix
}${
id
}
`
;
};
export
const
createScopedSvgIdMap
=
(
ids
:
string
[],
scopeId
:
string
)
=>
{
const
idMap
=
new
Map
<
string
,
string
>
();
ids
.
forEach
((
id
)
=>
{
if
(
!
id
)
return
;
idMap
.
set
(
id
,
toScopedSvgId
(
scopeId
,
id
));
});
return
idMap
;
};
export
const
scopeSvgReferenceValue
=
(
value
:
string
,
idMap
:
Map
<
string
,
string
>
)
=>
{
let
nextValue
=
value
.
replace
(
svgUrlReferenceReg
,
(
match
,
quote
:
string
,
id
:
string
)
=>
{
const
scopedId
=
idMap
.
get
(
id
);
return
scopedId
?
`url(
${
quote
}
#
${
scopedId
}${
quote
}
)`
:
match
;
});
const
hashOnlyMatch
=
nextValue
.
match
(
hashOnlyReferenceReg
);
if
(
hashOnlyMatch
)
{
const
scopedId
=
idMap
.
get
(
hashOnlyMatch
[
1
]);
if
(
scopedId
)
{
nextValue
=
`#
${
scopedId
}
`
;
}
}
return
nextValue
;
};
export
const
scopeSpaceSeparatedSvgReferenceValue
=
(
value
:
string
,
idMap
:
Map
<
string
,
string
>
)
=>
{
return
value
.
split
(
/
\s
+/
)
.
map
((
id
)
=>
idMap
.
get
(
id
)
||
id
)
.
join
(
' '
);
};
/**
* 为单个内联 SVG 实例生成独立 id 作用域,避免多个相同系统 icon 同屏渲染时
* `url(#id)`、`href="#id"` 等 DOM 级引用串到其他 SVG 实例。
*/
export
const
scopeSvgElementIds
=
(
svg
:
SVGSVGElement
,
scopeId
:
string
,
scopeKey
=
scopeId
)
=>
{
if
(
svg
.
dataset
.
fastgptIconScoped
===
scopeKey
)
return
;
const
elements
=
[
svg
,
...
Array
.
from
(
svg
.
querySelectorAll
(
'*'
))];
const
rawIds
=
elements
.
map
((
element
)
=>
element
.
getAttribute
(
'id'
))
.
filter
((
id
):
id
is
string
=>
!!
id
);
const
idMap
=
createScopedSvgIdMap
(
rawIds
,
scopeId
);
if
(
idMap
.
size
===
0
)
{
svg
.
dataset
.
fastgptIconScoped
=
scopeKey
;
return
;
}
elements
.
forEach
((
element
)
=>
{
const
id
=
element
.
getAttribute
(
'id'
);
const
scopedId
=
id
?
idMap
.
get
(
id
)
:
undefined
;
if
(
scopedId
)
{
element
.
setAttribute
(
'id'
,
scopedId
);
}
});
elements
.
forEach
((
element
)
=>
{
Array
.
from
(
element
.
attributes
).
forEach
((
attr
)
=>
{
if
(
attr
.
name
===
'id'
)
return
;
const
nextValue
=
spaceSeparatedReferenceAttrs
.
has
(
attr
.
name
)
?
scopeSpaceSeparatedSvgReferenceValue
(
attr
.
value
,
idMap
)
:
scopeSvgReferenceValue
(
attr
.
value
,
idMap
);
if
(
nextValue
!==
attr
.
value
)
{
element
.
setAttribute
(
attr
.
name
,
nextValue
);
}
});
});
svg
.
dataset
.
fastgptIconScoped
=
scopeKey
;
};
pnpm-lock.yaml
View file @
8784b622
...
@@ -1226,6 +1226,9 @@ importers:
...
@@ -1226,6 +1226,9 @@ importers:
'
@types/xml2js'
:
'
@types/xml2js'
:
specifier
:
^0.4.14
specifier
:
^0.4.14
version
:
0.4.14
version
:
0.4.14
tsdown
:
specifier
:
'
catalog:'
version
:
0.21.10(typescript@6.0.3)
projects/app
:
projects/app
:
dependencies
:
dependencies
:
pro
@
ce87008b
Subproject commit
58d0509b3a1a6cb5d47340858a4cdfcfc01b51a0
Subproject commit
ce87008bf0da48e31e1e74036d1c37e903ea64c9
projects/app/src/pages/api/core/chat/outLink/init.ts
View file @
8784b622
...
@@ -12,9 +12,13 @@ import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
...
@@ -12,9 +12,13 @@ import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
import
{
presignVariablesFileUrls
}
from
'@fastgpt/service/core/chat/utils'
;
import
{
presignVariablesFileUrls
}
from
'@fastgpt/service/core/chat/utils'
;
import
{
InitOutLinkChatQuerySchema
}
from
'@fastgpt/global/openapi/core/chat/outLink/api'
;
import
{
InitOutLinkChatQuerySchema
}
from
'@fastgpt/global/openapi/core/chat/outLink/api'
;
import
{
ChatGenerateStatusEnum
}
from
'@fastgpt/global/core/chat/constants'
;
import
{
ChatGenerateStatusEnum
}
from
'@fastgpt/global/core/chat/constants'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
const
{
chatId
,
shareId
,
outLinkUid
}
=
InitOutLinkChatQuerySchema
.
parse
(
req
.
query
);
const
{
chatId
,
shareId
,
outLinkUid
}
=
parseApiInput
({
req
,
querySchema
:
InitOutLinkChatQuerySchema
}).
query
;
// auth link permission
// auth link permission
const
{
uid
,
appId
}
=
await
authOutLink
({
shareId
,
outLinkUid
});
const
{
uid
,
appId
}
=
await
authOutLink
({
shareId
,
outLinkUid
});
...
...
projects/app/test/components/common/Icon/svgScope.test.ts
0 → 100644
View file @
8784b622
import
{
describe
,
expect
,
it
}
from
'vitest'
;
import
{
createScopedSvgIdMap
,
scopeSpaceSeparatedSvgReferenceValue
,
scopeSvgElementIds
,
scopeSvgReferenceValue
}
from
'@fastgpt/web/components/common/Icon/svgScope'
;
class
FakeSvgElement
{
public
attributes
:
{
name
:
string
;
value
:
string
}[];
public
dataset
:
Record
<
string
,
string
>
=
{};
constructor
(
attrs
:
Record
<
string
,
string
>
,
private
readonly
children
:
FakeSvgElement
[]
=
[]
)
{
this
.
attributes
=
Object
.
entries
(
attrs
).
map
(([
name
,
value
])
=>
({
name
,
value
}));
}
getAttribute
(
name
:
string
)
{
return
this
.
attributes
.
find
((
attr
)
=>
attr
.
name
===
name
)?.
value
??
null
;
}
setAttribute
(
name
:
string
,
value
:
string
)
{
const
attr
=
this
.
attributes
.
find
((
attr
)
=>
attr
.
name
===
name
);
if
(
attr
)
{
attr
.
value
=
value
;
return
;
}
this
.
attributes
.
push
({
name
,
value
});
}
querySelectorAll
()
{
return
this
.
children
;
}
}
describe
(
'svgScope'
,
()
=>
{
it
(
'scopes url and hash references with the generated id map'
,
()
=>
{
const
idMap
=
createScopedSvgIdMap
([
'paint0'
,
'clip0'
],
'scope-a'
);
expect
(
scopeSvgReferenceValue
(
'url(#paint0)'
,
idMap
)).
toBe
(
'url(#scope-a__paint0)'
);
expect
(
scopeSvgReferenceValue
(
'url("#paint0")'
,
idMap
)).
toBe
(
'url("#scope-a__paint0")'
);
expect
(
scopeSvgReferenceValue
(
'#clip0'
,
idMap
)).
toBe
(
'#scope-a__clip0'
);
expect
(
scopeSvgReferenceValue
(
'url(#missing)'
,
idMap
)).
toBe
(
'url(#missing)'
);
});
it
(
'scopes space-separated aria references'
,
()
=>
{
const
idMap
=
createScopedSvgIdMap
([
'title-id'
,
'desc-id'
],
'scope-a'
);
expect
(
scopeSpaceSeparatedSvgReferenceValue
(
'title-id desc-id missing'
,
idMap
)).
toBe
(
'scope-a__title-id scope-a__desc-id missing'
);
});
it
(
'scopes ids and references inside one svg instance only once'
,
()
=>
{
const
gradient
=
new
FakeSvgElement
({
id
:
'paint0'
});
const
title
=
new
FakeSvgElement
({
id
:
'title-id'
});
const
rect
=
new
FakeSvgElement
({
fill
:
'url(#paint0)'
,
'clip-path'
:
'url("#paint0")'
,
href
:
'#paint0'
,
'aria-labelledby'
:
'title-id missing'
});
const
svg
=
new
FakeSvgElement
({
id
:
'root'
},
[
gradient
,
title
,
rect
]);
scopeSvgElementIds
(
svg
as
unknown
as
SVGSVGElement
,
'scope-a'
);
expect
(
svg
.
getAttribute
(
'id'
)).
toBe
(
'scope-a__root'
);
expect
(
gradient
.
getAttribute
(
'id'
)).
toBe
(
'scope-a__paint0'
);
expect
(
title
.
getAttribute
(
'id'
)).
toBe
(
'scope-a__title-id'
);
expect
(
rect
.
getAttribute
(
'fill'
)).
toBe
(
'url(#scope-a__paint0)'
);
expect
(
rect
.
getAttribute
(
'clip-path'
)).
toBe
(
'url("#scope-a__paint0")'
);
expect
(
rect
.
getAttribute
(
'href'
)).
toBe
(
'#scope-a__paint0'
);
expect
(
rect
.
getAttribute
(
'aria-labelledby'
)).
toBe
(
'scope-a__title-id missing'
);
scopeSvgElementIds
(
svg
as
unknown
as
SVGSVGElement
,
'scope-a'
);
expect
(
gradient
.
getAttribute
(
'id'
)).
toBe
(
'scope-a__paint0'
);
expect
(
rect
.
getAttribute
(
'fill'
)).
toBe
(
'url(#scope-a__paint0)'
);
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment