添加完整的微信插件测试框架,包含: - 微信插件源码(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>
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { resolveStateDir } from "../storage/state-dir.js";
|
|
import { logger } from "../util/logger.js";
|
|
|
|
function resolveCredentialsDir(): string {
|
|
const override = process.env.OPENCLAW_OAUTH_DIR?.trim();
|
|
if (override) return override;
|
|
return path.join(resolveStateDir(), "credentials");
|
|
}
|
|
|
|
function safeKey(raw: string): string {
|
|
const trimmed = raw.trim().toLowerCase();
|
|
if (!trimmed) throw new Error("invalid key for allowFrom path");
|
|
const safe = trimmed.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
|
|
if (!safe || safe === "_") throw new Error("invalid key for allowFrom path");
|
|
return safe;
|
|
}
|
|
|
|
export function resolveFrameworkAllowFromPath(accountId: string): string {
|
|
const base = safeKey("openclaw-weixin");
|
|
const safeAccount = safeKey(accountId);
|
|
return path.join(resolveCredentialsDir(), `${base}-${safeAccount}-allowFrom.json`);
|
|
}
|
|
|
|
type AllowFromFileContent = {
|
|
version: number;
|
|
allowFrom: string[];
|
|
};
|
|
|
|
export function readFrameworkAllowFromList(accountId: string): string[] {
|
|
const filePath = resolveFrameworkAllowFromPath(accountId);
|
|
try {
|
|
if (!fs.existsSync(filePath)) return [];
|
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
const parsed = JSON.parse(raw) as AllowFromFileContent;
|
|
if (Array.isArray(parsed.allowFrom)) {
|
|
return parsed.allowFrom.filter((id): id is string => typeof id === "string" && id.trim() !== "");
|
|
}
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
return [];
|
|
}
|
|
|
|
async function withFileLock<T>(
|
|
filePath: string,
|
|
_options: { retries: { retries: number; factor: number; minTimeout: number; maxTimeout: number }; stale: number },
|
|
fn: () => Promise<T>
|
|
): Promise<T> {
|
|
const lockPath = `${filePath}.lock`;
|
|
const maxRetries = 3;
|
|
let attempt = 0;
|
|
|
|
while (attempt < maxRetries) {
|
|
try {
|
|
const dir = path.dirname(lockPath);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
|
|
const lockContent = JSON.stringify({ pid: process.pid, time: Date.now() });
|
|
fs.writeFileSync(lockPath, lockContent, { flag: "wx" });
|
|
const result = await fn();
|
|
fs.unlinkSync(lockPath);
|
|
return result;
|
|
} catch (err: any) {
|
|
if (err.code === "EEXIST") {
|
|
attempt++;
|
|
if (attempt >= maxRetries) break;
|
|
await new Promise((r) => setTimeout(r, 100 * attempt));
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
return await fn();
|
|
}
|
|
|
|
export async function registerUserInFrameworkStore(params: {
|
|
accountId: string;
|
|
userId: string;
|
|
}): Promise<{ changed: boolean }> {
|
|
const { accountId, userId } = params;
|
|
const trimmedUserId = userId.trim();
|
|
if (!trimmedUserId) return { changed: false };
|
|
|
|
const filePath = resolveFrameworkAllowFromPath(accountId);
|
|
|
|
const dir = path.dirname(filePath);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
const initial: AllowFromFileContent = { version: 1, allowFrom: [] };
|
|
fs.writeFileSync(filePath, JSON.stringify(initial, null, 2), "utf-8");
|
|
}
|
|
|
|
const LOCK_OPTIONS = {
|
|
retries: { retries: 3, factor: 2, minTimeout: 100, maxTimeout: 2000 },
|
|
stale: 10_000,
|
|
};
|
|
|
|
return await withFileLock(filePath, LOCK_OPTIONS, async () => {
|
|
let content: AllowFromFileContent = { version: 1, allowFrom: [] };
|
|
try {
|
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
const parsed = JSON.parse(raw) as AllowFromFileContent;
|
|
if (Array.isArray(parsed.allowFrom)) {
|
|
content = parsed;
|
|
}
|
|
} catch {
|
|
// If read/parse fails, start fresh
|
|
}
|
|
|
|
if (content.allowFrom.includes(trimmedUserId)) {
|
|
return { changed: false };
|
|
}
|
|
|
|
content.allowFrom.push(trimmedUserId);
|
|
fs.writeFileSync(filePath, JSON.stringify(content, null, 2), "utf-8");
|
|
logger.info(
|
|
`registerUserInFrameworkStore: added userId=${trimmedUserId} accountId=${accountId} path=${filePath}`,
|
|
);
|
|
return { changed: true };
|
|
});
|
|
} |