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
+45 -1
View File
@@ -30,6 +30,7 @@ const ALLOWED_EXTENSIONS = new Map([
['.html', 'text/html'],
['.htm', 'text/html'],
]);
const MAX_IMAGE_UPLOAD_BYTES = 1536 * 1024;
function asNumber(value) {
return Number(value ?? 0);
@@ -141,6 +142,9 @@ export function validateUploadRequest({ filename, sizeBytes, maxFileBytes = DEFA
if (!mimeType) {
throw Object.assign(new Error('暂不支持该文件类型'), { code: 'unsupported_file_type' });
}
if (mimeType.startsWith('image/') && normalizedSize > MAX_IMAGE_UPLOAD_BYTES) {
throw Object.assign(new Error('图片文件超过单文件大小限制'), { code: 'file_too_large' });
}
return { filename: normalizedFilename, sizeBytes: normalizedSize, expectedMimeType: mimeType };
}
@@ -176,6 +180,37 @@ export function createAssetService(pool, options = {}) {
return resolved;
};
const resolveStoragePathCandidates = (storageKey) => {
const normalized = String(storageKey ?? '').replace(/\\/g, '/');
const withoutMd = normalized.endsWith('.md') ? normalized.slice(0, -3) : normalized;
const match = withoutMd.match(/^users\/([^/]+)\/(assets|pages)\/([^/]+)\/(v\d+)$/);
if (!match) return [normalized];
const [, userId, scope, entityId, versionTag] = match;
return [
normalized,
`users/${userId}/${scope}/${entityId}/versions/${versionTag}.md`,
`users/${userId}/${scope}/${entityId}/versions/${versionTag}`,
].filter((candidate, index, list) => list.indexOf(candidate) === index);
};
const resolveReadableStoragePath = async (storageKey) => {
let lastError = null;
for (const candidate of resolveStoragePathCandidates(storageKey)) {
const absolutePath = absoluteStoragePath(candidate);
try {
await fs.stat(absolutePath);
return absolutePath;
} catch (error) {
if (error?.code === 'ENOENT') {
lastError = error;
continue;
}
throw error;
}
}
throw lastError ?? Object.assign(new Error('存储文件不存在'), { code: 'storage_not_found' });
};
const createUpload = async (userId, input) => {
const validated = validateUploadRequest({ ...input, maxFileBytes });
const conn = await pool.getConnection();
@@ -293,6 +328,9 @@ export function createAssetService(pool, options = {}) {
if (!detectedMimeType) {
throw Object.assign(new Error('无法确认文件类型'), { code: 'unsupported_file_type' });
}
if (detectedMimeType.startsWith('image/') && buffer.length > MAX_IMAGE_UPLOAD_BYTES) {
throw Object.assign(new Error('图片文件超过单文件大小限制'), { code: 'file_too_large' });
}
const target = absoluteStoragePath(upload.temporary_storage_key);
await fs.mkdir(path.dirname(target), { recursive: true });
@@ -644,7 +682,10 @@ export function createAssetService(pool, options = {}) {
const asset = rows[0];
if (!asset) throw Object.assign(new Error('资产不存在'), { code: 'asset_not_found' });
assertAssetDownloadable(asset);
return { asset: assetResponse(asset), path: absoluteStoragePath(asset.storage_key) };
return {
asset: assetResponse(asset),
path: await resolveReadableStoragePath(asset.storage_key),
};
};
const createChatAsset = async (
@@ -662,6 +703,9 @@ export function createAssetService(pool, options = {}) {
if (!detectedMimeType) {
throw Object.assign(new Error('无法确认文件类型'), { code: 'unsupported_file_type' });
}
if (detectedMimeType.startsWith('image/') && buffer.length > MAX_IMAGE_UPLOAD_BYTES) {
throw Object.assign(new Error('图片文件超过单文件大小限制'), { code: 'file_too_large' });
}
const conn = await pool.getConnection();
let finalPath;
try {