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 interface WechatAccount {6name: string;7alias: string;8default?: boolean;9default_publish_method?: string;10default_author?: string;11need_open_comment?: number;12only_fans_can_comment?: number;13app_id?: string;14app_secret?: string;15chrome_profile_path?: string;16}1718export interface WechatExtendConfig {19default_theme?: string;20default_color?: string;21default_publish_method?: string;22default_author?: string;23need_open_comment?: number;24only_fans_can_comment?: number;25chrome_profile_path?: string;26accounts?: WechatAccount[];27}2829export interface ResolvedAccount {30name?: string;31alias?: string;32default_publish_method?: string;33default_author?: string;34need_open_comment: number;35only_fans_can_comment: number;36app_id?: string;37app_secret?: string;38chrome_profile_path?: string;39}4041function stripQuotes(s: string): string {42return s.replace(/^['"]|['"]$/g, "");43}4445function toBool01(v: string): number {46return v === "1" || v === "true" ? 1 : 0;47}4849function parseWechatExtend(content: string): WechatExtendConfig {50const config: WechatExtendConfig = {};51const lines = content.split("\n");52let inAccounts = false;53let current: Record<string, string> | null = null;54const rawAccounts: Record<string, string>[] = [];5556for (const raw of lines) {57const trimmed = raw.trim();58if (!trimmed || trimmed.startsWith("#")) continue;5960if (trimmed === "accounts:") {61inAccounts = true;62continue;63}6465if (inAccounts) {66const listMatch = raw.match(/^\s+-\s+(.+)$/);67if (listMatch) {68if (current) rawAccounts.push(current);69current = {};70const kv = listMatch[1]!;71const ci = kv.indexOf(":");72if (ci > 0) {73current[kv.slice(0, ci).trim()] = stripQuotes(kv.slice(ci + 1).trim());74}75continue;76}7778if (current && /^\s{2,}/.test(raw) && !trimmed.startsWith("-")) {79const ci = trimmed.indexOf(":");80if (ci > 0) {81current[trimmed.slice(0, ci).trim()] = stripQuotes(trimmed.slice(ci + 1).trim());82}83continue;84}8586if (!/^\s/.test(raw)) {87if (current) rawAccounts.push(current);88current = null;89inAccounts = false;90} else {91continue;92}93}9495const ci = trimmed.indexOf(":");96if (ci < 0) continue;97const key = trimmed.slice(0, ci).trim();98const val = stripQuotes(trimmed.slice(ci + 1).trim());99if (val === "null" || val === "") continue;100101switch (key) {102case "default_theme": config.default_theme = val; break;103case "default_color": config.default_color = val; break;104case "default_publish_method": config.default_publish_method = val; break;105case "default_author": config.default_author = val; break;106case "need_open_comment": config.need_open_comment = toBool01(val); break;107case "only_fans_can_comment": config.only_fans_can_comment = toBool01(val); break;108case "chrome_profile_path": config.chrome_profile_path = val; break;109}110}111112if (current) rawAccounts.push(current);113114if (rawAccounts.length > 0) {115config.accounts = rawAccounts.map(a => ({116name: a.name || "",117alias: a.alias || "",118default: a.default === "true" || a.default === "1",119default_publish_method: a.default_publish_method || undefined,120default_author: a.default_author || undefined,121need_open_comment: a.need_open_comment ? toBool01(a.need_open_comment) : undefined,122only_fans_can_comment: a.only_fans_can_comment ? toBool01(a.only_fans_can_comment) : undefined,123app_id: a.app_id || undefined,124app_secret: a.app_secret || undefined,125chrome_profile_path: a.chrome_profile_path || undefined,126}));127}128129return config;130}131132export function loadWechatExtendConfig(): WechatExtendConfig {133const paths = [134path.join(process.cwd(), ".baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"),135path.join(136process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),137"baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"138),139path.join(os.homedir(), ".baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"),140];141for (const p of paths) {142try {143const content = fs.readFileSync(p, "utf-8");144return parseWechatExtend(content);145} catch {146continue;147}148}149return {};150}151152function selectAccount(config: WechatExtendConfig, alias?: string): WechatAccount | undefined {153if (!config.accounts || config.accounts.length === 0) return undefined;154if (alias) return config.accounts.find(a => a.alias === alias);155if (config.accounts.length === 1) return config.accounts[0];156return config.accounts.find(a => a.default);157}158159export function resolveAccount(config: WechatExtendConfig, alias?: string): ResolvedAccount {160const acct = selectAccount(config, alias);161return {162name: acct?.name,163alias: acct?.alias,164default_publish_method: acct?.default_publish_method ?? config.default_publish_method,165default_author: acct?.default_author ?? config.default_author,166need_open_comment: acct?.need_open_comment ?? config.need_open_comment ?? 1,167only_fans_can_comment: acct?.only_fans_can_comment ?? config.only_fans_can_comment ?? 0,168app_id: acct?.app_id,169app_secret: acct?.app_secret,170chrome_profile_path: acct?.chrome_profile_path ?? config.chrome_profile_path,171};172}173174function loadEnvFile(envPath: string): Record<string, string> {175const env: Record<string, string> = {};176if (!fs.existsSync(envPath)) return env;177const content = fs.readFileSync(envPath, "utf-8");178for (const line of content.split("\n")) {179const trimmed = line.trim();180if (!trimmed || trimmed.startsWith("#")) continue;181const eqIdx = trimmed.indexOf("=");182if (eqIdx > 0) {183const key = trimmed.slice(0, eqIdx).trim();184let value = trimmed.slice(eqIdx + 1).trim();185if ((value.startsWith('"') && value.endsWith('"')) ||186(value.startsWith("'") && value.endsWith("'"))) {187value = value.slice(1, -1);188}189env[key] = value;190}191}192return env;193}194195function aliasToEnvKey(alias: string): string {196return alias.toUpperCase().replace(/-/g, "_");197}198199interface CredentialSource {200name: string;201appIdKey: string;202appSecretKey: string;203appId?: string;204appSecret?: string;205}206207export interface LoadedCredentials {208appId: string;209appSecret: string;210source: string;211skippedSources: string[];212}213214function normalizeCredentialValue(value?: string): string | undefined {215const trimmed = value?.trim();216return trimmed ? trimmed : undefined;217}218219function describeMissingKeys(source: CredentialSource): string {220const missingKeys: string[] = [];221if (!source.appId) missingKeys.push(source.appIdKey);222if (!source.appSecret) missingKeys.push(source.appSecretKey);223return `${source.name} missing ${missingKeys.join(" and ")}`;224}225226function buildCredentialSource(227name: string,228values: Record<string, string | undefined>,229appIdKey: string,230appSecretKey: string,231): CredentialSource {232return {233name,234appIdKey,235appSecretKey,236appId: normalizeCredentialValue(values[appIdKey]),237appSecret: normalizeCredentialValue(values[appSecretKey]),238};239}240241function resolveCredentialSource(242sources: CredentialSource[],243account?: ResolvedAccount,244): LoadedCredentials {245const skippedSources: string[] = [];246247for (const source of sources) {248if (source.appId && source.appSecret) {249return {250appId: source.appId,251appSecret: source.appSecret,252source: source.name,253skippedSources,254};255}256257if (source.appId || source.appSecret) {258skippedSources.push(describeMissingKeys(source));259}260}261262const hint = account?.alias ? ` (account: ${account.alias})` : "";263const partialHint = skippedSources.length > 0264? `\nIncomplete credential sources skipped:\n- ${skippedSources.join("\n- ")}`265: "";266267throw new Error(268`Missing WECHAT_APP_ID or WECHAT_APP_SECRET${hint}.\n` +269"Set via EXTEND.md account config, environment variables, or .baoyu-skills/.env file." +270partialHint271);272}273274export function loadCredentials(account?: ResolvedAccount): LoadedCredentials {275const cwdEnvPath = path.join(process.cwd(), ".baoyu-skills", ".env");276const homeEnvPath = path.join(os.homedir(), ".baoyu-skills", ".env");277const cwdEnv = loadEnvFile(cwdEnvPath);278const homeEnv = loadEnvFile(homeEnvPath);279280const sources: CredentialSource[] = [];281282if (account?.app_id || account?.app_secret) {283sources.push({284name: account.alias ? `EXTEND.md account "${account.alias}"` : "EXTEND.md account config",285appIdKey: "app_id",286appSecretKey: "app_secret",287appId: normalizeCredentialValue(account.app_id),288appSecret: normalizeCredentialValue(account.app_secret),289});290}291292const prefix = account?.alias ? `WECHAT_${aliasToEnvKey(account.alias)}_` : "";293if (prefix) {294const prefixedKeyLabel = `${prefix}APP_ID/${prefix}APP_SECRET`;295sources.push(296buildCredentialSource(`process.env (${prefixedKeyLabel})`, process.env, `${prefix}APP_ID`, `${prefix}APP_SECRET`),297buildCredentialSource(`<cwd>/.baoyu-skills/.env (${prefixedKeyLabel})`, cwdEnv, `${prefix}APP_ID`, `${prefix}APP_SECRET`),298buildCredentialSource(`~/.baoyu-skills/.env (${prefixedKeyLabel})`, homeEnv, `${prefix}APP_ID`, `${prefix}APP_SECRET`),299);300}301302sources.push(303buildCredentialSource("process.env", process.env, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),304buildCredentialSource("<cwd>/.baoyu-skills/.env", cwdEnv, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),305buildCredentialSource("~/.baoyu-skills/.env", homeEnv, "WECHAT_APP_ID", "WECHAT_APP_SECRET"),306);307308return resolveCredentialSource(sources, account);309}310311export function listAccounts(config: WechatExtendConfig): string[] {312return (config.accounts || []).map(a => a.alias);313}314