release: prepare 0629001 portal updates
This commit is contained in:
@@ -17,9 +17,6 @@ import { buildContextPrefix } from '../utils/mindspaceChatContext';
|
||||
import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
import {
|
||||
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||
CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
|
||||
CHAT_IMAGE_MAX_SIDE,
|
||||
compressImageForUpload,
|
||||
} from '../utils/imageUpload';
|
||||
import { buildAbsoluteAssetImageUrl } from '../utils/mindspaceCards';
|
||||
import {
|
||||
@@ -94,9 +91,8 @@ export function usePageEditSubChat({
|
||||
|
||||
const space = await getMindSpace();
|
||||
const category =
|
||||
space.categories.find((item) => item.code === 'private') ??
|
||||
space.categories.find((item) => item.code === 'oa') ??
|
||||
space.categories.find((item) => item.code === 'public') ??
|
||||
space.categories.find((item) => item.code === 'oa') ??
|
||||
space.categories[0];
|
||||
if (!category) {
|
||||
throw new Error('当前空间暂无可用的图片上传分类');
|
||||
@@ -105,18 +101,23 @@ export function usePageEditSubChat({
|
||||
return category.id;
|
||||
}, []);
|
||||
|
||||
const uploadChatImage = useCallback(async (file: File): Promise<string> => {
|
||||
const uploadChatImage = useCallback(async (
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<string> => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('只支持图片文件');
|
||||
}
|
||||
const compressed = await compressImageForUpload(file, {
|
||||
maxInputBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||
maxOutputBytes: CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
|
||||
maxDimension: CHAT_IMAGE_MAX_SIDE,
|
||||
});
|
||||
const categoryId = await resolveChatImageUploadCategoryId();
|
||||
const asset = await uploadMindSpaceAsset(categoryId, compressed);
|
||||
return buildAbsoluteAssetImageUrl({ id: asset.id, updatedAt: asset.updatedAt });
|
||||
const asset = await uploadMindSpaceAsset(categoryId, file, {
|
||||
maxImageBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||
onProgress,
|
||||
});
|
||||
return buildAbsoluteAssetImageUrl({
|
||||
id: asset.id,
|
||||
updatedAt: asset.updatedAt,
|
||||
publicUrl: asset.publicUrl,
|
||||
});
|
||||
}, [resolveChatImageUploadCategoryId]);
|
||||
|
||||
const processEvent = useCallback((event: SessionEvent, requestId: string) => {
|
||||
|
||||
+319
-57
@@ -10,10 +10,10 @@ import {
|
||||
getMe,
|
||||
listNotifications,
|
||||
markNotificationRead,
|
||||
getSession,
|
||||
listSessions,
|
||||
loadSessionDetail,
|
||||
readConfig,
|
||||
rememberUserMemory,
|
||||
rememberProjectContext,
|
||||
uploadMindSpaceAsset,
|
||||
resumeSession,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
startSession,
|
||||
subscribeNotificationEvents,
|
||||
subscribeSessionEvents,
|
||||
syncUserMemory,
|
||||
updateProvider,
|
||||
} from '../api/client';
|
||||
import { appConfig } from '../config';
|
||||
@@ -36,6 +37,7 @@ import type {
|
||||
MindSpaceChatContext,
|
||||
PortalUser,
|
||||
Session,
|
||||
SessionSummary,
|
||||
SessionEvent,
|
||||
ToolConfirmation,
|
||||
UserNotification,
|
||||
@@ -45,9 +47,6 @@ import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
import { buildAutoChatSkillPrefix } from '../../chat-skills.mjs';
|
||||
import {
|
||||
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||
CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
|
||||
CHAT_IMAGE_MAX_SIDE,
|
||||
compressImageForUpload,
|
||||
} from '../utils/imageUpload';
|
||||
import { buildAbsoluteAssetImageUrl } from '../utils/mindspaceCards';
|
||||
import {
|
||||
@@ -61,9 +60,10 @@ import {
|
||||
pushMessage,
|
||||
} from '../utils/message';
|
||||
import {
|
||||
mergeSessionLists,
|
||||
appendSessionLists,
|
||||
prependUnique,
|
||||
shouldShowNewChatTitle,
|
||||
toSessionSummary,
|
||||
touchSession,
|
||||
} from '../utils/sessions';
|
||||
|
||||
@@ -72,6 +72,21 @@ const REPLY_RECOVERY_SYNC_DELAYS_MS = [1500, 5000, 12000];
|
||||
|
||||
export { INSUFFICIENT_BALANCE_NOTICE };
|
||||
|
||||
function mergeMessagePages(older: Message[], current: Message[]): Message[] {
|
||||
if (older.length === 0) return current;
|
||||
if (current.length === 0) return older;
|
||||
|
||||
const seen = new Set<string>();
|
||||
const merged: Message[] = [];
|
||||
for (const message of [...older, ...current]) {
|
||||
const key = message.id ?? `${message.role}:${message.created}:${merged.length}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
merged.push(message);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function isAmbiguousReplySubmitError(err: unknown) {
|
||||
if (!(err instanceof ApiError)) return true;
|
||||
return err.status === 0 || err.status === 409 || err.status >= 500;
|
||||
@@ -84,15 +99,23 @@ export function useTKMindChat(
|
||||
grantedSkills?: string[],
|
||||
) {
|
||||
const canUseProjectMemory = Boolean(capabilities?.context_memory);
|
||||
const canUseLongTermMemory = Boolean(capabilities?.memory_store);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [sessions, setSessions] = useState<SessionSummary[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||
const [sessionsLoadingMore, setSessionsLoadingMore] = useState(false);
|
||||
const [sessionsHasMore, setSessionsHasMore] = useState(false);
|
||||
const [sessionSearchQuery, setSessionSearchQueryState] = useState('');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [messageHistoryLoadingMore, setMessageHistoryLoadingMore] = useState(false);
|
||||
const [messageHistoryHasMore, setMessageHistoryHasMore] = useState(false);
|
||||
const [messageHistoryTotal, setMessageHistoryTotal] = useState(0);
|
||||
const [chatState, setChatState] = useState<ChatState>('loading');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [pendingTool, setPendingTool] = useState<ToolConfirmation | null>(null);
|
||||
const [memoryLoading, setMemoryLoading] = useState(false);
|
||||
const [userMemoryLoading, setUserMemoryLoading] = useState(false);
|
||||
const [rechargePrompt, setRechargePrompt] = useState(false);
|
||||
const [rechargeForced, setRechargeForced] = useState(false);
|
||||
const [subscribePrompt, setSubscribePrompt] = useState(false);
|
||||
@@ -104,7 +127,12 @@ export function useTKMindChat(
|
||||
const connectTokenRef = useRef(0);
|
||||
const messagesRef = useRef<Message[]>([]);
|
||||
const sessionRef = useRef<Session | null>(null);
|
||||
const sessionsRef = useRef<Session[]>([]);
|
||||
const sessionsRef = useRef<SessionSummary[]>([]);
|
||||
const sessionsOffsetRef = useRef(0);
|
||||
const sessionSearchQueryRef = useRef('');
|
||||
const messageHistoryLoadedCountRef = useRef(0);
|
||||
const messageHistoryTotalRef = useRef(0);
|
||||
const messageHistoryHasMoreRef = useRef(false);
|
||||
const rememberedContextRef = useRef<string | null>(null);
|
||||
const rememberInFlightRef = useRef(false);
|
||||
const fallbackRetriedRef = useRef(new Set<string>());
|
||||
@@ -187,6 +215,10 @@ export function useTKMindChat(
|
||||
messagesRef.current = messages;
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
messageHistoryHasMoreRef.current = messageHistoryHasMore;
|
||||
}, [messageHistoryHasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionRef.current = session;
|
||||
}, [session]);
|
||||
@@ -195,6 +227,10 @@ export function useTKMindChat(
|
||||
sessionsRef.current = sessions;
|
||||
}, [sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionSearchQueryRef.current = sessionSearchQuery;
|
||||
}, [sessionSearchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id) return;
|
||||
let cancelled = false;
|
||||
@@ -269,6 +305,27 @@ export function useTKMindChat(
|
||||
}
|
||||
}, [canUseProjectMemory]);
|
||||
|
||||
const formatUserMemoryNotice = useCallback((result: {
|
||||
analyzed: number;
|
||||
memories: number;
|
||||
totalMemories: number;
|
||||
syncedToSession: boolean;
|
||||
}) => {
|
||||
const fragments = [];
|
||||
if (result.memories > 0) {
|
||||
fragments.push(`新增 ${result.memories} 条长期记忆`);
|
||||
} else if (result.analyzed > 0) {
|
||||
fragments.push('已分析最近对话,暂无新的长期记忆');
|
||||
} else {
|
||||
fragments.push('没有新的用户对话可提炼');
|
||||
}
|
||||
fragments.push(`当前累计 ${result.totalMemories} 条`);
|
||||
if (result.syncedToSession) {
|
||||
fragments.push('已同步到当前会话');
|
||||
}
|
||||
return fragments.join(',');
|
||||
}, []);
|
||||
|
||||
const rememberRecentContext = useCallback(
|
||||
async ({
|
||||
silent = false,
|
||||
@@ -345,28 +402,80 @@ export function useTKMindChat(
|
||||
[canUseProjectMemory, loadProjectMemory],
|
||||
);
|
||||
|
||||
const refreshSessions = useCallback(async () => {
|
||||
setSessionsLoading(true);
|
||||
const refreshSessions = useCallback(
|
||||
async (options?: { preserveExisting?: boolean; query?: string }): Promise<SessionSummary[]> => {
|
||||
setSessionsLoading(true);
|
||||
try {
|
||||
const { items, page } = await listSessions({
|
||||
limit: appConfig.sessionPageSize,
|
||||
offset: 0,
|
||||
query: options?.query ?? sessionSearchQueryRef.current,
|
||||
});
|
||||
const merged = options?.preserveExisting ? appendSessionLists(sessionsRef.current, items) : items;
|
||||
const nextOffset = Number(page.offset ?? 0) + items.length;
|
||||
const total = Math.max(Number(page.total ?? merged.length), merged.length);
|
||||
sessionsOffsetRef.current = options?.preserveExisting
|
||||
? Math.max(sessionsOffsetRef.current, nextOffset)
|
||||
: nextOffset;
|
||||
setSessionsHasMore(merged.length < total);
|
||||
if (options?.query != null) {
|
||||
sessionSearchQueryRef.current = options.query;
|
||||
setSessionSearchQueryState(options.query);
|
||||
}
|
||||
setSessions(merged);
|
||||
return items;
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 0) {
|
||||
setError('网络不可用,无法刷新历史列表');
|
||||
}
|
||||
return [];
|
||||
} finally {
|
||||
setSessionsLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const loadMoreSessions = useCallback(async () => {
|
||||
if (sessionsLoading || sessionsLoadingMore || !sessionsHasMore) return;
|
||||
setSessionsLoadingMore(true);
|
||||
try {
|
||||
const items = await listSessions();
|
||||
setSessions((prev) => mergeSessionLists(prev, items));
|
||||
const { items, page } = await listSessions({
|
||||
limit: appConfig.sessionPageSize,
|
||||
offset: sessionsOffsetRef.current,
|
||||
query: sessionSearchQueryRef.current,
|
||||
});
|
||||
const merged = appendSessionLists(sessionsRef.current, items);
|
||||
const nextOffset = Number(page.offset ?? sessionsOffsetRef.current) + items.length;
|
||||
const total = Math.max(Number(page.total ?? merged.length), merged.length);
|
||||
sessionsOffsetRef.current = Math.max(sessionsOffsetRef.current, nextOffset);
|
||||
setSessionsHasMore(merged.length < total);
|
||||
setSessions(merged);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 0) {
|
||||
setError('网络不可用,无法刷新历史列表');
|
||||
setError('网络不可用,无法继续加载历史列表');
|
||||
}
|
||||
} finally {
|
||||
setSessionsLoading(false);
|
||||
setSessionsLoadingMore(false);
|
||||
}
|
||||
}, []);
|
||||
}, [sessionsHasMore, sessionsLoading, sessionsLoadingMore]);
|
||||
|
||||
const setSessionSearchQuery = useCallback((query: string) => {
|
||||
const nextQuery = query.trim();
|
||||
sessionSearchQueryRef.current = nextQuery;
|
||||
setSessionSearchQueryState(nextQuery);
|
||||
sessionsOffsetRef.current = 0;
|
||||
setSessionsHasMore(false);
|
||||
void refreshSessions({ query: nextQuery });
|
||||
}, [refreshSessions]);
|
||||
|
||||
const resolveChatImageUploadCategoryId = useCallback(async () => {
|
||||
if (chatImageCategoryIdRef.current) return chatImageCategoryIdRef.current;
|
||||
|
||||
const space = await getMindSpace();
|
||||
const category =
|
||||
space.categories.find((item) => item.code === 'private') ??
|
||||
space.categories.find((item) => item.code === 'oa') ??
|
||||
space.categories.find((item) => item.code === 'public') ??
|
||||
space.categories.find((item) => item.code === 'oa') ??
|
||||
space.categories[0];
|
||||
if (!category) {
|
||||
throw new Error('当前空间暂无可用的图片上传分类');
|
||||
@@ -376,32 +485,48 @@ export function useTKMindChat(
|
||||
return category.id;
|
||||
}, []);
|
||||
|
||||
const uploadChatImage = useCallback(async (file: File): Promise<string> => {
|
||||
const uploadChatImage = useCallback(async (
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<string> => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('只支持图片文件');
|
||||
}
|
||||
const compressed = await compressImageForUpload(file, {
|
||||
maxInputBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||
maxOutputBytes: CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
|
||||
maxDimension: CHAT_IMAGE_MAX_SIDE,
|
||||
});
|
||||
const categoryId = await resolveChatImageUploadCategoryId();
|
||||
const asset = await uploadMindSpaceAsset(categoryId, compressed);
|
||||
const asset = await uploadMindSpaceAsset(categoryId, file, {
|
||||
maxImageBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
|
||||
onProgress,
|
||||
});
|
||||
return buildAbsoluteAssetImageUrl({
|
||||
id: asset.id,
|
||||
updatedAt: asset.updatedAt,
|
||||
publicUrl: asset.publicUrl,
|
||||
});
|
||||
}, [resolveChatImageUploadCategoryId]);
|
||||
|
||||
const syncSessionMessages = useCallback(async (sessionId: string) => {
|
||||
try {
|
||||
const detail = await loadSessionDetail(sessionId);
|
||||
setSessions((prev) => prependUnique(prev, detail.session));
|
||||
const currentLoadedCount = Math.max(
|
||||
messageHistoryLoadedCountRef.current,
|
||||
messagesRef.current.length,
|
||||
appConfig.sessionMessagePageSize,
|
||||
);
|
||||
const detail = await loadSessionDetail(sessionId, undefined, {
|
||||
before: 0,
|
||||
limit: currentLoadedCount,
|
||||
});
|
||||
setSessions((prev) => prependUnique(prev, toSessionSummary(detail.session)));
|
||||
if (sessionRef.current?.id !== sessionId) return;
|
||||
messagesRef.current = detail.messages;
|
||||
messageHistoryLoadedCountRef.current = detail.messages.length;
|
||||
messageHistoryTotalRef.current = Math.max(Number(detail.page.total ?? detail.messages.length), detail.messages.length);
|
||||
setMessageHistoryHasMore(detail.messages.length < messageHistoryTotalRef.current);
|
||||
setMessageHistoryTotal(messageHistoryTotalRef.current);
|
||||
setMessages(detail.messages);
|
||||
setSession((current) => (current?.id === sessionId ? detail.session : current));
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn('Session final sync failed:', err);
|
||||
setNotice('对话已完成,但最终内容同步失败;当前显示可能仍是临时流式结果,请稍后刷新。');
|
||||
// Keep the optimistic streamed state if the follow-up sync fails.
|
||||
}
|
||||
}, []);
|
||||
@@ -468,6 +593,7 @@ export function useTKMindChat(
|
||||
setMessages((prev) => {
|
||||
const next = pushMessage(prev, event.message);
|
||||
messagesRef.current = next;
|
||||
messageHistoryLoadedCountRef.current = next.length;
|
||||
return next;
|
||||
});
|
||||
const confirmation = getToolConfirmation(event.message);
|
||||
@@ -480,10 +606,15 @@ export function useTKMindChat(
|
||||
return;
|
||||
}
|
||||
case 'UpdateConversation': {
|
||||
const snapshot = normalizeConversationMessages(
|
||||
let snapshot = normalizeConversationMessages(
|
||||
event.conversation.filter((m) => m.metadata?.userVisible),
|
||||
);
|
||||
const loadedCount = Math.max(messageHistoryLoadedCountRef.current, messagesRef.current.length);
|
||||
if (messageHistoryHasMoreRef.current && loadedCount > 0 && snapshot.length > loadedCount) {
|
||||
snapshot = snapshot.slice(-loadedCount);
|
||||
}
|
||||
messagesRef.current = mergeConversationSnapshot(messagesRef.current, snapshot);
|
||||
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||
setMessages(messagesRef.current);
|
||||
return;
|
||||
}
|
||||
@@ -589,6 +720,12 @@ export function useTKMindChat(
|
||||
setPendingTool(null);
|
||||
activeRequestId.current = null;
|
||||
messagesRef.current = [];
|
||||
messageHistoryLoadedCountRef.current = 0;
|
||||
messageHistoryTotalRef.current = 0;
|
||||
messageHistoryHasMoreRef.current = false;
|
||||
setMessageHistoryHasMore(false);
|
||||
setMessageHistoryTotal(0);
|
||||
setMessageHistoryLoadingMore(false);
|
||||
setMessages([]);
|
||||
setChatState('connecting');
|
||||
}, []);
|
||||
@@ -619,15 +756,29 @@ export function useTKMindChat(
|
||||
const hints = knownSession
|
||||
? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
|
||||
: undefined;
|
||||
const { session: detail, messages: history } = await loadSessionDetail(sessionId, hints);
|
||||
const { session: detail, messages: history, page } = await loadSessionDetail(
|
||||
sessionId,
|
||||
hints,
|
||||
{
|
||||
before: 0,
|
||||
limit: appConfig.sessionMessagePageSize,
|
||||
},
|
||||
);
|
||||
if (token !== connectTokenRef.current) return;
|
||||
|
||||
writeStoredSessionId(userRef.current?.id, sessionId);
|
||||
setSession({ ...detail, ...(resumed ?? {}), id: sessionId });
|
||||
messagesRef.current = history;
|
||||
messageHistoryLoadedCountRef.current = history.length;
|
||||
messageHistoryTotalRef.current = Math.max(Number(page.total ?? history.length), history.length);
|
||||
messageHistoryHasMoreRef.current = history.length < messageHistoryTotalRef.current;
|
||||
setMessageHistoryHasMore(messageHistoryHasMoreRef.current);
|
||||
setMessageHistoryTotal(messageHistoryTotalRef.current);
|
||||
setMessages(history);
|
||||
setChatState('idle');
|
||||
setSessions((prev) => prependUnique(prev, { ...detail, ...resumed, id: sessionId }));
|
||||
setSessions((prev) =>
|
||||
prependUnique(prev, toSessionSummary({ ...detail, ...(resumed ?? {}), id: sessionId })),
|
||||
);
|
||||
|
||||
subscribeToSession(sessionId);
|
||||
},
|
||||
@@ -646,6 +797,43 @@ export function useTKMindChat(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadOlderMessages = useCallback(async () => {
|
||||
const sessionId = sessionRef.current?.id;
|
||||
if (!sessionId || messageHistoryLoadingMore || !messageHistoryHasMoreRef.current) return;
|
||||
|
||||
const knownSession = sessionsRef.current.find((item) => item.id === sessionId);
|
||||
const hints = knownSession
|
||||
? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
|
||||
: undefined;
|
||||
|
||||
setMessageHistoryLoadingMore(true);
|
||||
try {
|
||||
const { messages: olderMessages, page } = await loadSessionDetail(
|
||||
sessionId,
|
||||
hints,
|
||||
{
|
||||
before: messageHistoryLoadedCountRef.current,
|
||||
limit: appConfig.sessionMessagePageSize,
|
||||
},
|
||||
);
|
||||
if (sessionRef.current?.id !== sessionId) return;
|
||||
const merged = mergeMessagePages(olderMessages, messagesRef.current);
|
||||
messagesRef.current = merged;
|
||||
messageHistoryLoadedCountRef.current = merged.length;
|
||||
messageHistoryTotalRef.current = Math.max(Number(page.total ?? merged.length), merged.length);
|
||||
messageHistoryHasMoreRef.current = merged.length < messageHistoryTotalRef.current;
|
||||
setMessageHistoryHasMore(messageHistoryHasMoreRef.current);
|
||||
setMessageHistoryTotal(messageHistoryTotalRef.current);
|
||||
setMessages(merged);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 0) {
|
||||
setError('网络不可用,无法继续加载更早消息');
|
||||
}
|
||||
} finally {
|
||||
setMessageHistoryLoadingMore(false);
|
||||
}
|
||||
}, [messageHistoryLoadingMore]);
|
||||
|
||||
const retryConnect = useCallback(async () => {
|
||||
const sessionId = session?.id ?? readStoredSessionId(userRef.current?.id);
|
||||
if (!sessionId) return;
|
||||
@@ -669,45 +857,54 @@ export function useTKMindChat(
|
||||
setChatState('loading');
|
||||
setError(null);
|
||||
|
||||
const summaries = await refreshSessions();
|
||||
|
||||
const previousSessionId = readStoredSessionId(userRef.current?.id);
|
||||
let staleSession = false;
|
||||
let restorableSessionId: string | null = null;
|
||||
|
||||
if (previousSessionId) {
|
||||
try {
|
||||
const previousSession = await getSession(previousSessionId);
|
||||
if (shouldShowNewChatTitle(previousSession)) {
|
||||
try {
|
||||
await deleteChatSession(previousSessionId);
|
||||
} catch {
|
||||
// Best-effort cleanup: a failed delete should not block the next fresh chat.
|
||||
}
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
} else {
|
||||
restorableSessionId = previousSessionId;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
staleSession = true;
|
||||
} else {
|
||||
throw err;
|
||||
const previousSession = summaries.find((item) => item.id === previousSessionId) ?? null;
|
||||
if (previousSession && shouldShowNewChatTitle(previousSession)) {
|
||||
try {
|
||||
await deleteChatSession(previousSessionId);
|
||||
} catch {
|
||||
// Best-effort cleanup: a failed delete should not block the next fresh chat.
|
||||
}
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
setSessions((prev) => prev.filter((item) => item.id !== previousSessionId));
|
||||
} else {
|
||||
restorableSessionId = previousSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
if (staleSession) {
|
||||
setNotice('上次会话已失效,已为你新建对话');
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
if (restorableSessionId) {
|
||||
await connectSessionRef.current(restorableSessionId, { showLoading: false });
|
||||
try {
|
||||
await connectSessionRef.current(restorableSessionId, { showLoading: false });
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
staleSession = true;
|
||||
setSession(null);
|
||||
messageHistoryLoadedCountRef.current = 0;
|
||||
messageHistoryTotalRef.current = 0;
|
||||
messageHistoryHasMoreRef.current = false;
|
||||
setMessageHistoryHasMore(false);
|
||||
setMessageHistoryTotal(0);
|
||||
setMessages([]);
|
||||
setChatState('idle');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (cancelled) return;
|
||||
} else {
|
||||
setChatState('idle');
|
||||
}
|
||||
void refreshSessions();
|
||||
if (staleSession) {
|
||||
setNotice('上次会话已失效,已为你新建对话');
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (err instanceof ApiError && err.status === 402) {
|
||||
@@ -732,7 +929,7 @@ export function useTKMindChat(
|
||||
useEffect(() => {
|
||||
const handleVisibility = () => {
|
||||
if (!document.hidden && session?.id) {
|
||||
void refreshSessions();
|
||||
void refreshSessions({ preserveExisting: true });
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
@@ -747,7 +944,11 @@ export function useTKMindChat(
|
||||
const token = connectTokenRef.current;
|
||||
const target = sessions.find((item) => item.id === sessionId);
|
||||
if (target) {
|
||||
setSession(target);
|
||||
setSession({
|
||||
...target,
|
||||
working_dir: userRef.current?.workspaceRoot ?? '',
|
||||
conversation: null,
|
||||
});
|
||||
writeStoredSessionId(userRef.current?.id, sessionId);
|
||||
}
|
||||
|
||||
@@ -794,6 +995,7 @@ export function useTKMindChat(
|
||||
const requestId = crypto.randomUUID();
|
||||
activeRequestId.current = requestId;
|
||||
messagesRef.current = [...messagesRef.current, userMessage];
|
||||
messageHistoryLoadedCountRef.current = messagesRef.current.length;
|
||||
setMessages(messagesRef.current);
|
||||
setChatState('streaming');
|
||||
setError(null);
|
||||
@@ -807,7 +1009,7 @@ export function useTKMindChat(
|
||||
activeSessionId = created.id;
|
||||
writeStoredSessionId(userRef.current?.id, created.id);
|
||||
setSession(created);
|
||||
setSessions((prev) => prependUnique(prev, created));
|
||||
setSessions((prev) => prependUnique(prev, toSessionSummary(created)));
|
||||
subscribeToSession(created.id);
|
||||
void ensureProvider(created.id);
|
||||
void loadProjectMemory(created.id, false);
|
||||
@@ -894,6 +1096,12 @@ export function useTKMindChat(
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
activeRequestId.current = null;
|
||||
messagesRef.current = [];
|
||||
messageHistoryLoadedCountRef.current = 0;
|
||||
messageHistoryTotalRef.current = 0;
|
||||
messageHistoryHasMoreRef.current = false;
|
||||
setMessageHistoryHasMore(false);
|
||||
setMessageHistoryTotal(0);
|
||||
setMessageHistoryLoadingMore(false);
|
||||
setMessages([]);
|
||||
setSession(null);
|
||||
setError(null);
|
||||
@@ -906,13 +1114,53 @@ export function useTKMindChat(
|
||||
await loadProjectMemory(session.id, true);
|
||||
}, [chatState, loadProjectMemory, session]);
|
||||
|
||||
const rememberCurrentUserMemory = useCallback(async () => {
|
||||
if (!session || !canUseLongTermMemory) return;
|
||||
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
|
||||
setUserMemoryLoading(true);
|
||||
try {
|
||||
const result = await rememberUserMemory(session.id);
|
||||
setNotice(formatUserMemoryNotice(result));
|
||||
} catch (err) {
|
||||
setNotice(`保存长期记忆失败:${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setUserMemoryLoading(false);
|
||||
}
|
||||
}, [canUseLongTermMemory, chatState, formatUserMemoryNotice, session]);
|
||||
|
||||
const refreshUserMemory = useCallback(async () => {
|
||||
if (!session || !canUseLongTermMemory) return;
|
||||
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
|
||||
setUserMemoryLoading(true);
|
||||
try {
|
||||
const result = await syncUserMemory(session.id);
|
||||
setNotice(
|
||||
result.syncedToSession
|
||||
? `长期记忆已同步(累计 ${result.totalMemories} 条)`
|
||||
: '长期记忆已刷新,但当前会话尚未同步',
|
||||
);
|
||||
} catch (err) {
|
||||
setNotice(`刷新长期记忆失败:${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
setUserMemoryLoading(false);
|
||||
}
|
||||
}, [canUseLongTermMemory, chatState, session]);
|
||||
|
||||
const rememberCurrentContext = useCallback(async () => {
|
||||
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
|
||||
await rememberRecentContext({ silent: false });
|
||||
}, [chatState, rememberRecentContext]);
|
||||
|
||||
const onSidebarOpen = useCallback(() => {
|
||||
void refreshSessions();
|
||||
if (sessionSearchQueryRef.current) {
|
||||
void refreshSessions({ query: sessionSearchQueryRef.current });
|
||||
return;
|
||||
}
|
||||
if (sessionsRef.current.length === 0) {
|
||||
void refreshSessions();
|
||||
return;
|
||||
}
|
||||
void refreshSessions({ preserveExisting: true });
|
||||
}, [refreshSessions]);
|
||||
|
||||
const deleteSession = useCallback(
|
||||
@@ -950,21 +1198,35 @@ export function useTKMindChat(
|
||||
session,
|
||||
sessions,
|
||||
sessionsLoading,
|
||||
sessionsLoadingMore,
|
||||
sessionsHasMore,
|
||||
sessionSearchQuery,
|
||||
messages,
|
||||
messageHistoryLoadingMore,
|
||||
messageHistoryHasMore,
|
||||
messageHistoryTotal,
|
||||
chatState,
|
||||
error,
|
||||
notice,
|
||||
pendingTool,
|
||||
memoryLoading,
|
||||
userMemoryLoading,
|
||||
canUseProjectMemory,
|
||||
canUseLongTermMemory,
|
||||
submit,
|
||||
stop,
|
||||
approveTool,
|
||||
newSession,
|
||||
rememberCurrentContext,
|
||||
refreshProjectMemory,
|
||||
rememberCurrentUserMemory,
|
||||
refreshUserMemory,
|
||||
switchSession,
|
||||
deleteSession,
|
||||
refreshSessions,
|
||||
loadMoreSessions,
|
||||
setSessionSearchQuery,
|
||||
loadOlderMessages,
|
||||
retryConnect,
|
||||
dismissNotice,
|
||||
onSidebarOpen,
|
||||
|
||||
Reference in New Issue
Block a user