Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,8 +5,10 @@ import {
|
||||
closeMindSpacePageEditSession,
|
||||
confirmTool,
|
||||
forkMindSpacePageEditSession,
|
||||
getMindSpace,
|
||||
loadSessionDetail,
|
||||
resumeSession,
|
||||
uploadMindSpaceAsset,
|
||||
sendReply,
|
||||
subscribeSessionEvents,
|
||||
} from '../api/client';
|
||||
@@ -14,7 +16,15 @@ import type { ChatState, Message, MindSpaceChatContext, PortalUser, Session, Ses
|
||||
import { buildContextPrefix } from '../utils/mindspaceChatContext';
|
||||
import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
import {
|
||||
createUserMessage,
|
||||
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 {
|
||||
buildUserMessage,
|
||||
normalizeConversationMessages,
|
||||
getDisplayText,
|
||||
getToolConfirmation,
|
||||
pushMessage,
|
||||
@@ -67,6 +77,7 @@ export function usePageEditSubChat({
|
||||
const messagesRef = useRef<Message[]>([]);
|
||||
const sessionRef = useRef<Session | null>(null);
|
||||
const closingRef = useRef(false);
|
||||
const chatImageCategoryIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages;
|
||||
@@ -78,6 +89,36 @@ export function usePageEditSubChat({
|
||||
|
||||
const dismissNotice = useCallback(() => setNotice(null), []);
|
||||
|
||||
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[0];
|
||||
if (!category) {
|
||||
throw new Error('当前空间暂无可用的图片上传分类');
|
||||
}
|
||||
chatImageCategoryIdRef.current = category.id;
|
||||
return category.id;
|
||||
}, []);
|
||||
|
||||
const uploadChatImage = useCallback(async (file: File): 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 });
|
||||
}, [resolveChatImageUploadCategoryId]);
|
||||
|
||||
const processEvent = useCallback((event: SessionEvent, requestId: string) => {
|
||||
const raw = event as SessionEvent & { chat_request_id?: string; request_id?: string };
|
||||
const eventRequestId = raw.chat_request_id ?? raw.request_id;
|
||||
@@ -101,7 +142,9 @@ export function usePageEditSubChat({
|
||||
return;
|
||||
}
|
||||
case 'UpdateConversation':
|
||||
messagesRef.current = event.conversation.filter((message) => message.metadata?.userVisible);
|
||||
messagesRef.current = normalizeConversationMessages(
|
||||
event.conversation.filter((message) => message.metadata?.userVisible),
|
||||
);
|
||||
setMessages(messagesRef.current);
|
||||
return;
|
||||
case 'Error':
|
||||
@@ -233,18 +276,20 @@ export function usePageEditSubChat({
|
||||
);
|
||||
|
||||
const submit = useCallback(
|
||||
async (text: string, context: MindSpaceChatContext) => {
|
||||
async (text: string, context: MindSpaceChatContext, imageUrls?: string[]) => {
|
||||
const currentSession = sessionRef.current;
|
||||
if (!currentSession || !text.trim()) return;
|
||||
const normalizedImageUrls = (imageUrls ?? []).filter((value) => typeof value === 'string' && value.trim());
|
||||
if (!currentSession || (!text.trim() && normalizedImageUrls.length === 0)) return;
|
||||
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
|
||||
|
||||
const trimmed = text.trim();
|
||||
const mindspacePrefix = buildContextPrefix(context);
|
||||
const userPrefix = buildUserAddressPrefix(user);
|
||||
const agentPrefix = `${userPrefix}${mindspacePrefix}`;
|
||||
const userMessage = createUserMessage(trimmed, {
|
||||
const userMessage = buildUserMessage(trimmed, {
|
||||
agentText: `${agentPrefix}${trimmed}`,
|
||||
displayText: trimmed,
|
||||
imageUrls: normalizedImageUrls,
|
||||
});
|
||||
const requestId = crypto.randomUUID();
|
||||
activeRequestId.current = requestId;
|
||||
@@ -332,6 +377,7 @@ export function usePageEditSubChat({
|
||||
submit,
|
||||
stop,
|
||||
approveTool,
|
||||
uploadChatImage,
|
||||
dismissNotice,
|
||||
retryConnect,
|
||||
openRecharge: () => setNotice('余额不足,请充值后继续使用'),
|
||||
|
||||
+83
-32
@@ -6,12 +6,14 @@ import {
|
||||
cancelRequest,
|
||||
confirmTool,
|
||||
deleteChatSession,
|
||||
getMindSpace,
|
||||
getMe,
|
||||
getSession,
|
||||
listSessions,
|
||||
loadSessionDetail,
|
||||
readConfig,
|
||||
rememberProjectContext,
|
||||
uploadMindSpaceAsset,
|
||||
resumeSession,
|
||||
sendReply,
|
||||
startSession,
|
||||
@@ -32,7 +34,15 @@ import type {
|
||||
import { buildContextPrefix } from '../utils/mindspaceChatContext';
|
||||
import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
import {
|
||||
createUserMessage,
|
||||
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 {
|
||||
buildUserMessage,
|
||||
normalizeConversationMessages,
|
||||
getDisplayText,
|
||||
getToolConfirmation,
|
||||
getVisibleText,
|
||||
@@ -40,7 +50,7 @@ import {
|
||||
isRelayServerErrorMessage,
|
||||
pushMessage,
|
||||
} from '../utils/message';
|
||||
import { prependUnique, sortAndTrim, touchSession } from '../utils/sessions';
|
||||
import { prependUnique, shouldShowNewChatTitle, sortAndTrim, touchSession } from '../utils/sessions';
|
||||
|
||||
const LEGACY_SESSION_KEY = 'tkmind-h5-session-id';
|
||||
|
||||
@@ -104,6 +114,7 @@ export function useTKMindChat(
|
||||
const fallbackRetriedRef = useRef(new Set<string>());
|
||||
const userRef = useRef(user);
|
||||
const onUserUpdateRef = useRef(onUserUpdate);
|
||||
const chatImageCategoryIdRef = useRef<string | null>(null);
|
||||
|
||||
const dismissNotice = useCallback(() => setNotice(null), []);
|
||||
|
||||
@@ -266,6 +277,40 @@ export function useTKMindChat(
|
||||
}
|
||||
}, []);
|
||||
|
||||
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[0];
|
||||
if (!category) {
|
||||
throw new Error('当前空间暂无可用的图片上传分类');
|
||||
}
|
||||
|
||||
chatImageCategoryIdRef.current = category.id;
|
||||
return category.id;
|
||||
}, []);
|
||||
|
||||
const uploadChatImage = useCallback(async (file: File): 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,
|
||||
});
|
||||
}, [resolveChatImageUploadCategoryId]);
|
||||
|
||||
const processEvent = useCallback(
|
||||
(event: SessionEvent, requestId: string, sessionId: string) => {
|
||||
const raw = event as SessionEvent & { chat_request_id?: string; request_id?: string };
|
||||
@@ -340,7 +385,9 @@ export function useTKMindChat(
|
||||
return;
|
||||
}
|
||||
case 'UpdateConversation':
|
||||
messagesRef.current = event.conversation.filter((m) => m.metadata?.userVisible);
|
||||
messagesRef.current = normalizeConversationMessages(
|
||||
event.conversation.filter((m) => m.metadata?.userVisible),
|
||||
);
|
||||
setMessages(messagesRef.current);
|
||||
return;
|
||||
case 'Error':
|
||||
@@ -507,17 +554,24 @@ export function useTKMindChat(
|
||||
setChatState('loading');
|
||||
setError(null);
|
||||
|
||||
let sessionId = readStoredSessionId(userRef.current?.id);
|
||||
const previousSessionId = readStoredSessionId(userRef.current?.id);
|
||||
let sessionId: string | null = null;
|
||||
let staleSession = false;
|
||||
let freshSession: Session | null = null;
|
||||
|
||||
if (sessionId) {
|
||||
if (previousSessionId) {
|
||||
try {
|
||||
await getSession(sessionId);
|
||||
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);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
|
||||
clearStoredSessionId(userRef.current?.id);
|
||||
sessionId = null;
|
||||
staleSession = true;
|
||||
} else {
|
||||
throw err;
|
||||
@@ -525,28 +579,20 @@ export function useTKMindChat(
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
freshSession = await startSession();
|
||||
sessionId = freshSession.id;
|
||||
writeStoredSessionId(userRef.current?.id, sessionId);
|
||||
if (staleSession) {
|
||||
setNotice('上次会话已失效,已为你新建对话');
|
||||
}
|
||||
const freshSession = await startSession();
|
||||
sessionId = freshSession.id;
|
||||
writeStoredSessionId(userRef.current?.id, sessionId);
|
||||
if (staleSession) {
|
||||
setNotice('上次会话已失效,已为你新建对话');
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
if (!freshSession) {
|
||||
await ensureProvider(sessionId);
|
||||
}
|
||||
if (cancelled) return;
|
||||
await connectSessionRef.current(sessionId, {
|
||||
skipResume: Boolean(freshSession),
|
||||
seedSession: freshSession ?? undefined,
|
||||
skipResume: true,
|
||||
seedSession: freshSession,
|
||||
});
|
||||
if (cancelled) return;
|
||||
if (freshSession) {
|
||||
void loadProjectMemory(sessionId, false);
|
||||
}
|
||||
void loadProjectMemory(sessionId, false);
|
||||
void refreshSessions();
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
@@ -605,8 +651,13 @@ export function useTKMindChat(
|
||||
);
|
||||
|
||||
const submit = useCallback(
|
||||
async (text: string, options?: { mindspaceContext?: MindSpaceChatContext }) => {
|
||||
if (!session || !text.trim()) return;
|
||||
async (
|
||||
text: string,
|
||||
options?: { mindspaceContext?: MindSpaceChatContext },
|
||||
imageUrls?: string[],
|
||||
) => {
|
||||
const normalizedImageUrls = (imageUrls ?? []).filter((value) => typeof value === 'string' && value.trim());
|
||||
if (!session || (!text.trim() && normalizedImageUrls.length === 0)) return;
|
||||
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
|
||||
|
||||
const trimmed = text.trim();
|
||||
@@ -615,12 +666,11 @@ export function useTKMindChat(
|
||||
: '';
|
||||
const userPrefix = buildUserAddressPrefix(userRef.current);
|
||||
const agentPrefix = `${userPrefix}${mindspacePrefix}`;
|
||||
const userMessage = agentPrefix
|
||||
? createUserMessage(trimmed, {
|
||||
agentText: `${agentPrefix}${trimmed}`,
|
||||
displayText: trimmed,
|
||||
})
|
||||
: createUserMessage(trimmed);
|
||||
const userMessage = buildUserMessage(trimmed, {
|
||||
agentText: `${agentPrefix}${trimmed}`,
|
||||
displayText: trimmed,
|
||||
imageUrls: normalizedImageUrls,
|
||||
});
|
||||
const requestId = crypto.randomUUID();
|
||||
activeRequestId.current = requestId;
|
||||
messagesRef.current = [...messagesRef.current, userMessage];
|
||||
@@ -768,6 +818,7 @@ export function useTKMindChat(
|
||||
retryConnect,
|
||||
dismissNotice,
|
||||
onSidebarOpen,
|
||||
uploadChatImage,
|
||||
workingDir: session?.working_dir ?? user?.workspaceRoot ?? '',
|
||||
balanceCents: user?.balanceCents,
|
||||
totalCreditCents: user?.totalCreditCents,
|
||||
|
||||
@@ -30,6 +30,7 @@ export function useVoiceSession({
|
||||
const recognitionRef = useRef<ReturnType<typeof createLiveSpeechRecognition> | null>(null);
|
||||
const startedAtRef = useRef(0);
|
||||
const sessionRef = useRef(0);
|
||||
const stopTimerRef = useRef<number | null>(null);
|
||||
|
||||
const syncDisplayText = useCallback(() => {
|
||||
if (userEditedRef.current) return;
|
||||
@@ -46,6 +47,10 @@ export function useVoiceSession({
|
||||
}, []);
|
||||
|
||||
const cleanup = useCallback(async () => {
|
||||
if (stopTimerRef.current != null) {
|
||||
window.clearTimeout(stopTimerRef.current);
|
||||
stopTimerRef.current = null;
|
||||
}
|
||||
recognitionRef.current?.abort();
|
||||
recognitionRef.current = null;
|
||||
const capture = captureRef.current;
|
||||
@@ -57,15 +62,27 @@ export function useVoiceSession({
|
||||
}, []);
|
||||
|
||||
const stopListening = useCallback(() => {
|
||||
recognitionRef.current?.stop();
|
||||
recognitionRef.current = null;
|
||||
const recognition = recognitionRef.current;
|
||||
if (recognition) {
|
||||
recognition.stop();
|
||||
recognitionRef.current = null;
|
||||
if (stopTimerRef.current != null) {
|
||||
window.clearTimeout(stopTimerRef.current);
|
||||
}
|
||||
stopTimerRef.current = window.setTimeout(() => {
|
||||
stopTimerRef.current = null;
|
||||
setPhase((current) => (current === 'transcribing' ? current : 'ready'));
|
||||
}, 220);
|
||||
}
|
||||
const capture = captureRef.current;
|
||||
captureRef.current = null;
|
||||
if (capture) {
|
||||
void capture.cancel();
|
||||
}
|
||||
setAnalyser(null);
|
||||
setPhase((current) => (current === 'transcribing' ? current : 'ready'));
|
||||
if (!recognition) {
|
||||
setPhase((current) => (current === 'transcribing' ? current : 'ready'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startServerAsrCapture = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user