Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post articles and image-text content to WeChat Official Account via API or Chrome CDP, with markdown-to-WeChat HTML conversion.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/wechat-socks-http.ts
1import net from "node:net";2import tls from "node:tls";3import { URL } from "node:url";4import { SocksClient } from "socks";56import type {7WechatClient,8WechatHttpInit,9WechatHttpResponse,10} from "./wechat-http.ts";1112export interface SocksProxyEndpoint {13host: string;14port: number;15}1617export function createSocksClient(proxy: SocksProxyEndpoint): WechatClient {18if (!proxy.host) throw new Error("SOCKS proxy host required");19if (!Number.isInteger(proxy.port) || proxy.port < 1 || proxy.port > 65535) {20throw new Error(`Invalid SOCKS proxy port: ${proxy.port}`);21}2223return async (url, init = {}) => {24return wechatHttpViaSocks(url, init, proxy);25};26}2728async function wechatHttpViaSocks(29rawUrl: string,30init: WechatHttpInit,31proxy: SocksProxyEndpoint,32): Promise<WechatHttpResponse> {33const url = new URL(rawUrl);34const isHttps = url.protocol === "https:";35if (url.protocol !== "https:" && url.protocol !== "http:") {36throw new Error(`Unsupported protocol for SOCKS client: ${url.protocol}`);37}38const targetPort = url.port ? Number(url.port) : isHttps ? 443 : 80;3940const { socket: tcpSocket } = await SocksClient.createConnection({41proxy: { host: proxy.host, port: proxy.port, type: 5 },42command: "connect",43destination: { host: url.hostname, port: targetPort },44});4546let stream: net.Socket | tls.TLSSocket;47if (isHttps) {48const tlsSocket = tls.connect({ socket: tcpSocket, servername: url.hostname });49await new Promise<void>((resolve, reject) => {50const onSecure = () => {51tlsSocket.removeListener("error", onError);52resolve();53};54const onError = (err: Error) => {55tlsSocket.removeListener("secureConnect", onSecure);56try {57tlsSocket.destroy();58} catch {59/* noop */60}61try {62tcpSocket.destroy();63} catch {64/* noop */65}66reject(err);67};68tlsSocket.once("secureConnect", onSecure);69tlsSocket.once("error", onError);70});71stream = tlsSocket;72} else {73stream = tcpSocket;74}7576try {77return await sendRequestAndReadResponse(stream, url, init);78} finally {79try {80stream.destroy();81} catch {82/* noop */83}84}85}8687async function sendRequestAndReadResponse(88stream: net.Socket | tls.TLSSocket,89url: URL,90init: WechatHttpInit,91): Promise<WechatHttpResponse> {92const method = init.method ?? (init.body !== undefined ? "POST" : "GET");93const body =94init.body === undefined95? undefined96: Buffer.isBuffer(init.body)97? init.body98: Buffer.from(init.body, "utf-8");99100const userHeaders = init.headers ?? {};101const headerMap = new Map<string, string>();102for (const [k, v] of Object.entries(userHeaders)) {103headerMap.set(k.toLowerCase(), `${k}: ${v}`);104}105if (!headerMap.has("host")) headerMap.set("host", `Host: ${url.host}`);106if (!headerMap.has("user-agent")) {107headerMap.set("user-agent", "User-Agent: baoyu-skills-wechat-api");108}109headerMap.set("connection", "Connection: close");110if (body && !headerMap.has("content-length")) {111headerMap.set("content-length", `Content-Length: ${body.length}`);112}113114const path = `${url.pathname || "/"}${url.search}`;115const requestHeader = Buffer.from(116`${method} ${path} HTTP/1.1\r\n` +117Array.from(headerMap.values()).join("\r\n") +118"\r\n\r\n",119"utf-8",120);121122await writeAll(stream, requestHeader);123if (body) await writeAll(stream, body);124125const raw = await readToEnd(stream);126return parseHttpResponse(raw);127}128129function writeAll(stream: net.Socket | tls.TLSSocket, data: Buffer): Promise<void> {130return new Promise((resolve, reject) => {131stream.write(data, (err) => {132if (err) reject(err);133else resolve();134});135});136}137138function readToEnd(stream: net.Socket | tls.TLSSocket): Promise<Buffer> {139return new Promise((resolve, reject) => {140const chunks: Buffer[] = [];141stream.on("data", (chunk: Buffer) => chunks.push(chunk));142stream.once("end", () => resolve(Buffer.concat(chunks)));143stream.once("error", reject);144});145}146147function parseHttpResponse(raw: Buffer): WechatHttpResponse {148const headerEnd = raw.indexOf("\r\n\r\n");149if (headerEnd < 0) {150throw new Error("Malformed HTTP response: missing header terminator");151}152const headerText = raw.subarray(0, headerEnd).toString("utf-8");153let bodyBytes = raw.subarray(headerEnd + 4);154155const lines = headerText.split("\r\n");156const statusLine = lines.shift() ?? "";157const statusMatch = statusLine.match(/^HTTP\/[\d.]+\s+(\d+)(?:\s+(.*))?$/);158if (!statusMatch) {159throw new Error(`Malformed HTTP status line: ${statusLine}`);160}161const status = Number.parseInt(statusMatch[1]!, 10);162const statusText = statusMatch[2] ?? "";163164const headers: Record<string, string | string[] | undefined> = {};165const lowercaseHeaders: Record<string, string> = {};166for (const line of lines) {167const colon = line.indexOf(":");168if (colon < 0) continue;169const key = line.slice(0, colon).trim();170const value = line.slice(colon + 1).trim();171const lower = key.toLowerCase();172const existing = headers[lower];173if (existing === undefined) {174headers[lower] = value;175} else if (Array.isArray(existing)) {176existing.push(value);177} else {178headers[lower] = [existing, value];179}180lowercaseHeaders[lower] = value;181}182183const transferEncoding = (lowercaseHeaders["transfer-encoding"] ?? "").toLowerCase();184if (transferEncoding.split(",").map((s) => s.trim()).includes("chunked")) {185bodyBytes = dechunk(bodyBytes);186} else if (lowercaseHeaders["content-length"] !== undefined) {187const length = Number.parseInt(lowercaseHeaders["content-length"]!, 10);188if (Number.isFinite(length) && length >= 0) {189bodyBytes = bodyBytes.subarray(0, length);190}191}192193return {194status,195statusText,196headers,197async buffer() {198return bodyBytes;199},200async text() {201return bodyBytes.toString("utf-8");202},203async json<T = unknown>() {204return JSON.parse(bodyBytes.toString("utf-8")) as T;205},206};207}208209function dechunk(raw: Buffer): Buffer {210const parts: Buffer[] = [];211let offset = 0;212while (offset < raw.length) {213const lineEnd = raw.indexOf("\r\n", offset);214if (lineEnd < 0) break;215const sizeText = raw.subarray(offset, lineEnd).toString("ascii").split(";")[0]!.trim();216const size = Number.parseInt(sizeText, 16);217if (!Number.isFinite(size) || size < 0) {218throw new Error(`Invalid chunked-encoding size: ${sizeText}`);219}220offset = lineEnd + 2;221if (size === 0) break;222if (offset + size > raw.length) {223throw new Error("Chunked-encoding body truncated");224}225parts.push(raw.subarray(offset, offset + size));226offset += size + 2;227}228return Buffer.concat(parts);229}230