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:
john
2026-06-19 23:06:43 +08:00
parent b0f5d6a51c
commit 229805a070
241 changed files with 13190 additions and 902 deletions
+135 -6
View File
@@ -43,8 +43,11 @@ import type {
UsageRecord,
BalanceUpdate,
} from '../types';
import { CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES } from '../utils/imageUpload';
import { normalizeConversationMessages, normalizeUserMessageForApi } from '../utils/message';
const API = '/api';
const DEFAULT_API_TIMEOUT_MS = 20_000;
export class ApiError extends Error {
readonly status: number;
@@ -65,7 +68,11 @@ export class ApiError extends Error {
}
}
async function parseErrorResponse(res: Response) {
async function parseErrorResponse(res: Response): Promise<{
message: string;
code?: string;
details?: InsufficientBalanceDetails | Record<string, unknown>;
}> {
const text = await res.text().catch(() => '');
try {
const body = JSON.parse(text) as Record<string, unknown>;
@@ -104,7 +111,13 @@ async function parseErrorResponse(res: Response) {
},
};
}
return { message, code, details };
return {
message,
code,
details: details && typeof details === 'object'
? (details as InsufficientBalanceDetails | Record<string, unknown>)
: undefined,
};
} catch {
return { message: text || res.statusText };
}
@@ -128,10 +141,32 @@ function notifyUnauthorized() {
unauthorizedHandler();
}
async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const controller = new AbortController();
const upstreamSignal = init?.signal;
const timeout = window.setTimeout(() => controller.abort(), DEFAULT_API_TIMEOUT_MS);
const abortFromUpstream = () => controller.abort();
if (upstreamSignal) {
if (upstreamSignal.aborted) controller.abort();
else upstreamSignal.addEventListener('abort', abortFromUpstream, { once: true });
}
try {
return await fetch(input, {
...init,
signal: controller.signal,
});
} finally {
window.clearTimeout(timeout);
upstreamSignal?.removeEventListener('abort', abortFromUpstream);
}
}
async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
let res: Response;
try {
res = await fetch(path, {
res = await fetchWithTimeout(path, {
...init,
headers: {
'Content-Type': 'application/json',
@@ -164,6 +199,9 @@ async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
function formatNetworkError(err: unknown) {
const message = err instanceof Error ? err.message : '网络请求失败';
if (err instanceof DOMException && err.name === 'AbortError') {
return '连接 goose 服务超时,请确认 goosed/H5 后端正在运行后重试';
}
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
return '无法连接后端服务,请先运行: pnpm dev 或 node server.mjs';
}
@@ -173,7 +211,7 @@ function formatNetworkError(err: unknown) {
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
let res: Response;
try {
res = await fetch(`${API}${path}`, {
res = await fetchWithTimeout(`${API}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
@@ -255,6 +293,25 @@ export type WechatBindingStatus = {
avatarUrl?: string | null;
};
export type WechatAgentRouteStatus = {
enabled: boolean;
bound: boolean;
appId: string | null;
openid: string | null;
agentSessionId: string | null;
routeStatus: string | null;
updatedAt: number | null;
};
export type WechatJsSdkSignature = {
appId: string;
timestamp: number;
nonceStr: string;
signature: string;
url: string;
jsApiList: string[];
};
export async function getWechatAuthConfig(): Promise<WechatAuthConfig> {
try {
const response = await fetch('/auth/wechat/config');
@@ -265,6 +322,22 @@ export async function getWechatAuthConfig(): Promise<WechatAuthConfig> {
}
}
export async function getWechatJsSdkSignature(url: string): Promise<WechatJsSdkSignature> {
let response: Response;
try {
response = await fetch(`/auth/wechat/js-sdk-signature?url=${encodeURIComponent(url)}`);
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as
| (WechatJsSdkSignature & { message?: string })
| null;
if (!response.ok || !body?.signature) {
throw new ApiError(response.status, body?.message ?? '微信语音初始化失败');
}
return body;
}
export async function checkAuth(): Promise<AuthStatus> {
try {
const response = await fetch('/auth/status');
@@ -373,6 +446,13 @@ export async function uploadMindSpaceAsset(
categoryId: string,
file: File,
): Promise<MindSpaceAsset> {
if (file.type.startsWith('image/') && file.size > CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES) {
throw new ApiError(
413,
`图片文件过大,当前 ${(file.size / 1024 / 1024).toFixed(2)}MB,请先压缩到 ${CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES / 1024 / 1024}MB 以下再上传。`,
);
}
const created = await apiFetch<{ data: MindSpaceUpload }>('/mindspace/v1/uploads', {
method: 'POST',
body: JSON.stringify({
@@ -1318,6 +1398,53 @@ export async function getWechatBindingStatus(): Promise<WechatBindingStatus> {
}
}
export async function getWechatAgentRouteStatus(): Promise<WechatAgentRouteStatus> {
try {
const response = await fetch('/auth/wechat/agent-route');
if (!response.ok) {
return {
enabled: false,
bound: false,
appId: null,
openid: null,
agentSessionId: null,
routeStatus: null,
updatedAt: null,
};
}
return (await response.json()) as WechatAgentRouteStatus;
} catch {
return {
enabled: false,
bound: false,
appId: null,
openid: null,
agentSessionId: null,
routeStatus: null,
updatedAt: null,
};
}
}
export async function resetWechatAgentRoute(): Promise<WechatAgentRouteStatus> {
let response: Response;
try {
response = await fetch('/auth/wechat/agent-route/reset', {
method: 'POST',
});
} catch (err) {
throw new ApiError(0, formatNetworkError(err));
}
const body = (await response.json().catch(() => null)) as {
message?: string;
route?: WechatAgentRouteStatus;
} | null;
if (!response.ok || !body?.route) {
throw new ApiError(response.status, body?.message ?? '重建公众号 Agent 路由失败');
}
return body.route;
}
export type WechatScanSession =
| {
mode: 'open';
@@ -1678,7 +1805,9 @@ export async function loadSessionDetail(sessionId: string): Promise<{
messages: Message[];
}> {
const detail = await getSession(sessionId);
const messages = (detail.conversation ?? []).filter((m) => m.metadata?.userVisible);
const messages = normalizeConversationMessages(
(detail.conversation ?? []).filter((m) => m.metadata?.userVisible),
);
return { session: detail, messages };
}
@@ -1691,7 +1820,7 @@ export async function sendReply(
method: 'POST',
body: JSON.stringify({
request_id: requestId,
user_message: userMessage,
user_message: normalizeUserMessageForApi(userMessage),
}),
});
}