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-extend-config.ts
1import fs from "node:fs";2import path from "node:path";3import os from "node:os";45export type StrictHostKeyChecking = "yes" | "no" | "accept-new";67export interface WechatAccount {8name: string;9alias: string;10default?: boolean;11default_publish_method?: string;12default_author?: string;13need_open_comment?: number;14only_fans_can_comment?: number;15app_id?: string;16app_secret?: string;17chrome_profile_path?: string;18remote_publish_host?: string;19remote_publish_user?: string;20remote_publish_port?: number;21remote_publish_identity_file?: string;22remote_publish_known_hosts_file?: string;23remote_publish_strict_host_key_checking?: StrictHostKeyChecking;24remote_publish_connect_timeout?: number;25remote_publish_proxy_jump?: string;26}2728export interface WechatExtendConfig {29default_theme?: string;30default_color?: string;31default_publish_method?: string;32default_author?: string;33need_open_comment?: number;34only_fans_can_comment?: number;35chrome_profile_path?: string;36remote_publish_host?: string;37remote_publish_user?: string;38remote_publish_port?: number;39remote_publish_identity_file?: string;40remote_publish_known_hosts_file?: string;41remote_publish_strict_host_key_checking?: StrictHostKeyChecking;42remote_publish_connect_timeout?: number;43remote_publish_proxy_jump?: string;44accounts?: WechatAccount[];45}4647export interface ResolvedAccount {48name?: string;49alias?: string;50default_publish_method?: string;51default_author?: string;52need_open_comment: number;53only_fans_can_comment: number;54app_id?: string;55app_secret?: string;56chrome_profile_path?: string;57remote_publish_host?: string;58remote_publish_user?: string;59remote_publish_port?: number;60remote_publish_identity_file?: string;61remote_publish_known_hosts_file?: string;62remote_publish_strict_host_key_checking?: StrictHostKeyChecking;63remote_publish_connect_timeout?: number;64remote_publish_proxy_jump?: string;65}6667function stripQuotes(s: string): string {68return s.replace(/^['"]|['"]$/g, "");69}7071function toBool01(v: string): number {72return v === "1" || v === "true" ? 1 : 0;73}7475function homeDir(): string {76return process.env.HOME || process.env.USERPROFILE || os.homedir();77}7879function parsePort(key: string, v: string): number {80const n = Number.parseInt(v, 10);81if (!Number.isFinite(n) || String(n) !== v.trim() || n < 1 || n > 65535) {82throw new Error(`Invalid ${key}: ${v} (expected integer 1-65535)`);83}84return n;85}8687function parsePositiveInt(key: string, v: string): number {88const n = Number.parseInt(v, 10);89if (!Number.isFinite(n) || String(n) !== v.trim() || n <= 0) {90throw new Error(`Invalid ${key}: ${v} (expected positive integer)`);91}92return n;93}9495function parseStrictHostKeyChecking(key: string, v: string): StrictHostKeyChecking {96const lower = v.toLowerCase();97if (lower === "yes" || lower === "no" || lower === "accept-new") {98return lower;99}100throw new Error(`Invalid ${key}: ${v} (expected yes|no|accept-new)`);101}102103function parseWechatExtend(content: string): WechatExtendConfig {104const config: WechatExtendConfig = {};105const lines = content.split("\n");106let inAccounts = false;107let current: Record<string, string> | null = null;108const rawAccounts: Record<string, string>[] = [];109110for (const raw of lines) {111const trimmed = raw.trim();112if (!trimmed || trimmed.startsWith("#")) continue;113114if (trimmed === "accounts:") {115inAccounts = true;116continue;117}118119if (inAccounts) {120const listMatch = raw.match(/^\s+-\s+(.+)$/);121if (listMatch) {122if (current) rawAccounts.push(current);123current = {};124const kv = listMatch[1]!;125const ci = kv.indexOf(":");126if (ci > 0) {127current[kv.slice(0, ci).trim()] = stripQuotes(kv.slice(ci + 1).trim());128}129continue;130}131132if (current && /^\s{2,}/.test(raw) && !trimmed.startsWith("-")) {133const ci = trimmed.indexOf(":");134if (ci > 0) {135current[trimmed.slice(0, ci).trim()] = stripQuotes(trimmed.slice(ci + 1).trim());136}137continue;138}139140if (!/^\s/.test(raw)) {141if (current) rawAccounts.push(current);142current = null;143inAccounts = false;144} else {145continue;146}147}148149const ci = trimmed.indexOf(":");150if (ci < 0) continue;151const key = trimmed.slice(0, ci).trim();152const val = stripQuotes(trimmed.slice(ci + 1).trim());153if (val === "null" || val === "") continue;154155switch (key) {156case "default_theme": config.default_theme = val; break;157case "default_color": config.default_color = val; break;158case "default_publish_method": config.default_publish_method = val; break;159case "default_author": config.default_author = val; break;160case "need_open_comment": config.need_open_comment = toBool01(val); break;161case "only_fans_can_comment": config.only_fans_can_comment = toBool01(val); break;162case "chrome_profile_path": config.chrome_profile_path = val; break;163case "remote_publish_host": config.remote_publish_host = val; break;164case "remote_publish_user": config.remote_publish_user = val; break;165case "remote_publish_port": config.remote_publish_port = parsePort("remote_publish_port", val); break;166case "remote_publish_identity_file": config.remote_publish_identity_file = val; break;167case "remote_publish_known_hosts_file": config.remote_publish_known_hosts_file = val; break;168case "remote_publish_strict_host_key_checking": config.remote_publish_strict_host_key_checking = parseStrictHostKeyChecking("remote_publish_strict_host_key_checking", val); break;169case "remote_publish_connect_timeout": config.remote_publish_connect_timeout = parsePositiveInt("remote_publish_connect_timeout", val); break;170case "remote_publish_proxy_jump": config.remote_publish_proxy_jump = val; break;171}172}173174if (current) rawAccounts.push(current);175176if (rawAccounts.length > 0) {177config.accounts = rawAccounts.map(a => ({178name: a.name || "",179alias: a.alias || "",180default: a.default === "true" || a.default === "1",181default_publish_method: a.default_publish_method || undefined,182default_author: a.default_author || undefined,183need_open_comment: a.need_open_comment ? toBool01(a.need_open_comment) : undefined,184only_fans_can_comment: a.only_fans_can_comment ? toBool01(a.only_fans_can_comment) : undefined,185app_id: a.app_id || undefined,186app_secret: a.app_secret || undefined,187chrome_profile_path: a.chrome_profile_path || undefined,188remote_publish_host: a.remote_publish_host || undefined,189remote_publish_user: a.remote_publish_user || undefined,190remote_publish_port: a.remote_publish_port ? parsePort("remote_publish_port", a.remote_publish_port) : undefined,191remote_publish_identity_file: a.remote_publish_identity_file || undefined,192remote_publish_known_hosts_file: a.remote_publish_known_hosts_file || undefined,193remote_publish_strict_host_key_checking: a.remote_publish_strict_host_key_checking194? parseStrictHostKeyChecking("remote_publish_strict_host_key_checking", a.remote_publish_strict_host_key_checking)195: undefined,196remote_publish_connect_timeout: a.remote_publish_connect_timeout197? parsePositiveInt("remote_publish_connect_timeout", a.remote_publish_connect_timeout)198: undefined,199remote_publish_proxy_jump: a.remote_publish_proxy_jump || undefined,200}));201}202203return config;204}205206export function loadWechatExtendConfig(): WechatExtendConfig {207const paths = [208path.join(process.cwd(), ".baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"),209path.join(210process.env.XDG_CONFIG_HOME || path.join(homeDir(), ".config"),211"baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"212),213path.join(homeDir(), ".baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"),214];215for (const p of paths) {216let content: string;217try {218content = fs.readFileSync(p, "utf-8");219} catch {220continue;221}222return parseWechatExtend(content);223}224return {};225}226227function selectAccount(config: WechatExtendConfig, alias?: string): WechatAccount | undefined {228if (!config.accounts || config.accounts.length === 0) return undefined;229if (alias) return config.accounts.find(a => a.alias === alias);230if (config.accounts.length === 1) return config.accounts[0];231return config.accounts.find(a => a.default);232}233234export function resolveAccount(config: WechatExtendConfig, alias?: string): ResolvedAccount {235const acct = selectAccount(config, alias);236return {237name: acct?.name,238alias: acct?.alias,239default_publish_method: acct?.default_publish_method ?? config.default_publish_method,240default_author: acct?.default_author ?? config.default_author,241need_open_comment: acct?.need_open_comment ?? config.need_open_comment ?? 1,242only_fans_can_comment: acct?.only_fans_can_comment ?? config.only_fans_can_comment ?? 0,243app_id: acct?.app_id,244app_secret: acct?.app_secret,245chrome_profile_path: acct?.chrome_profile_path ?? config.chrome_profile_path,246remote_publish_host: acct?.remote_publish_host ?? config.remote_publish_host,247remote_publish_user: acct?.remote_publish_user ?? config.remote_publish_user,248remote_publish_port: acct?.remote_publish_port ?? config.remote_publish_port,249remote_publish_identity_file: acct?.remote_publish_identity_file ?? config.remote_publish_identity_file,250remote_publish_known_hosts_file: acct?.remote_publish_known_hosts_file ?? config.remote_publish_known_hosts_file,251remote_publish_strict_host_key_checking:252acct?.remote_publish_strict_host_key_checking ?? config.remote_publish_strict_host_key_checking,253remote_publish_connect_timeout: acct?.remote_publish_connect_timeout ?? config.remote_publish_connect_timeout,254remote_publish_proxy_jump: acct?.remote_publish_proxy_jump ?? config.remote_publish_proxy_jump,255};256}257258function loadEnvFile(envPath: string): Record<string, string> {259const env: Record<string, string> = {};260if (!fs.existsSync(envPath)) return env;261const content = fs.readFileSync(envPath, "utf-8");262for (const line of content.split("\n")) {263const trimmed = line.trim();264if (!trimmed || trimmed.startsWith("#")) continue;265const eqIdx = trimmed.indexOf("=");266if (eqIdx > 0) {267const key = trimmed.slice(0, eqIdx).trim();268let value = trimmed.slice(eqIdx + 1).trim();269if ((value.startsWith('"') && value.endsWith('"')) ||270(value.startsWith("'") && value.endsWith("'"))) {271value = value.slice(1, -1);272}273env[key] = value;274}275}276return env;277}278279function aliasToEnvKey(alias: string): string {280return alias.toUpperCase().replace(/-/g, "_");281}282283interface CredentialSource {284name: string;285appIdKey: string;286appSecretKey: string;287appId?: string;288appSecret?: string;289}290291export interface LoadedCredentials {292appId: string;293appSecret: string;294source: string;295skippedSources: string[];296}297298function normalizeCredentialValue(value?: string): string | undefined {299const trimmed = value?.trim();300return trimmed ? trimmed : undefined;301}302303function describeMissingKeys(source: CredentialSource): string {304const missingKeys: string[] = [];305if (!source.appId) missingKeys.push(source.appIdKey);306if (!source.appSecret) missingKeys.push(source.appSecretKey);307return `${source.name} missing ${missingKeys.join(" and ")}`;308}309310function buildCredentialSource(311name: string,312values: Record<string, string | undefined>,313appIdKey: string,314appSecretKey: string,315): CredentialSource {316return {317name,318appIdKey,319appSecretKey,320appId: normalizeCredentialValue(values[appIdKey]),321appSecret: normalizeCredentialValue(values[appSecretKey]),322};323}324325function resolveCredentialSource(326sources: CredentialSource[],327account?: ResolvedAccount,328): LoadedCredentials {329const skippedSources: string[] = [];330331for (const source of sources) {332if (source.appId && source.appSecret) {333return {334appId: source.appId,335appSecret: source.appSecret,336source: source.name,337skippedSources,338};339}340341if (source.appId || source.appSecret) {342skippedSources.push(describeMissingKeys(source));343}344}345346const hint = account?.alias ? ` (account: ${account.alias})` : "";347const partialHint = skippedSources.length > 0348? `\nIncomplete credential sources skipped:\n- ${skippedSources.join("\n- ")}`349: "";350351throw new Error(352`Missing WECHAT_APP_ID or WECHAT_APP_SECRET${hint}.\n` +353"Set via EXTEND.md account config, environment variables, or .baoyu-skills/.env file." +354partialHint355);356}357358export function loadCredentials(account?: ResolvedAccount): LoadedCredentials {359const cwdEnvPath = path.join(process.cwd(), ".baoyu-skills", ".env");360const homeEnvPath = path.join(homeDir(), ".baoyu-skills", ".env");361const cwdEnv = loadEnvFile(cwdEnvPath);362const homeEnv = loadEnvFile(homeEnvPath);363364const sources: CredentialSource[] = [];365366if (account?.app_id || account?.app_secret) {367sources.push({368name: account.alias ? `EXTEND.md account "${account.alias}"` : "EXTEND.md account config",369appIdKey: "app_id",370appSecretKey: "app_secret",371appId: normalizeCredentialValue(account.app_id),372appSecret: normalizeCredentialValue(account.app_secret),373});374}375376const prefix = account?.alias ? `WECHAT_${aliasToEnvKey(account.alias)}_` : "";377if (prefix) {378const prefixedKeyLabel = `${prefix}APP_ID/${prefix}APP_SECRET`;379sources.push(380buildCredentialSource(`process.env (${prefixedKeyLabel})`, process.env, `${prefix}APP_ID`, `${prefix}APP_SECRET`),381buildCredentialSource(`<cwd>/.baoyu-skills/.env (${prefixedKeyLabel})`, cwdEnv, `${prefix}APP_ID`, `${prefix}APP_SECRET`),382buildCredentialSource(`~/.baoyu-skills/.env (${prefixedKeyLabel})`, homeEnv, `${prefix}APP_ID`, `${prefix}APP_SECRET`),383);384}385386sources.push(387buildCredentialSource("process.env", process.env, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),388buildCredentialSource("<cwd>/.baoyu-skills/.env", cwdEnv, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),389buildCredentialSource("~/.baoyu-skills/.env", homeEnv, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),390);391392return resolveCredentialSource(sources, account);393}394395export function listAccounts(config: WechatExtendConfig): string[] {396return (config.accounts || []).map(a => a.alias);397}398