Merge feature/add_skin into master: resolve all conflicts

- GameGlobal.js: keep upstream SERVER_URL with /ws suffix
- en.js/zh.js: merge both settings.nickname and settings.profile keys
- SettingsScene.js: keep both nickname row and profile button
- server/index.js: merge express app + content security proxy with
  noServer WebSocket mode and path validation
- Add .gitignore for node_modules and .codebuddy
This commit is contained in:
jakciehan
2026-05-12 07:05:20 +08:00
parent 38294c040c
commit d263c7bf48
48 changed files with 10480 additions and 25 deletions
@@ -0,0 +1,223 @@
/**
* WeChat Access Token Manager
* Manages the lifecycle of WeChat access tokens for content security APIs.
* - Auto-refreshes tokens before expiry (2-hour validity, refresh 5 minutes early)
* - Retry with exponential backoff on failure (2s, 4s, 8s)
* - Marks service as unavailable after all retries exhausted
*/
const https = require('https');
// ============================================================
// Configuration
// ============================================================
const TOKEN_VALIDITY_MS = 2 * 60 * 60 * 1000; // 2 hours
const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000; // Refresh 5 minutes before expiry
const RETRY_DELAYS = [2000, 4000, 8000]; // Exponential backoff delays in ms
const WECHAT_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/token';
class WechatTokenManager {
/**
* @param {object} options
* @param {string} options.appId - WeChat Mini Program App ID
* @param {string} options.appSecret - WeChat Mini Program App Secret
*/
constructor(options = {}) {
this.appId = options.appId || process.env.WX_APPID || '';
this.appSecret = options.appSecret || process.env.WX_APPSECRET || '';
this._accessToken = null;
this._expiresAt = 0;
this._refreshTimer = null;
this._isAvailable = false;
this._isRefreshing = false;
this._retryCount = 0;
}
/**
* Initialize the token manager and fetch the first token.
* @returns {Promise<boolean>} true if token obtained successfully
*/
async init() {
console.log('[TokenManager] Initializing...');
const success = await this._refreshToken();
if (success) {
this._scheduleRefresh();
}
return success;
}
/**
* Get the current valid access token.
* If the token is expired or about to expire, refresh it first.
* @returns {Promise<string|null>} The access token, or null if unavailable
*/
async getAccessToken() {
// If token is still valid (with margin), return it directly
if (this._accessToken && Date.now() < this._expiresAt - TOKEN_REFRESH_MARGIN_MS) {
return this._accessToken;
}
// Token expired or about to expire, try to refresh
if (!this._isRefreshing) {
const success = await this._refreshToken();
if (success) {
this._scheduleRefresh();
}
}
return this._isAvailable ? this._accessToken : null;
}
/**
* Check if the token manager is currently available.
* @returns {boolean}
*/
isAvailable() {
return this._isAvailable;
}
/**
* Refresh the access token by calling WeChat API.
* Implements retry with exponential backoff.
* @returns {Promise<boolean>}
*/
async _refreshToken() {
if (this._isRefreshing) {
// Wait for the ongoing refresh to complete
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!this._isRefreshing) {
clearInterval(checkInterval);
resolve(this._isAvailable);
}
}, 100);
});
}
this._isRefreshing = true;
for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
try {
const token = await this._fetchTokenFromWechat();
if (token) {
this._accessToken = token;
this._expiresAt = Date.now() + TOKEN_VALIDITY_MS;
this._isAvailable = true;
this._retryCount = 0;
this._isRefreshing = false;
console.log('[TokenManager] Token refreshed successfully, expires at:',
new Date(this._expiresAt).toISOString());
return true;
}
} catch (err) {
console.error(`[TokenManager] Fetch token failed (attempt ${attempt + 1}):`, err.message);
}
// Wait before retry (if there are more retries)
if (attempt < RETRY_DELAYS.length) {
const delay = RETRY_DELAYS[attempt];
console.log(`[TokenManager] Retrying in ${delay}ms...`);
await this._sleep(delay);
}
}
// All retries exhausted
this._isAvailable = false;
this._isRefreshing = false;
this._accessToken = null;
console.error('[TokenManager] All retry attempts exhausted. Service marked as unavailable.');
// Schedule a recovery attempt after 10 minutes
setTimeout(() => {
console.log('[TokenManager] Attempting recovery refresh...');
this._refreshToken().then((success) => {
if (success) {
this._scheduleRefresh();
}
});
}, 10 * 60 * 1000);
return false;
}
/**
* Fetch access token from WeChat API.
* @returns {Promise<string|null>}
*/
_fetchTokenFromWechat() {
return new Promise((resolve, reject) => {
if (!this.appId || !this.appSecret) {
reject(new Error('AppId or AppSecret not configured'));
return;
}
const url = `${WECHAT_TOKEN_URL}?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`;
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const result = JSON.parse(data);
if (result.access_token) {
resolve(result.access_token);
} else {
reject(new Error(`WeChat API error: ${result.errcode} - ${result.errmsg}`));
}
} catch (e) {
reject(new Error(`Invalid JSON response: ${data.substring(0, 100)}`));
}
});
}).on('error', (err) => {
reject(err);
});
});
}
/**
* Schedule the next token refresh before expiry.
*/
_scheduleRefresh() {
if (this._refreshTimer) {
clearTimeout(this._refreshTimer);
}
const refreshAt = this._expiresAt - TOKEN_REFRESH_MARGIN_MS;
const delay = Math.max(refreshAt - Date.now(), 60000); // At least 1 minute
console.log(`[TokenManager] Next refresh scheduled in ${Math.round(delay / 1000)}s`);
this._refreshTimer = setTimeout(async () => {
console.log('[TokenManager] Scheduled refresh triggered');
const success = await this._refreshToken();
if (success) {
this._scheduleRefresh();
}
}, delay);
}
/**
* Utility: sleep for a given number of milliseconds.
* @param {number} ms
* @returns {Promise<void>}
*/
_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Clean up resources.
*/
destroy() {
if (this._refreshTimer) {
clearTimeout(this._refreshTimer);
this._refreshTimer = null;
}
this._accessToken = null;
this._isAvailable = false;
console.log('[TokenManager] Destroyed');
}
}
module.exports = WechatTokenManager;