feat(mindspace): 0630004 空间 UI、聊天连接、微信分享与 Agent 能力
含 MindSpace 三列布局与统计修复、聊天加载态与连接降级、平台页脚标记与 og:site_name 微信卡片、勾选资料删除 Agent 接口及内部话术过滤。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+92
-14
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user