merge: 0629001 into 0629002
合并反馈、语音 ASR、MindSpace 修复等 0629001 发布改动,并与 Agent Runs 网关改动完成冲突解决。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,9 +18,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 {
|
||||
@@ -110,9 +107,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('当前空间暂无可用的图片上传分类');
|
||||
@@ -121,18 +117,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) => {
|
||||
|
||||
+318
-56
@@ -12,15 +12,16 @@ import {
|
||||
getMe,
|
||||
listNotifications,
|
||||
markNotificationRead,
|
||||
getSession,
|
||||
listSessions,
|
||||
loadSessionDetail,
|
||||
readConfig,
|
||||
rememberUserMemory,
|
||||
rememberProjectContext,
|
||||
uploadMindSpaceAsset,
|
||||
resumeSession,
|
||||
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';
|
||||
|
||||
@@ -86,6 +86,21 @@ async function waitForAgentRun(runId: string) {
|
||||
throw new Error('后台任务仍在排队,请稍后刷新会话查看结果');
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -98,15 +113,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);
|
||||
@@ -118,7 +141,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>());
|
||||
@@ -201,6 +229,10 @@ export function useTKMindChat(
|
||||
messagesRef.current = messages;
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
messageHistoryHasMoreRef.current = messageHistoryHasMore;
|
||||
}, [messageHistoryHasMore]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionRef.current = session;
|
||||
}, [session]);
|
||||
@@ -209,6 +241,10 @@ export function useTKMindChat(
|
||||
sessionsRef.current = sessions;
|
||||
}, [sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionSearchQueryRef.current = sessionSearchQuery;
|
||||
}, [sessionSearchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id) return;
|
||||
let cancelled = false;
|
||||
@@ -283,6 +319,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,
|
||||
@@ -359,28 +416,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('当前空间暂无可用的图片上传分类');
|
||||
@@ -390,32 +499,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.
|
||||
}
|
||||
}, []);
|
||||
@@ -488,6 +613,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);
|
||||
@@ -500,10 +626,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;
|
||||
}
|
||||
@@ -609,6 +740,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');
|
||||
}, []);
|
||||
@@ -639,15 +776,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);
|
||||
},
|
||||
@@ -666,6 +817,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;
|
||||
@@ -689,45 +877,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) {
|
||||
@@ -752,7 +949,7 @@ export function useTKMindChat(
|
||||
useEffect(() => {
|
||||
const handleVisibility = () => {
|
||||
if (!document.hidden && session?.id) {
|
||||
void refreshSessions();
|
||||
void refreshSessions({ preserveExisting: true });
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
@@ -767,7 +964,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);
|
||||
}
|
||||
|
||||
@@ -819,6 +1020,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('waiting');
|
||||
setError(null);
|
||||
@@ -926,6 +1128,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);
|
||||
@@ -938,6 +1146,38 @@ 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' ||
|
||||
@@ -949,7 +1189,15 @@ export function useTKMindChat(
|
||||
}, [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(
|
||||
@@ -987,21 +1235,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,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { transcribeOneShot } from '../voice/asrTransport';
|
||||
import { mapMicError } from '../voice/audioAnalyser';
|
||||
import { shouldFallbackToServerAsr, shouldPreferServerAsr } from '../voice/capabilities';
|
||||
import { shouldFallbackToServerAsr, shouldUseLiveSpeech } from '../voice/capabilities';
|
||||
import { MicCapture } from '../voice/micCapture';
|
||||
import { waitForMicRelease } from '../voice/micSession';
|
||||
import { createLiveSpeechRecognition, isSpeechRecognitionSupported } from '../voice/speechRecognition';
|
||||
import { createLiveSpeechRecognition } from '../voice/speechRecognition';
|
||||
import type { VoiceSessionPhase } from '../voice/types';
|
||||
|
||||
const MIN_RECORD_MS = 500;
|
||||
@@ -19,9 +19,7 @@ export function useVoiceSession({
|
||||
const [phase, setPhase] = useState<VoiceSessionPhase>('idle');
|
||||
const [text, setText] = useState('');
|
||||
const [analyser, setAnalyser] = useState<AnalyserNode | null>(null);
|
||||
const [liveRecognition, setLiveRecognition] = useState(
|
||||
isSpeechRecognitionSupported() && !shouldPreferServerAsr(),
|
||||
);
|
||||
const [liveRecognition, setLiveRecognition] = useState(shouldUseLiveSpeech());
|
||||
|
||||
const committedRef = useRef('');
|
||||
const interimRef = useRef('');
|
||||
@@ -155,7 +153,7 @@ export function useVoiceSession({
|
||||
if (sessionRef.current !== sessionId) return;
|
||||
|
||||
startedAtRef.current = Date.now();
|
||||
const useLiveSpeech = isSpeechRecognitionSupported() && !shouldPreferServerAsr();
|
||||
const useLiveSpeech = shouldUseLiveSpeech();
|
||||
|
||||
if (useLiveSpeech) {
|
||||
const recognition = createLiveSpeechRecognition({
|
||||
|
||||
Reference in New Issue
Block a user