#!/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://m.tkmind.cn', }, { type: 'view', name: 'M空间', url: 'https://m.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, ), );