feat: chat uploads, vision turn isolation, and MindSpace agent improvements
Add chat file/image upload UX, attachment proxying, vision thumbnails, and per-turn image scoping so agents only use the current upload. Extend MindSpace asset context, billing token state, OA/scenario verify scripts, and related runtime config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
} from '../api/client';
|
||||
import type { AgentRun } from '../api/client';
|
||||
import { appConfig } from '../config';
|
||||
import { BALANCE_REFRESH_EVENT, isRechargeNotification, requestBalanceRefresh } from '../utils/balanceRefresh';
|
||||
import {
|
||||
clearStoredSessionId,
|
||||
readStoredSessionId,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
} from '../utils/sessionStorage';
|
||||
import type {
|
||||
CapabilityMap,
|
||||
ChatFileAttachment,
|
||||
ChatState,
|
||||
Message,
|
||||
MindSpaceChatContext,
|
||||
@@ -340,6 +342,7 @@ export function useTKMindChat(
|
||||
const userRef = useRef(user);
|
||||
const onUserUpdateRef = useRef(onUserUpdate);
|
||||
const chatImageCategoryIdRef = useRef<string | null>(null);
|
||||
const chatFileCategoryIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
chatStateRef.current = chatState;
|
||||
@@ -422,6 +425,19 @@ export function useTKMindChat(
|
||||
onUserUpdateRef.current = onUserUpdate;
|
||||
}, [onUserUpdate]);
|
||||
|
||||
const refreshUserBalance = useCallback(() => {
|
||||
if (!onUserUpdateRef.current) return;
|
||||
void getMe()
|
||||
.then(({ user: fresh }) => onUserUpdateRef.current?.(fresh))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleBalanceRefresh = () => refreshUserBalance();
|
||||
window.addEventListener(BALANCE_REFRESH_EVENT, handleBalanceRefresh);
|
||||
return () => window.removeEventListener(BALANCE_REFRESH_EVENT, handleBalanceRefresh);
|
||||
}, [refreshUserBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages;
|
||||
}, [messages]);
|
||||
@@ -453,6 +469,9 @@ export function useTKMindChat(
|
||||
seenNotificationIdsRef.current.add(notification.id);
|
||||
setActiveNotification(notification);
|
||||
setNotice(`${notification.title}\n${notification.body}`.trim());
|
||||
if (isRechargeNotification(notification)) {
|
||||
requestBalanceRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
const pullNotifications = async () => {
|
||||
@@ -715,6 +734,46 @@ export function useTKMindChat(
|
||||
});
|
||||
}, [resolveChatImageUploadCategoryId]);
|
||||
|
||||
const resolveChatFileUploadCategoryId = useCallback(async () => {
|
||||
if (chatFileCategoryIdRef.current) return chatFileCategoryIdRef.current;
|
||||
|
||||
const space = await getMindSpace();
|
||||
const category =
|
||||
space.categories.find((item) => item.code === 'oa') ??
|
||||
space.categories[0];
|
||||
if (!category) {
|
||||
throw new Error('当前空间暂无可用的附件上传分类');
|
||||
}
|
||||
|
||||
chatFileCategoryIdRef.current = category.id;
|
||||
return category.id;
|
||||
}, []);
|
||||
|
||||
const uploadChatAttachment = useCallback(async (
|
||||
file: File,
|
||||
onProgress?: (progress: number) => void,
|
||||
options: { messageId?: string | null } = {},
|
||||
) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
throw new Error('请使用图片上传入口发送图片');
|
||||
}
|
||||
const categoryId = await resolveChatFileUploadCategoryId();
|
||||
const asset = await uploadMindSpaceAsset(categoryId, file, {
|
||||
onProgress,
|
||||
sessionId: sessionRef.current?.id ?? null,
|
||||
messageId: options.messageId ?? null,
|
||||
});
|
||||
return {
|
||||
assetId: asset.id,
|
||||
downloadUrl: buildAbsoluteAssetDownloadUrl({
|
||||
id: asset.id,
|
||||
updatedAt: asset.updatedAt,
|
||||
}),
|
||||
filename: asset.displayName || asset.filename || file.name,
|
||||
mimeType: asset.mimeType || file.type || 'application/octet-stream',
|
||||
};
|
||||
}, [resolveChatFileUploadCategoryId]);
|
||||
|
||||
const syncSessionMessages = useCallback(async (sessionId: string) => {
|
||||
// REGRESSION GUARD: Finish may arrive before the backend persists assistant turns.
|
||||
// Never blind-replace with server snapshot — merge + retry (see chat-finish-sync.mjs).
|
||||
@@ -1306,7 +1365,13 @@ export function useTKMindChat(
|
||||
const submit = useCallback(
|
||||
async (
|
||||
text: string,
|
||||
options?: { mindspaceContext?: MindSpaceChatContext; messageId?: string; forceDeepReasoning?: boolean; selectedChatSkill?: string },
|
||||
options?: {
|
||||
mindspaceContext?: MindSpaceChatContext;
|
||||
messageId?: string;
|
||||
forceDeepReasoning?: boolean;
|
||||
selectedChatSkill?: string;
|
||||
fileAttachments?: ChatFileAttachment[];
|
||||
},
|
||||
imageUrls?: string[],
|
||||
previewImageUrls?: string[],
|
||||
) => {
|
||||
@@ -1314,7 +1379,10 @@ export function useTKMindChat(
|
||||
const normalizedPreviewImageUrls = (previewImageUrls ?? []).filter(
|
||||
(value) => typeof value === 'string' && value.trim(),
|
||||
);
|
||||
if (!text.trim() && normalizedImageUrls.length === 0) return;
|
||||
const normalizedFileAttachments = (options?.fileAttachments ?? []).filter(
|
||||
(item) => item?.downloadUrl?.trim() && item?.filename?.trim(),
|
||||
);
|
||||
if (!text.trim() && normalizedImageUrls.length === 0 && normalizedFileAttachments.length === 0) return;
|
||||
// Use the ref here (not the React state) so that rapid back-to-back calls in the
|
||||
// same render cycle are blocked even before the state update has been re-rendered.
|
||||
if (
|
||||
@@ -1338,6 +1406,7 @@ export function useTKMindChat(
|
||||
displayText: trimmed,
|
||||
imageUrls: normalizedImageUrls,
|
||||
previewImageUrls: normalizedPreviewImageUrls,
|
||||
fileAttachments: normalizedFileAttachments,
|
||||
});
|
||||
userMessage.metadata = {
|
||||
...userMessage.metadata,
|
||||
@@ -1437,7 +1506,7 @@ export function useTKMindChat(
|
||||
if (!activeSessionId) {
|
||||
throw new Error('后台任务已提交,但未返回会话');
|
||||
}
|
||||
if (normalizedImageUrls.length > 0) {
|
||||
if (normalizedImageUrls.length > 0 || normalizedFileAttachments.length > 0) {
|
||||
void claimMindSpaceConversationUploads(activeSessionId, userMessage.id).catch(() => {});
|
||||
}
|
||||
if (!session?.id || session.id !== activeSessionId) {
|
||||
@@ -1778,6 +1847,7 @@ export function useTKMindChat(
|
||||
dismissNotice,
|
||||
onSidebarOpen,
|
||||
uploadChatImage,
|
||||
uploadChatAttachment,
|
||||
workingDir: session?.working_dir ?? user?.workspaceRoot ?? '',
|
||||
balanceCents: user?.balanceCents,
|
||||
totalCreditCents: user?.totalCreditCents,
|
||||
|
||||
Reference in New Issue
Block a user