Files
workphone-sdk/sdk/scripts/run_wechat_private_domain_acceptance.mjs

195 lines
7.7 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const deviceId = process.env.WORKPHONE_DEVICE_ID || "xgfe65eimrrofyws";
const base = (process.env.WORKPHONE_BASE || "http://127.0.0.1:8899").replace(/\/$/, "");
const writeTo = process.env.WORKPHONE_TEST_TO_ID || "";
const includeRealWrite = process.env.WORKPHONE_INCLUDE_REAL_WRITE === "1";
const stamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
const outputDir = path.join(
root,
"开发文档/8、部署/05-测试验收",
`${stamp.slice(0, 8)}_微信私域能力验收`,
);
const rows = [];
async function request(method, route, body) {
const response = await fetch(`${base}${route}`, {
method,
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(90_000),
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(data).slice(0, 300)}`);
}
return data;
}
function query(route, params) {
const search = new URLSearchParams(params);
return `${route}?${search.toString()}`;
}
function dataOf(body) {
return body?.data && typeof body.data === "object" ? body.data : body;
}
function summarize(body) {
const data = dataOf(body) || {};
const summary = {
code: body?.code,
channel_used: body?.channel_used || data.channel,
success: data.success ?? body?.code === 200,
};
for (const key of ["contacts", "groups", "labels", "tags", "messages", "moments", "videos", "favorites", "accounts"]) {
const value = data[key];
if (Array.isArray(value)) summary[`${key}_count`] = value.length;
}
for (const key of [
"count",
"contact_count",
"group_count",
"tag_count",
"message_count",
"returned_count",
"requested_limit",
"offset",
"total_count",
"raw_total_count",
"has_more",
"dry_run",
"confirm_required",
"error_code",
"error",
"note",
]) {
if (key in data) summary[key] = data[key];
}
return summary;
}
function passes(body) {
const data = dataOf(body) || {};
return (
body?.code === undefined ||
body.code === 200 ||
data.success === true ||
data.dry_run === true ||
data.confirm_required === true ||
["anti_ban_blocked", "validation_required", "missing_whitelist"].includes(data.error_code)
);
}
async function run(name, category, task) {
const started = Date.now();
process.stdout.write(`${category.padEnd(10)} ${name.padEnd(28)} `);
try {
const body = await task();
const status = passes(body) ? "passed" : "failed";
rows.push({ name, category, status, elapsed_ms: Date.now() - started, summary: summarize(body) });
console.log(`${status} ${Date.now() - started}ms`);
} catch (error) {
rows.push({ name, category, status: "failed", elapsed_ms: Date.now() - started, error: String(error.message || error) });
console.log(`failed ${Date.now() - started}ms`);
}
}
const common = { device_id: deviceId, platform: "wechat" };
const hook = (action, params = {}) =>
request("POST", "/api/v3/hook/execute", {
...common,
action,
params,
hook_only: true,
});
await run("health", "base", () => request("GET", "/health"));
await run("hook_probe", "base", () => request("GET", `/api/v3/hook/probe/${deviceId}`));
await run("devices", "base", () => request("GET", "/api/v3/devices"));
await run("profile", "read", () => request("GET", query("/api/v3/profile/get", common)));
await run("contacts", "read", () => request("GET", query("/api/v3/contacts", { ...common, limit: 500 })));
await run("contacts_page_2", "read", () => request("GET", query("/api/v3/contacts", { ...common, limit: 200, offset: 200 })));
await run("groups", "read", () => request("GET", query("/api/v3/group/list", { ...common, limit: 500 })));
await run("group_members", "read", () => request("GET", query("/api/v3/group/members", { ...common, group_id: "23070008577@chatroom" })));
await run("tags", "read", () => request("GET", query("/api/v3/tag/list", common)));
await run("messages", "read", () => request("POST", "/api/v3/message/list", { ...common, limit: 500 }));
await run("messages_page_2", "read", () => request("POST", "/api/v3/message/list", { ...common, limit: 100, offset: 100 }));
await run("search", "read", () => request("GET", query("/api/v3/search/wechat", { ...common, keyword: "客户" })));
await run("hook_data_preview", "read", () =>
request(
"GET",
query(`/api/v3/hook/data/${deviceId}`, {
modules: "profile,contacts,groups,labels,messages,device_info",
contact_limit: 200,
message_limit: 50,
}),
),
);
await run("customer_profile_bundle", "read", () =>
request("GET", query("/api/v3/customer/profile-bundle", { ...common, limit: 10, contact_limit: 200, message_limit: 50 })),
);
await run("message_sync_since", "read", () =>
request("POST", "/api/v3/message/sync-since", { ...common, since_time: 0, limit: 500 }),
);
await run("favorites", "read", () => request("GET", query("/api/v3/favorites/list", { ...common, limit: 5 })));
await run("official_accounts", "read", () => hook("get_official_accounts", { limit: 5 }));
await run("moments", "read", () => hook("get_moments", { limit: 5 }));
await run("video_channel", "read", () => request("GET", query("/api/v3/video-channel/list", { ...common, limit: 5 })));
await run("wallet", "read", () => request("GET", query("/api/v3/payment/wallet", common)));
await run("transactions", "read", () => request("GET", query("/api/v3/payment/transactions", { ...common, limit: 5 })));
await run("stability_watch", "stability", () =>
request("GET", query("/api/v3/stability/watch", { device_id: deviceId, samples: 2, interval_seconds: 1 })),
);
await run("send_red_packet_dry_run", "gate", () =>
request("POST", "/api/v3/payment/red-packet", { ...common, to_id: writeTo || "filehelper", amount: "0.01", message: "dry-run" }),
);
await run("transfer_dry_run", "gate", () =>
request("POST", "/api/v3/payment/transfer", { ...common, to_id: writeTo || "filehelper", amount: "0.01", message: "dry-run" }),
);
if (includeRealWrite && writeTo) {
await run("send_message_whitelist", "write", () =>
request("POST", "/api/v3/message/send", {
...common,
to_id: writeTo,
content: `[私域验收] ${stamp}`,
msg_type: "text",
channel: "hook",
}),
);
} else {
rows.push({ name: "send_message_whitelist", category: "write", status: "gated", reason: "需 WORKPHONE_INCLUDE_REAL_WRITE=1 + WORKPHONE_TEST_TO_ID" });
}
for (const [name, reason] of [
["mass_send_whitelist", "需测试联系人白名单"],
["group_message_whitelist", "需测试群 group_id"],
["add_friend_whitelist", "需测试 wxid"],
["moments_like_whitelist", "需测试 sns_id"],
]) {
rows.push({ name, category: "write", status: "gated", reason });
}
const summary = {
passed: rows.filter((row) => row.status === "passed").length,
failed: rows.filter((row) => row.status === "failed").length,
gated: rows.filter((row) => row.status === "gated").length,
total: rows.length,
};
await mkdir(outputDir, { recursive: true });
const reportPath = path.join(outputDir, `private_domain_acceptance_node_${stamp}.json`);
await writeFile(reportPath, `${JSON.stringify({ timestamp: new Date().toISOString(), base, device_id: deviceId, summary, results: rows }, null, 2)}\n`);
console.log(`\n私域能力验收(Node): passed=${summary.passed} failed=${summary.failed} gated=${summary.gated} -> ${reportPath}`);
process.exitCode = summary.failed === 0 ? 0 : 1;