feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力

含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-30 09:30:51 +08:00
parent b1b8d3afc6
commit 722b18326f
53 changed files with 2450 additions and 313 deletions
+92 -14
View File
@@ -116,6 +116,61 @@ function isAmbiguousReplySubmitError(err: unknown) {
return err.status === 0 || err.status === 409 || err.status >= 500;
}
const SESSION_LIST_RETRY_ATTEMPTS = 3;
const SESSION_LIST_RETRY_DELAY_MS = 450;
function isTransientSessionListError(err: unknown) {
return err instanceof ApiError && (err.status === 0 || err.status >= 502);
}
function sessionListFailureMessage(err: unknown, action: 'refresh' | 'load-more') {
const fallback =
action === 'refresh' ? '历史列表暂时无法加载,请稍后重试' : '暂时无法加载更多历史,请稍后重试';
if (!(err instanceof ApiError)) return fallback;
if (err.status === 0) {
return action === 'refresh'
? '无法连接后端,历史列表未刷新(聊天仍可使用)'
: '无法连接后端,暂时无法加载更多历史';
}
return err.message?.trim() || fallback;
}
async function fetchSessionListWithRetry(
options?: Parameters<typeof listSessions>[0],
): Promise<Awaited<ReturnType<typeof listSessions>>> {
let lastErr: unknown;
for (let attempt = 0; attempt < SESSION_LIST_RETRY_ATTEMPTS; attempt += 1) {
try {
return await listSessions(options);
} catch (err) {
lastErr = err;
if (!isTransientSessionListError(err) || attempt === SESSION_LIST_RETRY_ATTEMPTS - 1) break;
await new Promise((resolve) => window.setTimeout(resolve, SESSION_LIST_RETRY_DELAY_MS * (attempt + 1)));
}
}
throw lastErr;
}
function isTransientConnectError(err: unknown) {
if (!(err instanceof ApiError)) return false;
if (err.status === 0 || err.status >= 502) return true;
return /超时|timeout/i.test(err.message);
}
async function withTransientConnectRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
let lastErr: unknown;
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (!isTransientConnectError(err) || attempt === attempts - 1) break;
await new Promise((resolve) => window.setTimeout(resolve, 600 * (attempt + 1)));
}
}
throw lastErr;
}
export function useTKMindChat(
user?: PortalUser | null,
onUserUpdate?: (user: PortalUser) => void,
@@ -437,7 +492,7 @@ export function useTKMindChat(
async (options?: { preserveExisting?: boolean; query?: string }): Promise<SessionSummary[]> => {
setSessionsLoading(true);
try {
const { items, page } = await listSessions({
const { items, page } = await fetchSessionListWithRetry({
limit: appConfig.sessionPageSize,
offset: 0,
query: options?.query ?? sessionSearchQueryRef.current,
@@ -456,8 +511,8 @@ export function useTKMindChat(
setSessions(merged);
return items;
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
setError('网络不可用,无法刷新历史列表');
if (err instanceof ApiError || err instanceof Error) {
setNotice(sessionListFailureMessage(err, 'refresh'));
}
return [];
} finally {
@@ -471,7 +526,7 @@ export function useTKMindChat(
if (sessionsLoading || sessionsLoadingMore || !sessionsHasMore) return;
setSessionsLoadingMore(true);
try {
const { items, page } = await listSessions({
const { items, page } = await fetchSessionListWithRetry({
limit: appConfig.sessionPageSize,
offset: sessionsOffsetRef.current,
query: sessionSearchQueryRef.current,
@@ -483,8 +538,8 @@ export function useTKMindChat(
setSessionsHasMore(merged.length < total);
setSessions(merged);
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
setError('网络不可用,无法继续加载历史列表');
if (isTransientSessionListError(err) || err instanceof ApiError) {
setNotice(sessionListFailureMessage(err, 'load-more'));
}
} finally {
setSessionsLoadingMore(false);
@@ -808,7 +863,12 @@ export function useTKMindChat(
const connectSession = useCallback(
async (
sessionId: string,
options?: { showLoading?: boolean; skipResume?: boolean; seedSession?: Session },
options?: {
showLoading?: boolean;
skipResume?: boolean;
seedSession?: Session;
skipReconcile?: boolean;
},
) => {
const showLoading = options?.showLoading !== false;
const token = ++connectTokenRef.current;
@@ -825,20 +885,22 @@ export function useTKMindChat(
const resumed = options?.skipResume
? (options.seedSession ?? null)
: await resumeSession(sessionId);
: await withTransientConnectRetry(() =>
resumeSession(sessionId, {
skipReconcile: options?.skipReconcile ?? false,
}),
);
if (token !== connectTokenRef.current) return;
const knownSession = sessionsRef.current.find((s) => s.id === sessionId);
const hints = knownSession
? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
: undefined;
const { session: detail, messages: history, page } = await loadSessionDetail(
sessionId,
hints,
{
const { session: detail, messages: history, page } = await withTransientConnectRetry(() =>
loadSessionDetail(sessionId, hints, {
before: 0,
limit: appConfig.sessionMessagePageSize,
},
}),
);
if (token !== connectTokenRef.current) return;
@@ -957,7 +1019,10 @@ export function useTKMindChat(
if (cancelled) return;
if (restorableSessionId) {
try {
await connectSessionRef.current(restorableSessionId, { showLoading: false });
await connectSessionRef.current(restorableSessionId, {
showLoading: false,
skipReconcile: true,
});
} catch (err) {
if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
clearStoredSessionId(userRef.current?.id);
@@ -970,6 +1035,17 @@ export function useTKMindChat(
setMessageHistoryTotal(0);
setMessages([]);
setChatState('idle');
} else if (isTransientConnectError(err)) {
clearStoredSessionId(userRef.current?.id);
setSession(null);
messageHistoryLoadedCountRef.current = 0;
messageHistoryTotalRef.current = 0;
messageHistoryHasMoreRef.current = false;
setMessageHistoryHasMore(false);
setMessageHistoryTotal(0);
setMessages([]);
setChatState('idle');
setNotice('上次会话恢复超时,已为你准备新对话,可直接发送消息');
} else {
throw err;
}
@@ -1311,6 +1387,8 @@ export function useTKMindChat(
userMemoryLoading,
canUseProjectMemory,
canUseLongTermMemory,
capabilities,
grantedSkills,
submit,
stop,
approveTool,