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:
+75
-3
@@ -1,6 +1,10 @@
|
||||
import express from 'express';
|
||||
import crypto from 'node:crypto';
|
||||
import { createPlazaPostService, formatPostRow, mapPlazaError } from './plaza-posts.mjs';
|
||||
import { createPlazaEventService } from './plaza-events.mjs';
|
||||
import { createPlazaRecommendService } from './plaza-recommend.mjs';
|
||||
import { createPlazaInteractionService } from './plaza-interactions.mjs';
|
||||
import { parseCookies } from './auth.mjs';
|
||||
import {
|
||||
ensureAlgorithmConfig,
|
||||
loadAlgorithmConfig,
|
||||
@@ -23,6 +27,14 @@ export async function bootstrapPlaza(pool) {
|
||||
formatPostRow,
|
||||
plazaRedis: redis,
|
||||
});
|
||||
const events = createPlazaEventService(pool);
|
||||
const recommend = createPlazaRecommendService(pool, {
|
||||
eventService: events,
|
||||
formatPostRow,
|
||||
loadViewerReactions: (viewerId, postIds) =>
|
||||
interactions.loadViewerReactions(viewerId, postIds),
|
||||
algorithmConfig,
|
||||
});
|
||||
|
||||
let ops = null;
|
||||
const posts = createPlazaPostService(pool, {
|
||||
@@ -30,6 +42,7 @@ export async function bootstrapPlaza(pool) {
|
||||
interactions.loadViewerReactions(viewerId, postIds),
|
||||
plazaRedis: redis,
|
||||
algorithmConfig,
|
||||
recommendService: recommend,
|
||||
onPostPublished: (postId) => seo?.notifyPostPublished(postId),
|
||||
loadFeaturedPosts: async (viewerId) => {
|
||||
if (!ops) return { homepage_banner: [], trending: [], category_top: {} };
|
||||
@@ -47,7 +60,7 @@ export async function bootstrapPlaza(pool) {
|
||||
recalculateHotScores,
|
||||
writebackPublications,
|
||||
});
|
||||
return { posts, interactions, seo, ops, redis };
|
||||
return { posts, interactions, events, recommend, seo, ops, redis };
|
||||
}
|
||||
|
||||
export function createNoopPlaza() {
|
||||
@@ -55,7 +68,9 @@ export function createNoopPlaza() {
|
||||
}
|
||||
|
||||
export function isPlazaPublicRead(path, method) {
|
||||
if (method !== 'GET' || !path.startsWith('/plaza/v1/')) return false;
|
||||
if (!path.startsWith('/plaza/v1/')) return false;
|
||||
if (method === 'POST' && path === '/plaza/v1/events') return true;
|
||||
if (method !== 'GET') return false;
|
||||
return (
|
||||
path === '/plaza/v1/feed' ||
|
||||
path === '/plaza/v1/categories' ||
|
||||
@@ -75,6 +90,38 @@ export function plazaClientIp(req) {
|
||||
return req.ip;
|
||||
}
|
||||
|
||||
const PLAZA_SID_COOKIE = 'plaza_sid';
|
||||
|
||||
function resolvePlazaSessionId(req, res) {
|
||||
const cookies = parseCookies(req.get('cookie'));
|
||||
let sessionId = cookies[PLAZA_SID_COOKIE];
|
||||
if (!sessionId) {
|
||||
sessionId = crypto.randomUUID();
|
||||
res.append(
|
||||
'Set-Cookie',
|
||||
`${PLAZA_SID_COOKIE}=${sessionId}; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly`,
|
||||
);
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
function recordPlazaEventsAsync(eventsService, req, res, events) {
|
||||
if (!eventsService || !Array.isArray(events) || events.length === 0) return;
|
||||
const sessionId = resolvePlazaSessionId(req, res);
|
||||
void eventsService
|
||||
.recordEvents({
|
||||
userId: req.currentUser?.id ?? null,
|
||||
sessionId,
|
||||
events,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function reactionEventType(type) {
|
||||
if (type === 'like' || type === 'collect' || type === 'share') return type;
|
||||
return null;
|
||||
}
|
||||
|
||||
function plazaRouteError(res, req, error, { sendError }) {
|
||||
const status = mapPlazaError(error);
|
||||
const code = error?.code ?? 'internal_error';
|
||||
@@ -83,7 +130,7 @@ function plazaRouteError(res, req, error, { sendError }) {
|
||||
}
|
||||
|
||||
export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
||||
const { posts, interactions, seo, ops, redis } = plaza;
|
||||
const { posts, interactions, events, seo, ops, redis } = plaza;
|
||||
|
||||
function ensurePosts(res, req) {
|
||||
if (!posts) { sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用'); return false; }
|
||||
@@ -180,12 +227,14 @@ export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
||||
api.get('/plaza/v1/feed', async (req, res) => {
|
||||
if (!ensurePosts(res, req)) return;
|
||||
try {
|
||||
const sessionId = resolvePlazaSessionId(req, res);
|
||||
const feed = await posts.listFeed({
|
||||
sort: req.query.sort,
|
||||
categorySlug: req.query.category ?? null,
|
||||
cursor: req.query.cursor ?? null,
|
||||
limit: req.query.limit,
|
||||
viewerId: req.currentUser?.id ?? null,
|
||||
sessionId,
|
||||
});
|
||||
return sendData(res, req, feed);
|
||||
} catch (error) {
|
||||
@@ -193,11 +242,32 @@ export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/plaza/v1/events', async (req, res) => {
|
||||
if (!ensurePosts(res, req)) return;
|
||||
if (!events) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
|
||||
try {
|
||||
const sessionId =
|
||||
String(req.body?.session_id ?? '').trim() || resolvePlazaSessionId(req, res);
|
||||
const result = await events.recordEvents({
|
||||
userId: req.currentUser?.id ?? null,
|
||||
sessionId,
|
||||
events: req.body?.events ?? [],
|
||||
});
|
||||
return sendData(res, req, { ...result, session_id: sessionId }, 201);
|
||||
} catch (error) {
|
||||
return routeError(res, req, error);
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/plaza/v1/posts/:id/reactions', async (req, res) => {
|
||||
if (!ensureInteractions(res, req)) return;
|
||||
if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权,请重新登录');
|
||||
try {
|
||||
const result = await interactions.addReaction(req.currentUser.id, req.params.id, req.body?.type);
|
||||
const eventType = reactionEventType(result.type);
|
||||
if (eventType) {
|
||||
recordPlazaEventsAsync(events, req, res, [{ event_type: eventType, post_id: req.params.id }]);
|
||||
}
|
||||
return sendData(res, req, result);
|
||||
} catch (error) {
|
||||
return routeError(res, req, error);
|
||||
@@ -235,6 +305,7 @@ export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
||||
if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权,请重新登录');
|
||||
try {
|
||||
const comment = await interactions.createComment(req.currentUser.id, req.params.id, req.body ?? {});
|
||||
recordPlazaEventsAsync(events, req, res, [{ event_type: 'comment', post_id: req.params.id }]);
|
||||
return sendData(res, req, { comment }, 201);
|
||||
} catch (error) {
|
||||
return routeError(res, req, error);
|
||||
@@ -317,6 +388,7 @@ export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
||||
viewerId: req.currentUser?.id ?? null,
|
||||
});
|
||||
void redis.recordView(req.params.id, plazaClientIp(req)).catch(() => {});
|
||||
recordPlazaEventsAsync(events, req, res, [{ event_type: 'view', post_id: req.params.id }]);
|
||||
return sendData(res, req, { post });
|
||||
} catch (error) {
|
||||
return routeError(res, req, error);
|
||||
|
||||
Reference in New Issue
Block a user