3 Commits

Author SHA1 Message Date
jiang 536cd6a5d1 增加获取用户 ID 的功能,Agent chat 请求头新增传递 userId
Build Push and Deploy / docker-image (push) Successful in 1m12s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped
2026-05-11 16:37:55 +08:00
jiang 133f5d417f 调整聊天框为临时模式,优化滚动和过渡效果,避免出现页面横向拉伸 2026-05-08 17:42:21 +08:00
jiang cf43700459 重构聊天会话标题管理,支持首轮对话更新 2026-05-08 17:22:54 +08:00
7 changed files with 89 additions and 15 deletions
+1
View File
@@ -6,4 +6,5 @@ body {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden;
} }
+5 -3
View File
@@ -155,19 +155,21 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
return ( return (
<Drawer <Drawer
anchor="right" anchor="right"
variant="persistent" variant="temporary"
open={open} open={open}
onClose={onClose} onClose={onClose}
hideBackdrop hideBackdrop
disableScrollLock
disableEnforceFocus
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.modal + 100 }} sx={{ zIndex: (muiTheme) => muiTheme.zIndex.modal + 100 }}
PaperProps={{ PaperProps={{
sx: { sx: {
width: { xs: "100%", sm: width }, width: { xs: "100%", sm: width },
background: "transparent", background: "transparent",
boxShadow: "none", boxShadow: "none",
overflow: "visible", overflow: open ? "visible" : "hidden",
zIndex: (muiTheme) => muiTheme.zIndex.modal + 100, zIndex: (muiTheme) => muiTheme.zIndex.modal + 100,
transition: isResizing ? "none" : "width 0.2s cubic-bezier(0, 0, 0.2, 1)", transition: isResizing ? "none" : undefined,
}, },
}} }}
> >
+22 -11
View File
@@ -68,14 +68,6 @@ const toSessionSummary = (session: ChatSessionRecord): ChatSessionSummary => ({
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
}); });
const buildSessionTitle = (messages: Message[]) => {
const firstUserMessage = messages.find((message) => message.role === "user");
if (!firstUserMessage) return "新对话";
const title = firstUserMessage.content.replace(/\s+/g, " ").trim();
if (!title) return "新对话";
return title.length > 24 ? `${title.slice(0, 24)}...` : title;
};
const getDb = () => const getDb = () =>
openDB<ChatDB>(CHAT_DB_NAME, CHAT_DB_VERSION, { openDB<ChatDB>(CHAT_DB_NAME, CHAT_DB_VERSION, {
upgrade(db) { upgrade(db) {
@@ -170,7 +162,7 @@ const migrateLegacyLocalStorage = async () => {
const now = Date.now(); const now = Date.now();
const sessionRecord: ChatSessionRecord = { const sessionRecord: ChatSessionRecord = {
id: createId(), id: createId(),
title: buildSessionTitle(legacyState.messages), title: "新对话",
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
sessionId: legacyState.sessionId, sessionId: legacyState.sessionId,
@@ -244,9 +236,8 @@ export const saveActiveChatState = async (
const now = Date.now(); const now = Date.now();
const storageSessionId = state.storageSessionId ?? createId(); const storageSessionId = state.storageSessionId ?? createId();
const computedTitle = buildSessionTitle(state.messages);
const preferredTitle = state.title?.trim(); const preferredTitle = state.title?.trim();
const finalTitle = preferredTitle || computedTitle; const finalTitle = preferredTitle || existingSession?.title || "新对话";
const nextRecord: ChatSessionRecord = { const nextRecord: ChatSessionRecord = {
id: storageSessionId, id: storageSessionId,
title: finalTitle, title: finalTitle,
@@ -278,6 +269,26 @@ export const listChatSessions = async (): Promise<ChatSessionSummary[]> => {
.map(toSessionSummary); .map(toSessionSummary);
}; };
export const updateChatSessionTitle = async (
storageSessionId: string,
title: string,
): Promise<void> => {
if (typeof window === "undefined") return;
const normalizedTitle = title.trim();
if (!normalizedTitle) return;
const db = await getDb();
const session = await db.get(SESSION_STORE, storageSessionId);
if (!session) return;
await db.put(SESSION_STORE, {
...session,
title: normalizedTitle,
updatedAt: Date.now(),
});
};
export const createEmptyChatSession = async (): Promise<LoadedChatState> => { export const createEmptyChatSession = async (): Promise<LoadedChatState> => {
if (typeof window === "undefined") return emptyLoadedChatState(); if (typeof window === "undefined") return emptyLoadedChatState();
@@ -25,6 +25,7 @@ import {
loadActiveChatState, loadActiveChatState,
loadChatSessionById, loadChatSessionById,
saveActiveChatState, saveActiveChatState,
updateChatSessionTitle,
} from "../chatStorage"; } from "../chatStorage";
type UseAgentChatSessionOptions = { type UseAgentChatSessionOptions = {
@@ -110,6 +111,7 @@ export const useAgentChatSession = ({
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
const sessionIdRef = useRef<string | undefined>(undefined); const sessionIdRef = useRef<string | undefined>(undefined);
const cancelPromiseRef = useRef<Promise<void> | null>(null); const cancelPromiseRef = useRef<Promise<void> | null>(null);
const titleUpdateNonceRef = useRef(0);
useEffect(() => { useEffect(() => {
sessionIdRef.current = sessionId; sessionIdRef.current = sessionId;
@@ -130,6 +132,7 @@ export const useAgentChatSession = ({
sessionIdRef.current = loadedState.sessionId; sessionIdRef.current = loadedState.sessionId;
hydrationCompletedRef.current = true; hydrationCompletedRef.current = true;
hydrationNonceRef.current += 1; hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
setMessages(loadedState.messages); setMessages(loadedState.messages);
setSessionTitle(loadedState.title); setSessionTitle(loadedState.title);
@@ -308,6 +311,19 @@ export const useAgentChatSession = ({
const nextTitle = event.title.trim(); const nextTitle = event.title.trim();
if (nextTitle) { if (nextTitle) {
setSessionTitle(nextTitle); setSessionTitle(nextTitle);
const currentStorageSessionId = storageSessionIdRef.current;
if (currentStorageSessionId) {
const currentNonce = ++titleUpdateNonceRef.current;
void updateChatSessionTitle(currentStorageSessionId, nextTitle)
.then(() => listChatSessions())
.then((sessions) => {
if (titleUpdateNonceRef.current !== currentNonce) return;
setChatSessions(sessions);
})
.catch((error) => {
console.error("[GlobalChatbox] Failed to persist session title:", error);
});
}
} }
} else if (event.type === "done") { } else if (event.type === "done") {
setMessages((prev) => setMessages((prev) =>
@@ -431,6 +447,7 @@ export const useAgentChatSession = ({
setSessionId(undefined); setSessionId(undefined);
sessionIdRef.current = undefined; sessionIdRef.current = undefined;
storageSessionIdRef.current = undefined; storageSessionIdRef.current = undefined;
titleUpdateNonceRef.current += 1;
setIsStreaming(false); setIsStreaming(false);
}, []); }, []);
@@ -445,6 +462,7 @@ export const useAgentChatSession = ({
const sessions = await listChatSessions(); const sessions = await listChatSessions();
hydrationNonceRef.current += 1; hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = newState.storageSessionId; storageSessionIdRef.current = newState.storageSessionId;
sessionIdRef.current = newState.sessionId; sessionIdRef.current = newState.sessionId;
setMessages(newState.messages); setMessages(newState.messages);
@@ -469,6 +487,7 @@ export const useAgentChatSession = ({
]); ]);
hydrationNonceRef.current += 1; hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = nextState.storageSessionId; storageSessionIdRef.current = nextState.storageSessionId;
sessionIdRef.current = nextState.sessionId; sessionIdRef.current = nextState.sessionId;
setBranchTransition(null); setBranchTransition(null);
@@ -501,6 +520,7 @@ export const useAgentChatSession = ({
if (!nextActiveSessionId) { if (!nextActiveSessionId) {
hydrationNonceRef.current += 1; hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = undefined; storageSessionIdRef.current = undefined;
sessionIdRef.current = undefined; sessionIdRef.current = undefined;
setBranchTransition(null); setBranchTransition(null);
@@ -517,6 +537,7 @@ export const useAgentChatSession = ({
listChatSessions(), listChatSessions(),
]); ]);
hydrationNonceRef.current += 1; hydrationNonceRef.current += 1;
titleUpdateNonceRef.current += 1;
storageSessionIdRef.current = nextState.storageSessionId; storageSessionIdRef.current = nextState.storageSessionId;
sessionIdRef.current = nextState.sessionId; sessionIdRef.current = nextState.sessionId;
setBranchTransition(null); setBranchTransition(null);
+27
View File
@@ -49,3 +49,30 @@ export const getAccessToken = async () => {
} }
return null; return null;
}; };
export const getUserId = async () => {
const session = await getSession();
const sessionUserId = typeof session?.user?.id === "string" ? session.user.id : null;
if (sessionUserId) {
return sessionUserId;
}
const accessToken = await getAccessToken();
if (!accessToken) {
return null;
}
const payload = decodeJwtPayload(accessToken);
if (!payload || typeof payload !== "object") {
return null;
}
const candidate =
typeof payload.sub === "string"
? payload.sub
: typeof payload.user_id === "string"
? payload.user_id
: null;
return candidate;
};
+3
View File
@@ -99,6 +99,7 @@ export const streamAgentChat = async ({
session_id: sessionId, session_id: sessionId,
}), }),
projectHeaderMode: "include", projectHeaderMode: "include",
userHeaderMode: "include",
skipAuthRedirect: true, skipAuthRedirect: true,
}, },
); );
@@ -229,6 +230,7 @@ export const abortAgentChat = async (sessionId?: string) => {
session_id: sessionId, session_id: sessionId,
}), }),
projectHeaderMode: "include", projectHeaderMode: "include",
userHeaderMode: "include",
skipAuthRedirect: true, skipAuthRedirect: true,
}); });
@@ -249,6 +251,7 @@ export const forkAgentChat = async (sessionId: string | undefined, keepMessageCo
keep_message_count: keepMessageCount, keep_message_count: keepMessageCount,
}), }),
projectHeaderMode: "include", projectHeaderMode: "include",
userHeaderMode: "include",
skipAuthRedirect: true, skipAuthRedirect: true,
}); });
+10 -1
View File
@@ -1,12 +1,14 @@
import { getAccessToken } from "@/lib/authToken"; import { getAccessToken, getUserId } from "@/lib/authToken";
import { useProjectStore } from "@/store/projectStore"; import { useProjectStore } from "@/store/projectStore";
export type AuthHeaderMode = "include" | "omit"; export type AuthHeaderMode = "include" | "omit";
export type ProjectHeaderMode = "auto" | "include" | "omit"; export type ProjectHeaderMode = "auto" | "include" | "omit";
export type UserHeaderMode = "include" | "omit";
export interface AuthContextHeaderOptions { export interface AuthContextHeaderOptions {
authHeaderMode?: AuthHeaderMode; authHeaderMode?: AuthHeaderMode;
projectHeaderMode?: ProjectHeaderMode; projectHeaderMode?: ProjectHeaderMode;
userHeaderMode?: UserHeaderMode;
} }
const shouldIncludeProjectHeader = ( const shouldIncludeProjectHeader = (
@@ -34,6 +36,13 @@ export const applyAuthContextHeaders = async (
headers.set("Authorization", `Bearer ${accessToken}`); headers.set("Authorization", `Bearer ${accessToken}`);
} }
if (options.userHeaderMode === "include") {
const userId = await getUserId();
if (userId) {
headers.set("X-User-Id", userId);
}
}
const projectId = useProjectStore.getState().currentProjectId; const projectId = useProjectStore.getState().currentProjectId;
if ( if (
projectId && projectId &&