Files
memind/scripts/wechat-mp-menu.mjs
T
2026-07-02 06:55:26 +08:00

139 lines
3.8 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 微信服务号自定义菜单(menu/create)。
*
* 变更规范:
* - 只在本仓库修改 MENU 常量,禁止 SSH 到 105 直接改线上脚本。
* - 流程:本地改码 → Git commit → 正式发布 → 再执行本脚本同步到微信 API。
* - 详见 docs/105-server-operations.md
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadH5Environment } from './load-env.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
loadH5Environment(__dirname);
const DEFAULT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/stable_token';
const DEFAULT_MENU_CREATE_URL = 'https://api.weixin.qq.com/cgi-bin/menu/create';
const DEFAULT_MENU_GET_URL = 'https://api.weixin.qq.com/cgi-bin/menu/get';
const MENU = {
button: [
{
type: 'view',
name: 'TKMind',
url: 'https://mm.tkmind.cn',
},
{
type: 'view',
name: 'M空间',
url: 'https://mm.tkmind.cn/space',
},
{
type: 'view',
name: 'M发现',
url: 'https://plaza.tkmind.cn',
},
],
};
function endpointFromTokenUrl(pathname, fallback) {
const tokenUrl = process.env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_TOKEN_URL;
try {
const url = new URL(tokenUrl);
url.pathname = pathname;
url.search = '';
return url.toString();
} catch {
return fallback;
}
}
async function readJson(response) {
const text = await response.text();
if (!response.ok) {
throw new Error(text || `HTTP ${response.status}`);
}
return text ? JSON.parse(text) : null;
}
async function getAccessToken() {
const appid =
process.env.H5_WECHAT_MP_APP_ID?.trim() ?? process.env.H5_WECHAT_APP_ID?.trim() ?? '';
const secret =
process.env.H5_WECHAT_MP_APP_SECRET?.trim() ?? process.env.H5_WECHAT_APP_SECRET?.trim() ?? '';
if (!appid || !secret) {
throw new Error(
'缺少 H5_WECHAT_MP_APP_ID / H5_WECHAT_MP_APP_SECRET(或 H5_WECHAT_APP_ID / H5_WECHAT_APP_SECRET',
);
}
const tokenUrl = process.env.H5_WECHAT_MP_TOKEN_URL?.trim() || DEFAULT_TOKEN_URL;
const payload = await readJson(
await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credential',
appid,
secret,
force_refresh: false,
}),
}),
);
if (!payload?.access_token) {
throw new Error(payload?.errmsg || `获取 access_token 失败 (${payload?.errcode ?? 'unknown'})`);
}
return payload.access_token;
}
async function createMenu(accessToken) {
const baseUrl =
process.env.H5_WECHAT_MP_MENU_CREATE_URL?.trim() ||
endpointFromTokenUrl('/cgi-bin/menu/create', DEFAULT_MENU_CREATE_URL);
const url = `${baseUrl}?access_token=${encodeURIComponent(accessToken)}`;
const payload = await readJson(
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(MENU),
}),
);
if (Number(payload?.errcode ?? 0) !== 0) {
throw new Error(payload?.errmsg || `创建菜单失败 (${payload?.errcode})`);
}
return payload;
}
async function getMenu(accessToken) {
const baseUrl =
process.env.H5_WECHAT_MP_MENU_GET_URL?.trim() ||
endpointFromTokenUrl('/cgi-bin/menu/get', DEFAULT_MENU_GET_URL);
const url = `${baseUrl}?access_token=${encodeURIComponent(accessToken)}`;
return readJson(await fetch(url, { method: 'GET' }));
}
const dryRun = process.argv.includes('--dry-run');
if (dryRun) {
console.log(JSON.stringify(MENU, null, 2));
process.exit(0);
}
const accessToken = await getAccessToken();
const created = await createMenu(accessToken);
const current = await getMenu(accessToken).catch((err) => ({ warning: err.message }));
console.log(
JSON.stringify(
{
ok: true,
created,
menu: MENU,
current,
},
null,
2,
),
);