#!/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;