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
ee440d9a
authored
May 26, 2026
by
Xianquan
Committed by
GitHub
May 26, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(chat): isolate share chat cache keys (#6988)
parent
6e95cfb0
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
160 additions
and
11 deletions
+160
-11
projects/app/src/web/core/chat/context/useChatStore.ts
+73
-9
projects/app/test/web/core/chat/context/useChatStore.test.ts
+87
-2
No files found.
projects/app/src/web/core/chat/context/useChatStore.ts
View file @
ee440d9a
import
{
create
,
createJSONStorage
,
devtools
,
persist
,
immer
}
from
'@fastgpt/web/common/zustand'
;
import
{
getNanoid
}
from
'@fastgpt/global/common/string/tools'
;
import
{
type
OutLinkChatAuthProps
}
from
'@fastgpt/global/support/permission/chat'
;
import
type
{
ChatSourceEnum
}
from
'@fastgpt/global/core/chat/constants'
;
import
{
ChatSourceEnum
}
from
'@fastgpt/global/core/chat/constants'
;
import
{
ChatSidebarPaneEnum
}
from
'@/pageComponents/chat/constants'
;
type
State
=
{
...
...
@@ -28,6 +28,30 @@ type State = {
resetChatCache
:
()
=>
any
;
};
/**
* 生成按应用恢复会话用的缓存 key。
*
* 普通会话按 source + appId 隔离;分享会话的权限边界是 shareId + outLinkUid,
* 因此分享缓存 key 必须包含完整外链身份,避免同 app 下不同分享链接或外链用户串用 chatId。
*/
const
getAppChatIdCacheKey
=
({
source
,
appId
,
outLinkAuthData
}:
{
source
?:
`
${
ChatSourceEnum
}
`
;
appId
?:
string
;
outLinkAuthData
?:
OutLinkChatAuthProps
;
})
=>
{
if
(
!
source
||
!
appId
)
return
;
if
(
source
===
ChatSourceEnum
.
share
)
{
const
{
shareId
,
outLinkUid
}
=
outLinkAuthData
||
{};
if
(
!
shareId
||
!
outLinkUid
)
return
;
return
`
${
source
}
:
${
shareId
}
:
${
outLinkUid
}
:
${
appId
}
`
;
}
return
`
${
source
}
:
${
appId
}
`
;
};
const
createCustomStorage
=
()
=>
{
// source/chatId/appId 跟当前 tab 绑定,放 sessionStorage;其余跨 tab 共享字段放 localStorage
const
sessionKeys
=
[
'source'
,
'chatId'
,
'appId'
];
...
...
@@ -82,8 +106,13 @@ export const useChatStore = create<State>()(
source
:
undefined
,
setSource
(
e
)
{
set
((
state
)
=>
{
// 首次进入 chat 页面,如果相同的 source,则恢复上一次的 chatId
if
(
!
state
.
chatId
&&
state
.
lastChatId
&&
state
.
lastChatId
.
startsWith
(
e
))
{
// 分享会话的恢复必须依赖 shareId + outLinkUid,不能只靠 lastChatId 的 source 前缀。
if
(
e
!==
ChatSourceEnum
.
share
&&
!
state
.
chatId
&&
state
.
lastChatId
&&
state
.
lastChatId
.
startsWith
(
e
)
)
{
state
.
chatId
=
state
.
lastChatId
.
split
(
'-'
)[
1
];
}
else
if
(
e
!==
get
().
source
)
{
// 来源改变,强制重置 chatId
...
...
@@ -99,12 +128,21 @@ export const useChatStore = create<State>()(
set
((
state
)
=>
{
if
(
state
.
appId
!==
e
)
{
// 离开当前应用前,记住该应用最近一次会话
if
(
state
.
appId
&&
state
.
chatId
)
{
state
.
appChatIdMap
[
state
.
appId
]
=
state
.
chatId
;
const
currentCacheKey
=
getAppChatIdCacheKey
({
source
:
state
.
source
,
appId
:
state
.
appId
,
outLinkAuthData
:
state
.
outLinkAuthData
});
if
(
currentCacheKey
&&
state
.
chatId
)
{
state
.
appChatIdMap
[
currentCacheKey
]
=
state
.
chatId
;
}
// 切换到目标应用:优先恢复该应用上次的 chatId,否则临时生成(待历史列表加载后再对齐)
const
restoredChatId
=
state
.
appChatIdMap
[
e
];
const
nextCacheKey
=
getAppChatIdCacheKey
({
source
:
state
.
source
,
appId
:
e
,
outLinkAuthData
:
state
.
outLinkAuthData
});
const
restoredChatId
=
nextCacheKey
?
state
.
appChatIdMap
[
nextCacheKey
]
:
undefined
;
state
.
chatId
=
restoredChatId
||
getNanoid
(
24
);
if
(
state
.
source
)
{
state
.
lastChatId
=
`
${
state
.
source
}
-
${
state
.
chatId
}
`
;
...
...
@@ -122,8 +160,13 @@ export const useChatStore = create<State>()(
set
((
state
)
=>
{
state
.
chatId
=
id
;
state
.
lastChatId
=
`
${
state
.
source
}
-
${
id
}
`
;
if
(
state
.
appId
)
{
state
.
appChatIdMap
[
state
.
appId
]
=
id
;
const
cacheKey
=
getAppChatIdCacheKey
({
source
:
state
.
source
,
appId
:
state
.
appId
,
outLinkAuthData
:
state
.
outLinkAuthData
});
if
(
cacheKey
)
{
state
.
appChatIdMap
[
cacheKey
]
=
id
;
}
});
},
...
...
@@ -142,7 +185,28 @@ export const useChatStore = create<State>()(
outLinkAuthData
:
{},
setOutLinkAuthData
(
e
)
{
set
((
state
)
=>
{
const
currentCacheKey
=
getAppChatIdCacheKey
({
source
:
state
.
source
,
appId
:
state
.
appId
,
outLinkAuthData
:
state
.
outLinkAuthData
});
if
(
currentCacheKey
&&
state
.
chatId
)
{
state
.
appChatIdMap
[
currentCacheKey
]
=
state
.
chatId
;
}
state
.
outLinkAuthData
=
e
;
const
nextCacheKey
=
getAppChatIdCacheKey
({
source
:
state
.
source
,
appId
:
state
.
appId
,
outLinkAuthData
:
e
});
if
(
nextCacheKey
)
{
const
restoredChatId
=
state
.
appChatIdMap
[
nextCacheKey
];
state
.
chatId
=
restoredChatId
||
state
.
chatId
||
getNanoid
(
24
);
state
.
lastChatId
=
`
${
state
.
source
}
-
${
state
.
chatId
}
`
;
state
.
appChatIdMap
[
nextCacheKey
]
=
state
.
chatId
;
}
});
},
resetChatCache
()
{
...
...
projects/app/test/web/core/chat/context/useChatStore.test.ts
View file @
ee440d9a
...
...
@@ -84,6 +84,91 @@ describe('useChatStore', () => {
expect
(
useChatStore
.
getState
().
chatId
).
toBe
(
'chat-b'
);
});
it
(
'should namespace app chat cache by source'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
store
.
setSource
(
ChatSourceEnum
.
online
);
store
.
setAppId
(
'app-a'
);
store
.
setChatId
(
'online-chat-id'
);
store
.
setSource
(
ChatSourceEnum
.
api
);
store
.
setAppId
(
'app-b'
);
store
.
setChatId
(
'api-chat-id'
);
expect
(
useChatStore
.
getState
().
appChatIdMap
).
toMatchObject
({
[
`
${
ChatSourceEnum
.
online
}
:app-a`
]:
'online-chat-id'
,
[
`
${
ChatSourceEnum
.
api
}
:app-b`
]:
'api-chat-id'
});
});
it
(
'should not restore app cached chatId for share source'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
useChatStore
.
setState
({
source
:
ChatSourceEnum
.
share
,
appChatIdMap
:
{
[
`
${
ChatSourceEnum
.
online
}
:app-a`
]:
'normal-chat-id'
,
[
`
${
ChatSourceEnum
.
share
}
:app-a`
]:
'legacy-share-chat-id'
,
[
`
${
ChatSourceEnum
.
share
}
:share-a:user-a:app-a`
]:
'share-chat-id'
}
});
store
.
setAppId
(
'app-a'
);
const
newState
=
useChatStore
.
getState
();
expect
(
newState
.
chatId
).
toBe
(
'test-generated-id'
);
expect
(
newState
.
chatId
).
not
.
toBe
(
'normal-chat-id'
);
expect
(
newState
.
chatId
).
not
.
toBe
(
'legacy-share-chat-id'
);
expect
(
newState
.
chatId
).
not
.
toBe
(
'share-chat-id'
);
});
it
(
'should save share chatId with share identity namespace'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
store
.
setSource
(
ChatSourceEnum
.
share
);
store
.
setAppId
(
'app-a'
);
store
.
setOutLinkAuthData
({
shareId
:
'share-a'
,
outLinkUid
:
'user-a'
});
store
.
setChatId
(
'share-chat-id'
);
expect
(
useChatStore
.
getState
().
appChatIdMap
).
toEqual
({
[
`
${
ChatSourceEnum
.
share
}
:share-a:user-a:app-a`
]:
'share-chat-id'
});
});
it
(
'should restore share chatId only with matched share identity namespace'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
useChatStore
.
setState
({
source
:
ChatSourceEnum
.
share
,
appId
:
'app-a'
,
chatId
:
'new-chat-id'
,
appChatIdMap
:
{
[
`
${
ChatSourceEnum
.
share
}
:share-a:user-a:app-a`
]:
'matched-share-chat'
,
[
`
${
ChatSourceEnum
.
share
}
:share-a:user-b:app-a`
]:
'other-user-chat'
}
});
store
.
setOutLinkAuthData
({
shareId
:
'share-a'
,
outLinkUid
:
'user-a'
});
expect
(
useChatStore
.
getState
().
chatId
).
toBe
(
'matched-share-chat'
);
});
it
(
'should not restore last chatId for share source'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
useChatStore
.
setState
({
source
:
undefined
,
chatId
:
''
,
lastChatId
:
`
${
ChatSourceEnum
.
share
}
-cached-share-chat`
});
store
.
setSource
(
ChatSourceEnum
.
share
);
const
newState
=
useChatStore
.
getState
();
expect
(
newState
.
chatId
).
toBe
(
'test-generated-id'
);
expect
(
newState
.
chatId
).
not
.
toBe
(
'cached-share-chat'
);
});
it
(
'should keep chatId when setting the same appId'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
store
.
setSource
(
ChatSourceEnum
.
online
);
...
...
@@ -120,7 +205,7 @@ describe('useChatStore', () => {
it
(
'should restore last chat when setting same source and lastChatId with different id'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
const
source
=
ChatSourceEnum
.
shar
e
;
const
source
=
ChatSourceEnum
.
onlin
e
;
const
chatId
=
'test'
;
useChatStore
.
setState
({
lastChatId
:
`
${
source
}
-
${
chatId
}
`
,
...
...
@@ -135,7 +220,7 @@ describe('useChatStore', () => {
// The expected value should be 'test', not 'test-generated-id', since lastChatId is '${source}-test'
it
(
'should restore last chat when setting same source and lastChatId with id that matches getNanoid'
,
()
=>
{
const
store
=
useChatStore
.
getState
();
const
source
=
ChatSourceEnum
.
shar
e
;
const
source
=
ChatSourceEnum
.
onlin
e
;
const
chatId
=
'test'
;
useChatStore
.
setState
({
...
...
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