添加完整的微信插件测试框架,包含: - 微信插件源码(weixin-plugin/) - TypeScript 类型定义(types.ts) - 插件编译脚本(scripts/build-weixin-plugin.sh) - 简化测试脚本(scripts/test-weixin-simple.js) - 完整测试脚本(scripts/test-weixin-plugin.js) - 项目配置和依赖管理(package.json) - 使用说明文档(README.md) 主要功能: - 支持二维码登录认证 - HTTP 长轮询消息接收 - 文本、图片、视频、文件消息收发 - 自动回复测试 - 运行状态统计 Generated with [Claude Code](https://claude.com/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { getConfig } from "./api.js";
|
|
|
|
/** Subset of getConfig fields that we actually need; add new fields here as needed. */
|
|
export interface CachedConfig {
|
|
typingTicket: string;
|
|
}
|
|
|
|
const CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
const CONFIG_CACHE_INITIAL_RETRY_MS = 2_000;
|
|
const CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
|
|
interface ConfigCacheEntry {
|
|
config: CachedConfig;
|
|
everSucceeded: boolean;
|
|
nextFetchAt: number;
|
|
retryDelayMs: number;
|
|
}
|
|
|
|
/**
|
|
* Per-user getConfig cache with periodic random refresh (within 24h) and
|
|
* exponential-backoff retry (up to 1h) on failure.
|
|
*/
|
|
export class WeixinConfigManager {
|
|
private cache = new Map<string, ConfigCacheEntry>();
|
|
|
|
constructor(
|
|
private apiOpts: { baseUrl: string; token?: string },
|
|
private log: (msg: string) => void,
|
|
) {}
|
|
|
|
async getForUser(userId: string, contextToken?: string): Promise<CachedConfig> {
|
|
const now = Date.now();
|
|
const entry = this.cache.get(userId);
|
|
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
|
|
if (shouldFetch) {
|
|
let fetchOk = false;
|
|
try {
|
|
const resp = await getConfig({
|
|
baseUrl: this.apiOpts.baseUrl,
|
|
token: this.apiOpts.token,
|
|
ilinkUserId: userId,
|
|
contextToken,
|
|
});
|
|
if (resp.ret === 0) {
|
|
this.cache.set(userId, {
|
|
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
everSucceeded: true,
|
|
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
});
|
|
this.log(
|
|
`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`,
|
|
);
|
|
fetchOk = true;
|
|
}
|
|
} catch (err) {
|
|
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
}
|
|
if (!fetchOk) {
|
|
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
if (entry) {
|
|
entry.nextFetchAt = now + nextDelay;
|
|
entry.retryDelayMs = nextDelay;
|
|
} else {
|
|
this.cache.set(userId, {
|
|
config: { typingTicket: "" },
|
|
everSucceeded: false,
|
|
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
}
|
|
}
|