W1-W3 实现企业微信个人应用消息通知

这个提交包含在:
2026-05-11 09:17:00 +08:00
父节点 805e050ba8
当前提交 b1ae41fa99
共修改 12 个文件,包含 869 行新增和 111 行删除
@@ -0,0 +1,166 @@
const tokenCache = new Map();
function toSafeString(value, maxLength = 2000) {
if (value == null) {
return '';
}
return String(value).trim().slice(0, maxLength);
}
function sanitizeWecomResponse(payload) {
if (!payload || typeof payload !== 'object') {
return null;
}
const result = {};
for (const key of ['errcode', 'errmsg', 'invaliduser', 'invalidparty', 'invalidtag']) {
if (payload[key] != null) {
result[key] = payload[key];
}
}
return Object.keys(result).length ? result : null;
}
function getCredentialKey(config) {
return [
toSafeString(config?.wecomCorpId, 128),
toSafeString(config?.wecomAgentId, 64),
toSafeString(config?.wecomAgentSecret, 16),
].join(':');
}
function resolveTimeoutMs(timeoutMs) {
return Math.max(100, Number(timeoutMs ?? 5000));
}
function buildMissingConfigResult(missing) {
return {
ok: false,
retryable: false,
code: 'wecom_app_missing_config',
message: `Missing WeCom app config: ${missing.join(',')}`,
response: {
missing,
},
accessToken: null,
};
}
async function getWecomAppAccessToken({ config, timeoutMs = 5000, fetchImpl = fetch }) {
const corpId = toSafeString(config?.wecomCorpId, 128);
const agentId = toSafeString(config?.wecomAgentId, 64);
const agentSecret = toSafeString(config?.wecomAgentSecret, 500);
const missing = [];
if (!corpId) missing.push('WECOM_CORP_ID');
if (!agentId) missing.push('WECOM_AGENT_ID');
if (!agentSecret) missing.push('WECOM_AGENT_SECRET');
if (missing.length) {
return buildMissingConfigResult(missing);
}
const cacheKey = getCredentialKey(config);
const cached = tokenCache.get(cacheKey);
if (cached?.accessToken && cached.expiresAt > Date.now() + 60 * 1000) {
return {
ok: true,
retryable: false,
code: 'wecom_app_token_cached',
message: null,
response: { cached: true },
accessToken: cached.accessToken,
};
}
const timeout = resolveTimeoutMs(timeoutMs);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const url = new URL('https://qyapi.weixin.qq.com/cgi-bin/gettoken');
url.searchParams.set('corpid', corpId);
url.searchParams.set('corpsecret', agentSecret);
try {
const response = await fetchImpl(url, {
method: 'GET',
signal: controller.signal,
});
const payload = await response.json().catch(() => null);
const sanitized = sanitizeWecomResponse(payload);
if (!response.ok) {
return {
ok: false,
retryable: response.status >= 500 || response.status === 429,
code:
response.status >= 500
? 'wecom_app_token_http_5xx'
: response.status === 429
? 'wecom_app_token_http_429'
: 'wecom_app_token_http_4xx',
message: `WeCom app token HTTP ${response.status}`,
response: {
status: response.status,
body: sanitized,
},
accessToken: null,
};
}
if (!payload || Number(payload.errcode ?? 0) !== 0 || !payload.access_token) {
return {
ok: false,
retryable: false,
code: 'wecom_app_token_api_error',
message: `WeCom app token errcode ${payload?.errcode ?? 'missing_payload'}`,
response: sanitized,
accessToken: null,
};
}
const expiresInSeconds = Math.max(300, Number(payload.expires_in ?? 7200));
tokenCache.set(cacheKey, {
accessToken: String(payload.access_token),
expiresAt: Date.now() + Math.max(60, expiresInSeconds - 120) * 1000,
});
return {
ok: true,
retryable: false,
code: 'wecom_app_token_fetched',
message: null,
response: { cached: false, expiresIn: expiresInSeconds },
accessToken: String(payload.access_token),
};
} catch (error) {
if (error?.name === 'AbortError') {
return {
ok: false,
retryable: true,
code: 'wecom_app_token_timeout',
message: `WeCom app token timeout (${timeout}ms)`,
response: null,
accessToken: null,
};
}
return {
ok: false,
retryable: true,
code: 'wecom_app_token_network_error',
message: String(error?.message ?? error ?? 'WeCom app token network error'),
response: null,
accessToken: null,
};
} finally {
clearTimeout(timer);
}
}
function clearWecomAppAccessTokenCache() {
tokenCache.clear();
}
module.exports = {
clearWecomAppAccessTokenCache,
getWecomAppAccessToken,
sanitizeWecomResponse,
};