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:
+135
-6
@@ -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),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user