feat(mindspace): add WeChat MP config and one-click draft push on publish.

Let users bind their own official account credentials in M 配置 and push MindSpace pages to the WeChat draft box during or after publish, reusing the existing draft conversion pipeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-10 16:43:25 +08:00
parent 3bc2442769
commit 78b7d546c2
19 changed files with 1635 additions and 16 deletions
+126
View File
@@ -0,0 +1,126 @@
function assertRouter(api) {
if (
!api ||
typeof api.get !== 'function' ||
typeof api.post !== 'function' ||
typeof api.put !== 'function' ||
typeof api.delete !== 'function'
) {
throw new Error(
'attachPortalMindSpaceWechatRoutes requires an Express-compatible router',
);
}
}
export function attachPortalMindSpaceWechatRoutes(
api,
{
getWechatMpConfig = () => null,
getWechatPageDraft = () => null,
sendData,
handleMindSpaceError,
} = {},
) {
assertRouter(api);
if (typeof sendData !== 'function' || typeof handleMindSpaceError !== 'function') {
throw new Error(
'attachPortalMindSpaceWechatRoutes requires response dependencies',
);
}
api.get('/mindspace/v1/wechat-mp/config', async (req, res) => {
const service = getWechatMpConfig();
if (!service) {
return res.status(503).json({ message: '公众号配置未启用' });
}
try {
return sendData(res, req, await service.getConfig(req.currentUser.id));
} catch (error) {
return handleMindSpaceError(res, req, error);
}
});
api.put('/mindspace/v1/wechat-mp/config', async (req, res) => {
const service = getWechatMpConfig();
if (!service) {
return res.status(503).json({ message: '公众号配置未启用' });
}
try {
return sendData(
res,
req,
await service.upsertConfig(req.currentUser.id, {
appId: req.body?.app_id ?? req.body?.appId,
appSecret: req.body?.app_secret ?? req.body?.appSecret,
accountType: req.body?.account_type ?? req.body?.accountType,
label: req.body?.label,
author: req.body?.author,
verify: req.body?.verify !== false,
}),
);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
});
api.post('/mindspace/v1/wechat-mp/config/verify', async (req, res) => {
const service = getWechatMpConfig();
if (!service) {
return res.status(503).json({ message: '公众号配置未启用' });
}
try {
return sendData(res, req, await service.verifyConfig(req.currentUser.id));
} catch (error) {
return handleMindSpaceError(res, req, error);
}
});
api.delete('/mindspace/v1/wechat-mp/config', async (req, res) => {
const service = getWechatMpConfig();
if (!service) {
return res.status(503).json({ message: '公众号配置未启用' });
}
try {
return sendData(res, req, await service.deleteConfig(req.currentUser.id));
} catch (error) {
return handleMindSpaceError(res, req, error);
}
});
api.post('/mindspace/v1/pages/:pageId/wechat-draft', async (req, res) => {
const service = getWechatPageDraft();
if (!service) {
return res.status(503).json({ message: '公众号草稿推送未启用' });
}
try {
return sendData(
res,
req,
await service.pushPageDraft(req.currentUser.id, req.params.pageId, {
triggeredBy: req.currentUser.id,
}),
);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
});
api.get('/mindspace/v1/pages/:pageId/wechat-draft/runs', async (req, res) => {
const service = getWechatPageDraft();
if (!service) {
return res.status(503).json({ message: '公众号草稿推送未启用' });
}
try {
return sendData(
res,
req,
await service.listRuns(req.currentUser.id, {
pageId: req.params.pageId,
limit: req.query?.limit,
}),
);
} catch (error) {
return handleMindSpaceError(res, req, error);
}
});
}
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalMindSpaceWechatRoutes } from './portal-mindspace-wechat-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
return {
routes,
get(path, handler) {
routes.set(`GET ${path}`, handler);
},
post(path, handler) {
routes.set(`POST ${path}`, handler);
},
put(path, handler) {
routes.set(`PUT ${path}`, handler);
},
delete(path, handler) {
routes.set(`DELETE ${path}`, handler);
},
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
};
}
function createRequest(overrides = {}) {
return {
body: {},
params: { pageId: 'page-1' },
query: {},
currentUser: { id: 'user-1' },
requestId: 'request-1',
...overrides,
};
}
test('wechat config routes delegate to services', async () => {
const calls = [];
const api = createRouterRecorder();
attachPortalMindSpaceWechatRoutes(api, {
getWechatMpConfig: () => ({
async getConfig(userId) {
calls.push(['getConfig', userId]);
return { configured: true, appId: 'wx-test' };
},
async upsertConfig(userId, payload) {
calls.push(['upsertConfig', userId, payload]);
return { configured: true, appId: payload.appId };
},
async verifyConfig(userId) {
calls.push(['verifyConfig', userId]);
return { configured: true, verifiedAt: 1 };
},
async deleteConfig(userId) {
calls.push(['deleteConfig', userId]);
return { ok: true };
},
}),
getWechatPageDraft: () => ({
async pushPageDraft(userId, pageId) {
calls.push(['pushPageDraft', userId, pageId]);
return { ok: true, draftMediaId: 'draft-1' };
},
async listRuns(userId, options) {
calls.push(['listRuns', userId, options]);
return [];
},
}),
sendData(res, req, data) {
return res.status(200).json({ data });
},
handleMindSpaceError(res, req, error) {
return res.status(400).json({ message: error.message });
},
});
const getRes = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/wechat-mp/config')(createRequest(), getRes);
assert.deepEqual(calls[0], ['getConfig', 'user-1']);
const putRes = createResponseRecorder();
await api.routes.get('PUT /mindspace/v1/wechat-mp/config')(
createRequest({ body: { app_id: 'wx-new', app_secret: 'secret' } }),
putRes,
);
assert.equal(calls.at(-1)[0], 'upsertConfig');
const pushRes = createResponseRecorder();
await api.routes.get('POST /mindspace/v1/pages/:pageId/wechat-draft')(createRequest(), pushRes);
assert.deepEqual(calls.at(-1), ['pushPageDraft', 'user-1', 'page-1']);
});