Files

139 lines
3.8 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
2026-06-27 08:25:11 +08:00
/**
* 微信服务号自定义菜单(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',
2026-06-27 08:25:11 +08:00
name: 'TKMind',
2026-07-03 17:27:51 +08:00
url: 'https://m.tkmind.cn',
},
{
type: 'view',
name: 'M空间',
2026-07-03 17:27:51 +08:00
url: 'https://m.tkmind.cn/space',
},
{
type: 'view',
2026-06-27 08:25:11 +08:00
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,
),
);