feat(baoyu-post-to-wechat): add multi-account support
Support multiple WeChat Official Accounts via EXTEND.md accounts block. Each account gets isolated Chrome profile, credential resolution chain (inline → prefixed env → unprefixed env), and --account CLI arg.
This commit is contained in:
parent
c0941f8089
commit
5276fae6bd
41
README.md
41
README.md
|
|
@ -581,6 +581,47 @@ To obtain credentials:
|
|||
|
||||
**Browser Method** (no API setup needed): Requires Google Chrome. First run opens browser for QR code login (session preserved).
|
||||
|
||||
**Multi-Account Support**: Manage multiple WeChat Official Accounts via `EXTEND.md`:
|
||||
|
||||
```bash
|
||||
mkdir -p .baoyu-skills/baoyu-post-to-wechat
|
||||
```
|
||||
|
||||
Create `.baoyu-skills/baoyu-post-to-wechat/EXTEND.md`:
|
||||
|
||||
```yaml
|
||||
# Global settings (shared across all accounts)
|
||||
default_theme: default
|
||||
default_color: blue
|
||||
|
||||
# Account list
|
||||
accounts:
|
||||
- name: My Tech Blog
|
||||
alias: tech-blog
|
||||
default: false
|
||||
default_publish_method: api
|
||||
default_author: Author Name
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
app_id: wx1234567890abcdef
|
||||
app_secret: your_app_secret_here
|
||||
- name: AI Newsletter
|
||||
alias: ai-news
|
||||
default_publish_method: browser
|
||||
default_author: AI Newsletter
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
```
|
||||
|
||||
| Accounts configured | Behavior |
|
||||
|---------------------|----------|
|
||||
| No `accounts` block | Single-account mode (backward compatible) |
|
||||
| 1 account | Auto-select, no prompt |
|
||||
| 2+ accounts | Prompt to select, or use `--account <alias>` |
|
||||
| 1 account has `default: true` | Pre-selected as default |
|
||||
|
||||
Each account gets an isolated Chrome profile for independent login sessions (browser method). API credentials can be set inline in EXTEND.md or via `.env` with alias-prefixed keys (e.g., `WECHAT_TECH_BLOG_APP_ID`).
|
||||
|
||||
#### baoyu-post-to-weibo
|
||||
|
||||
Post content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input. Uses real Chrome with CDP to bypass anti-automation.
|
||||
|
|
|
|||
41
README.zh.md
41
README.zh.md
|
|
@ -581,6 +581,47 @@ WECHAT_APP_SECRET=你的AppSecret
|
|||
|
||||
**浏览器方式**(无需 API 配置):需已安装 Google Chrome,首次运行需扫码登录(登录状态会保存)
|
||||
|
||||
**多账号支持**:通过 `EXTEND.md` 管理多个微信公众号:
|
||||
|
||||
```bash
|
||||
mkdir -p .baoyu-skills/baoyu-post-to-wechat
|
||||
```
|
||||
|
||||
创建 `.baoyu-skills/baoyu-post-to-wechat/EXTEND.md`:
|
||||
|
||||
```yaml
|
||||
# 全局设置(所有账号共享)
|
||||
default_theme: default
|
||||
default_color: blue
|
||||
|
||||
# 账号列表
|
||||
accounts:
|
||||
- name: 宝玉的技术分享
|
||||
alias: baoyu
|
||||
default: false
|
||||
default_publish_method: api
|
||||
default_author: 宝玉
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
app_id: wx1234567890abcdef
|
||||
app_secret: 你的 AppSecret
|
||||
- name: AI 工具集
|
||||
alias: ai-tools
|
||||
default_publish_method: browser
|
||||
default_author: AI 工具集
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
```
|
||||
|
||||
| 账号配置情况 | 行为 |
|
||||
|-------------|------|
|
||||
| 无 `accounts` 块 | 单账号模式(向后兼容) |
|
||||
| 1 个账号 | 自动选择,无需提示 |
|
||||
| 2+ 个账号 | 提示选择,或使用 `--account <别名>` |
|
||||
| 某账号设置 `default: true` | 预选为默认账号 |
|
||||
|
||||
每个账号拥有独立的 Chrome 配置目录,保证浏览器方式下的登录会话互不干扰。API 凭证可在 EXTEND.md 中直接配置,也可通过 `.env` 文件使用别名前缀的环境变量(如 `WECHAT_BAOYU_APP_ID`)。
|
||||
|
||||
#### baoyu-post-to-weibo
|
||||
|
||||
发布内容到微博。支持文字、图片、视频发布和头条文章(长篇 Markdown)。使用真实 Chrome + CDP 绕过反自动化检测。
|
||||
|
|
|
|||
|
|
@ -95,9 +95,115 @@ chrome_profile_path: /path/to/chrome/profile
|
|||
**Value priority**:
|
||||
1. CLI arguments
|
||||
2. Frontmatter
|
||||
3. EXTEND.md
|
||||
3. EXTEND.md (account-level → global-level)
|
||||
4. Skill defaults
|
||||
|
||||
## Multi-Account Support
|
||||
|
||||
EXTEND.md supports managing multiple WeChat Official Accounts. When `accounts:` block is present, each account can have its own credentials, Chrome profile, and default settings.
|
||||
|
||||
**Compatibility rules**:
|
||||
|
||||
| Condition | Mode | Behavior |
|
||||
|-----------|------|----------|
|
||||
| No `accounts` block | Single-account | Current behavior, unchanged |
|
||||
| `accounts` with 1 entry | Single-account | Auto-select, no prompt |
|
||||
| `accounts` with 2+ entries | Multi-account | Prompt to select before publishing |
|
||||
| `accounts` with `default: true` | Multi-account | Pre-select default, user can switch |
|
||||
|
||||
**Multi-account EXTEND.md example**:
|
||||
|
||||
```md
|
||||
default_theme: default
|
||||
default_color: blue
|
||||
|
||||
accounts:
|
||||
- name: 宝玉的技术分享
|
||||
alias: baoyu
|
||||
default: true
|
||||
default_publish_method: api
|
||||
default_author: 宝玉
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
app_id: wx1234567890abcdef
|
||||
app_secret: abc123secret456
|
||||
- name: AI工具集
|
||||
alias: ai-tools
|
||||
default_publish_method: browser
|
||||
default_author: AI工具集
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
```
|
||||
|
||||
**Per-account keys** (can be set per-account or globally as fallback):
|
||||
`default_publish_method`, `default_author`, `need_open_comment`, `only_fans_can_comment`, `app_id`, `app_secret`, `chrome_profile_path`
|
||||
|
||||
**Global-only keys** (always shared across accounts):
|
||||
`default_theme`, `default_color`
|
||||
|
||||
### Account Selection (Step 0.5)
|
||||
|
||||
Insert between Step 0 and Step 1 in the Article Posting Workflow:
|
||||
|
||||
```
|
||||
if no accounts block:
|
||||
→ single-account mode (current behavior)
|
||||
elif accounts.length == 1:
|
||||
→ auto-select the only account
|
||||
elif --account <alias> CLI arg:
|
||||
→ select matching account
|
||||
elif one account has default: true:
|
||||
→ pre-select, show: "Using account: <name> (--account to switch)"
|
||||
else:
|
||||
→ prompt user:
|
||||
"Multiple WeChat accounts configured:
|
||||
1) <name1> (<alias1>)
|
||||
2) <name2> (<alias2>)
|
||||
Select account [1-N]:"
|
||||
```
|
||||
|
||||
### Credential Resolution (API Method)
|
||||
|
||||
For a selected account with alias `{alias}`:
|
||||
|
||||
1. `app_id` / `app_secret` inline in EXTEND.md account block
|
||||
2. Env var `WECHAT_{ALIAS}_APP_ID` / `WECHAT_{ALIAS}_APP_SECRET` (alias uppercased, hyphens → underscores)
|
||||
3. `.baoyu-skills/.env` with prefixed key `WECHAT_{ALIAS}_APP_ID`
|
||||
4. `~/.baoyu-skills/.env` with prefixed key
|
||||
5. Fallback to unprefixed `WECHAT_APP_ID` / `WECHAT_APP_SECRET`
|
||||
|
||||
**.env multi-account example**:
|
||||
|
||||
```bash
|
||||
# Account: baoyu
|
||||
WECHAT_BAOYU_APP_ID=wx1234567890abcdef
|
||||
WECHAT_BAOYU_APP_SECRET=abc123secret456
|
||||
|
||||
# Account: ai-tools
|
||||
WECHAT_AI_TOOLS_APP_ID=wxabcdef1234567890
|
||||
WECHAT_AI_TOOLS_APP_SECRET=def789secret012
|
||||
```
|
||||
|
||||
### Chrome Profile (Browser Method)
|
||||
|
||||
Each account uses an isolated Chrome profile for independent login sessions:
|
||||
|
||||
| Source | Path |
|
||||
|--------|------|
|
||||
| Account `chrome_profile_path` in EXTEND.md | Use as-is |
|
||||
| Auto-generated from alias | `{shared_profile_parent}/wechat-{alias}/` |
|
||||
| Single-account fallback | Shared default profile (current behavior) |
|
||||
|
||||
### CLI `--account` Argument
|
||||
|
||||
All publishing scripts accept `--account <alias>`:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/wechat-api.ts <file> --theme default --account ai-tools
|
||||
${BUN_X} {baseDir}/scripts/wechat-article.ts --markdown <file> --theme default --account baoyu
|
||||
${BUN_X} {baseDir}/scripts/wechat-browser.ts --markdown <file> --images ./photos/ --account baoyu
|
||||
```
|
||||
|
||||
## Pre-flight Check (Optional)
|
||||
|
||||
Before first use, suggest running the environment check. User can skip if they prefer.
|
||||
|
|
@ -139,6 +245,7 @@ Copy this checklist and check off items as you complete them:
|
|||
```
|
||||
Publishing Progress:
|
||||
- [ ] Step 0: Load preferences (EXTEND.md)
|
||||
- [ ] Step 0.5: Resolve account (multi-account only)
|
||||
- [ ] Step 1: Determine input type
|
||||
- [ ] Step 2: Select method and configure credentials
|
||||
- [ ] Step 3: Resolve theme/color and validate metadata
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ options:
|
|||
|
||||
## EXTEND.md Template
|
||||
|
||||
### Single Account (Default)
|
||||
|
||||
```md
|
||||
default_theme: [default/grace/simple/modern]
|
||||
default_color: [preset name, hex, or empty for theme default]
|
||||
|
|
@ -162,6 +164,40 @@ only_fans_can_comment: [1/0]
|
|||
chrome_profile_path:
|
||||
```
|
||||
|
||||
### Multi-Account
|
||||
|
||||
```md
|
||||
default_theme: [default/grace/simple/modern]
|
||||
default_color: [preset name, hex, or empty for theme default]
|
||||
|
||||
accounts:
|
||||
- name: [display name]
|
||||
alias: [short key, e.g. "baoyu"]
|
||||
default: true
|
||||
default_publish_method: [api/browser]
|
||||
default_author: [author name]
|
||||
need_open_comment: [1/0]
|
||||
only_fans_can_comment: [1/0]
|
||||
app_id: [WeChat App ID, optional]
|
||||
app_secret: [WeChat App Secret, optional]
|
||||
- name: [second account name]
|
||||
alias: [short key, e.g. "ai-tools"]
|
||||
default_publish_method: [api/browser]
|
||||
default_author: [author name]
|
||||
need_open_comment: [1/0]
|
||||
only_fans_can_comment: [1/0]
|
||||
```
|
||||
|
||||
## Adding More Accounts Later
|
||||
|
||||
After initial setup, users can add accounts by editing EXTEND.md:
|
||||
|
||||
1. Add an `accounts:` block with list items
|
||||
2. Move per-account settings (author, publish method, comments) into each account entry
|
||||
3. Keep global settings (theme, color) at the top level
|
||||
4. Each account needs a unique `alias` (used for CLI `--account` arg and Chrome profile naming)
|
||||
5. Set `default: true` on the primary account
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or delete it to trigger setup again.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { execSync, type ChildProcess } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import {
|
||||
|
|
@ -80,6 +81,11 @@ export function getDefaultProfileDir(): string {
|
|||
});
|
||||
}
|
||||
|
||||
export function getAccountProfileDir(alias: string): string {
|
||||
const base = getDefaultProfileDir();
|
||||
return path.join(path.dirname(base), `wechat-${alias}`);
|
||||
}
|
||||
|
||||
export interface ChromeSession {
|
||||
cdp: CdpConnection;
|
||||
sessionId: string;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
interface WechatConfig {
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
}
|
||||
import { loadWechatExtendConfig, resolveAccount, loadCredentials } from "./wechat-extend-config.ts";
|
||||
|
||||
interface AccessTokenResponse {
|
||||
access_token?: string;
|
||||
|
|
@ -38,53 +33,14 @@ interface ArticleOptions {
|
|||
thumbMediaId: string;
|
||||
articleType: ArticleType;
|
||||
imageMediaIds?: string[];
|
||||
needOpenComment?: number;
|
||||
onlyFansCanComment?: number;
|
||||
}
|
||||
|
||||
const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
||||
const UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";
|
||||
const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";
|
||||
|
||||
function loadEnvFile(envPath: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (!fs.existsSync(envPath)) return env;
|
||||
|
||||
const content = fs.readFileSync(envPath, "utf-8");
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eqIdx = trimmed.indexOf("=");
|
||||
if (eqIdx > 0) {
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function loadConfig(): WechatConfig {
|
||||
const cwdEnvPath = path.join(process.cwd(), ".baoyu-skills", ".env");
|
||||
const homeEnvPath = path.join(os.homedir(), ".baoyu-skills", ".env");
|
||||
|
||||
const cwdEnv = loadEnvFile(cwdEnvPath);
|
||||
const homeEnv = loadEnvFile(homeEnvPath);
|
||||
|
||||
const appId = process.env.WECHAT_APP_ID || cwdEnv.WECHAT_APP_ID || homeEnv.WECHAT_APP_ID;
|
||||
const appSecret = process.env.WECHAT_APP_SECRET || cwdEnv.WECHAT_APP_SECRET || homeEnv.WECHAT_APP_SECRET;
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
throw new Error(
|
||||
"Missing WECHAT_APP_ID or WECHAT_APP_SECRET.\n" +
|
||||
"Set via environment variables or in .baoyu-skills/.env file."
|
||||
);
|
||||
}
|
||||
|
||||
return { appId, appSecret };
|
||||
}
|
||||
|
||||
async function fetchAccessToken(appId: string, appSecret: string): Promise<string> {
|
||||
const url = `${TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
||||
|
|
@ -241,6 +197,9 @@ async function publishToDraft(
|
|||
|
||||
let article: Record<string, unknown>;
|
||||
|
||||
const noc = options.needOpenComment ?? 1;
|
||||
const ofcc = options.onlyFansCanComment ?? 0;
|
||||
|
||||
if (options.articleType === "newspic") {
|
||||
if (!options.imageMediaIds || options.imageMediaIds.length === 0) {
|
||||
throw new Error("newspic requires at least one image");
|
||||
|
|
@ -249,8 +208,8 @@ async function publishToDraft(
|
|||
article_type: "newspic",
|
||||
title: options.title,
|
||||
content: options.content,
|
||||
need_open_comment: 1,
|
||||
only_fans_can_comment: 0,
|
||||
need_open_comment: noc,
|
||||
only_fans_can_comment: ofcc,
|
||||
image_info: {
|
||||
image_list: options.imageMediaIds.map(id => ({ image_media_id: id })),
|
||||
},
|
||||
|
|
@ -262,8 +221,8 @@ async function publishToDraft(
|
|||
title: options.title,
|
||||
content: options.content,
|
||||
thumb_media_id: options.thumbMediaId,
|
||||
need_open_comment: 1,
|
||||
only_fans_can_comment: 0,
|
||||
need_open_comment: noc,
|
||||
only_fans_can_comment: ofcc,
|
||||
};
|
||||
if (options.author) article.author = options.author;
|
||||
if (options.digest) article.digest = options.digest;
|
||||
|
|
@ -368,6 +327,7 @@ Options:
|
|||
--theme <name> Theme name for markdown (default, grace, simple, modern). Default: default
|
||||
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
|
||||
--cover <path> Cover image path (local or URL)
|
||||
--account <alias> Select account by alias (for multi-account setups)
|
||||
--no-cite Disable bottom citations for ordinary external links in markdown mode
|
||||
--dry-run Parse and render only, don't publish
|
||||
--help Show this help
|
||||
|
|
@ -412,6 +372,7 @@ interface CliArgs {
|
|||
theme: string;
|
||||
color?: string;
|
||||
cover?: string;
|
||||
account?: string;
|
||||
citeStatus: boolean;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
|
@ -449,6 +410,8 @@ function parseArgs(argv: string[]): CliArgs {
|
|||
args.color = argv[++i];
|
||||
} else if (arg === "--cover" && argv[i + 1]) {
|
||||
args.cover = argv[++i];
|
||||
} else if (arg === "--account" && argv[i + 1]) {
|
||||
args.account = argv[++i];
|
||||
} else if (arg === "--cite") {
|
||||
args.citeStatus = true;
|
||||
} else if (arg === "--no-cite") {
|
||||
|
|
@ -550,6 +513,12 @@ async function main(): Promise<void> {
|
|||
if (digest) console.error(`[wechat-api] Digest: ${digest.slice(0, 50)}...`);
|
||||
console.error(`[wechat-api] Type: ${args.articleType}`);
|
||||
|
||||
const extConfig = loadWechatExtendConfig();
|
||||
const resolved = resolveAccount(extConfig, args.account);
|
||||
if (resolved.name) console.error(`[wechat-api] Account: ${resolved.name} (${resolved.alias})`);
|
||||
|
||||
if (!author && resolved.default_author) author = resolved.default_author;
|
||||
|
||||
if (args.dryRun) {
|
||||
console.log(JSON.stringify({
|
||||
articleType: args.articleType,
|
||||
|
|
@ -558,13 +527,14 @@ async function main(): Promise<void> {
|
|||
digest: digest || undefined,
|
||||
htmlPath,
|
||||
contentLength: htmlContent.length,
|
||||
account: resolved.alias || undefined,
|
||||
}, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const creds = loadCredentials(resolved);
|
||||
console.error("[wechat-api] Fetching access token...");
|
||||
const accessToken = await fetchAccessToken(config.appId, config.appSecret);
|
||||
const accessToken = await fetchAccessToken(creds.appId, creds.appSecret);
|
||||
|
||||
console.error("[wechat-api] Uploading images...");
|
||||
const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml(
|
||||
|
|
@ -617,6 +587,8 @@ async function main(): Promise<void> {
|
|||
thumbMediaId,
|
||||
articleType: args.articleType,
|
||||
imageMediaIds: args.articleType === "newspic" ? allMediaIds : undefined,
|
||||
needOpenComment: resolved.need_open_comment,
|
||||
onlyFansCanComment: resolved.only_fans_can_comment,
|
||||
}, accessToken);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import path from 'node:path';
|
|||
import { spawnSync } from 'node:child_process';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts';
|
||||
import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, getAccountProfileDir, type ChromeSession, type CdpConnection } from './cdp.ts';
|
||||
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
||||
|
||||
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
||||
|
||||
|
|
@ -690,6 +691,7 @@ Options:
|
|||
--image <path> Content image, can repeat (only with --content)
|
||||
--submit Save as draft
|
||||
--profile <dir> Chrome profile directory
|
||||
--account <alias> Select account by alias (for multi-account setups)
|
||||
--cdp-port <port> Connect to existing Chrome debug port instead of launching new instance
|
||||
|
||||
Examples:
|
||||
|
|
@ -725,6 +727,7 @@ async function main(): Promise<void> {
|
|||
let submit = false;
|
||||
let profileDir: string | undefined;
|
||||
let cdpPort: number | undefined;
|
||||
let accountAlias: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
|
|
@ -741,9 +744,20 @@ async function main(): Promise<void> {
|
|||
else if (arg === '--image' && args[i + 1]) images.push(args[++i]!);
|
||||
else if (arg === '--submit') submit = true;
|
||||
else if (arg === '--profile' && args[i + 1]) profileDir = args[++i];
|
||||
else if (arg === '--account' && args[i + 1]) accountAlias = args[++i];
|
||||
else if (arg === '--cdp-port' && args[i + 1]) cdpPort = parseInt(args[++i]!, 10);
|
||||
}
|
||||
|
||||
const extConfig = loadWechatExtendConfig();
|
||||
const resolved = resolveAccount(extConfig, accountAlias);
|
||||
if (resolved.name) console.log(`[wechat] Account: ${resolved.name} (${resolved.alias})`);
|
||||
|
||||
if (!author && resolved.default_author) author = resolved.default_author;
|
||||
|
||||
if (!profileDir && resolved.alias) {
|
||||
profileDir = resolved.chrome_profile_path || getAccountProfileDir(resolved.alias);
|
||||
}
|
||||
|
||||
if (!markdownFile && !htmlFile && !title) { console.error('Error: --title is required (or use --markdown/--html)'); process.exit(1); }
|
||||
if (!markdownFile && !htmlFile && !content) { console.error('Error: --content, --html, or --markdown is required'); process.exit(1); }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import {
|
|||
CdpConnection,
|
||||
findChromeExecutable,
|
||||
getDefaultProfileDir,
|
||||
getAccountProfileDir,
|
||||
launchChrome,
|
||||
sleep,
|
||||
} from './cdp.ts';
|
||||
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
||||
|
||||
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
||||
|
||||
|
|
@ -664,6 +666,7 @@ Options:
|
|||
--image <path> Add image (can be repeated)
|
||||
--submit Save as draft (default: preview only)
|
||||
--profile <dir> Chrome profile directory
|
||||
--account <alias> Select account by alias (for multi-account setups)
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
|
|
@ -685,6 +688,7 @@ async function main(): Promise<void> {
|
|||
let content: string | undefined;
|
||||
let markdownFile: string | undefined;
|
||||
let imagesDir: string | undefined;
|
||||
let accountAlias: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
|
|
@ -702,9 +706,19 @@ async function main(): Promise<void> {
|
|||
submit = true;
|
||||
} else if (arg === '--profile' && args[i + 1]) {
|
||||
profileDir = args[++i];
|
||||
} else if (arg === '--account' && args[i + 1]) {
|
||||
accountAlias = args[++i];
|
||||
}
|
||||
}
|
||||
|
||||
const extConfig = loadWechatExtendConfig();
|
||||
const resolved = resolveAccount(extConfig, accountAlias);
|
||||
if (resolved.name) console.log(`[wechat-browser] Account: ${resolved.name} (${resolved.alias})`);
|
||||
|
||||
if (!profileDir && resolved.alias) {
|
||||
profileDir = resolved.chrome_profile_path || getAccountProfileDir(resolved.alias);
|
||||
}
|
||||
|
||||
if (!markdownFile && !title) {
|
||||
console.error('Error: --title or --markdown is required');
|
||||
process.exit(1);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
|
||||
export interface WechatAccount {
|
||||
name: string;
|
||||
alias: string;
|
||||
default?: boolean;
|
||||
default_publish_method?: string;
|
||||
default_author?: string;
|
||||
need_open_comment?: number;
|
||||
only_fans_can_comment?: number;
|
||||
app_id?: string;
|
||||
app_secret?: string;
|
||||
chrome_profile_path?: string;
|
||||
}
|
||||
|
||||
export interface WechatExtendConfig {
|
||||
default_theme?: string;
|
||||
default_color?: string;
|
||||
default_publish_method?: string;
|
||||
default_author?: string;
|
||||
need_open_comment?: number;
|
||||
only_fans_can_comment?: number;
|
||||
chrome_profile_path?: string;
|
||||
accounts?: WechatAccount[];
|
||||
}
|
||||
|
||||
export interface ResolvedAccount {
|
||||
name?: string;
|
||||
alias?: string;
|
||||
default_publish_method?: string;
|
||||
default_author?: string;
|
||||
need_open_comment: number;
|
||||
only_fans_can_comment: number;
|
||||
app_id?: string;
|
||||
app_secret?: string;
|
||||
chrome_profile_path?: string;
|
||||
}
|
||||
|
||||
function stripQuotes(s: string): string {
|
||||
return s.replace(/^['"]|['"]$/g, "");
|
||||
}
|
||||
|
||||
function toBool01(v: string): number {
|
||||
return v === "1" || v === "true" ? 1 : 0;
|
||||
}
|
||||
|
||||
function parseWechatExtend(content: string): WechatExtendConfig {
|
||||
const config: WechatExtendConfig = {};
|
||||
const lines = content.split("\n");
|
||||
let inAccounts = false;
|
||||
let current: Record<string, string> | null = null;
|
||||
const rawAccounts: Record<string, string>[] = [];
|
||||
|
||||
for (const raw of lines) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
if (trimmed === "accounts:") {
|
||||
inAccounts = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inAccounts) {
|
||||
const listMatch = raw.match(/^\s+-\s+(.+)$/);
|
||||
if (listMatch) {
|
||||
if (current) rawAccounts.push(current);
|
||||
current = {};
|
||||
const kv = listMatch[1]!;
|
||||
const ci = kv.indexOf(":");
|
||||
if (ci > 0) {
|
||||
current[kv.slice(0, ci).trim()] = stripQuotes(kv.slice(ci + 1).trim());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current && /^\s{2,}/.test(raw) && !trimmed.startsWith("-")) {
|
||||
const ci = trimmed.indexOf(":");
|
||||
if (ci > 0) {
|
||||
current[trimmed.slice(0, ci).trim()] = stripQuotes(trimmed.slice(ci + 1).trim());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!/^\s/.test(raw)) {
|
||||
if (current) rawAccounts.push(current);
|
||||
current = null;
|
||||
inAccounts = false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const ci = trimmed.indexOf(":");
|
||||
if (ci < 0) continue;
|
||||
const key = trimmed.slice(0, ci).trim();
|
||||
const val = stripQuotes(trimmed.slice(ci + 1).trim());
|
||||
if (val === "null" || val === "") continue;
|
||||
|
||||
switch (key) {
|
||||
case "default_theme": config.default_theme = val; break;
|
||||
case "default_color": config.default_color = val; break;
|
||||
case "default_publish_method": config.default_publish_method = val; break;
|
||||
case "default_author": config.default_author = val; break;
|
||||
case "need_open_comment": config.need_open_comment = toBool01(val); break;
|
||||
case "only_fans_can_comment": config.only_fans_can_comment = toBool01(val); break;
|
||||
case "chrome_profile_path": config.chrome_profile_path = val; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (current) rawAccounts.push(current);
|
||||
|
||||
if (rawAccounts.length > 0) {
|
||||
config.accounts = rawAccounts.map(a => ({
|
||||
name: a.name || "",
|
||||
alias: a.alias || "",
|
||||
default: a.default === "true" || a.default === "1",
|
||||
default_publish_method: a.default_publish_method || undefined,
|
||||
default_author: a.default_author || undefined,
|
||||
need_open_comment: a.need_open_comment ? toBool01(a.need_open_comment) : undefined,
|
||||
only_fans_can_comment: a.only_fans_can_comment ? toBool01(a.only_fans_can_comment) : undefined,
|
||||
app_id: a.app_id || undefined,
|
||||
app_secret: a.app_secret || undefined,
|
||||
chrome_profile_path: a.chrome_profile_path || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function loadWechatExtendConfig(): WechatExtendConfig {
|
||||
const paths = [
|
||||
path.join(process.cwd(), ".baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"),
|
||||
path.join(
|
||||
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
|
||||
"baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"
|
||||
),
|
||||
path.join(os.homedir(), ".baoyu-skills", "baoyu-post-to-wechat", "EXTEND.md"),
|
||||
];
|
||||
for (const p of paths) {
|
||||
try {
|
||||
const content = fs.readFileSync(p, "utf-8");
|
||||
return parseWechatExtend(content);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function selectAccount(config: WechatExtendConfig, alias?: string): WechatAccount | undefined {
|
||||
if (!config.accounts || config.accounts.length === 0) return undefined;
|
||||
if (alias) return config.accounts.find(a => a.alias === alias);
|
||||
if (config.accounts.length === 1) return config.accounts[0];
|
||||
return config.accounts.find(a => a.default);
|
||||
}
|
||||
|
||||
export function resolveAccount(config: WechatExtendConfig, alias?: string): ResolvedAccount {
|
||||
const acct = selectAccount(config, alias);
|
||||
return {
|
||||
name: acct?.name,
|
||||
alias: acct?.alias,
|
||||
default_publish_method: acct?.default_publish_method ?? config.default_publish_method,
|
||||
default_author: acct?.default_author ?? config.default_author,
|
||||
need_open_comment: acct?.need_open_comment ?? config.need_open_comment ?? 1,
|
||||
only_fans_can_comment: acct?.only_fans_can_comment ?? config.only_fans_can_comment ?? 0,
|
||||
app_id: acct?.app_id,
|
||||
app_secret: acct?.app_secret,
|
||||
chrome_profile_path: acct?.chrome_profile_path ?? config.chrome_profile_path,
|
||||
};
|
||||
}
|
||||
|
||||
function loadEnvFile(envPath: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (!fs.existsSync(envPath)) return env;
|
||||
const content = fs.readFileSync(envPath, "utf-8");
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eqIdx = trimmed.indexOf("=");
|
||||
if (eqIdx > 0) {
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
function aliasToEnvKey(alias: string): string {
|
||||
return alias.toUpperCase().replace(/-/g, "_");
|
||||
}
|
||||
|
||||
export function loadCredentials(account?: ResolvedAccount): { appId: string; appSecret: string } {
|
||||
if (account?.app_id && account?.app_secret) {
|
||||
return { appId: account.app_id, appSecret: account.app_secret };
|
||||
}
|
||||
|
||||
const cwdEnvPath = path.join(process.cwd(), ".baoyu-skills", ".env");
|
||||
const homeEnvPath = path.join(os.homedir(), ".baoyu-skills", ".env");
|
||||
const cwdEnv = loadEnvFile(cwdEnvPath);
|
||||
const homeEnv = loadEnvFile(homeEnvPath);
|
||||
|
||||
const prefix = account?.alias ? `WECHAT_${aliasToEnvKey(account.alias)}_` : "";
|
||||
|
||||
let appId = "";
|
||||
let appSecret = "";
|
||||
|
||||
if (prefix) {
|
||||
appId = process.env[`${prefix}APP_ID`]
|
||||
|| cwdEnv[`${prefix}APP_ID`]
|
||||
|| homeEnv[`${prefix}APP_ID`]
|
||||
|| "";
|
||||
appSecret = process.env[`${prefix}APP_SECRET`]
|
||||
|| cwdEnv[`${prefix}APP_SECRET`]
|
||||
|| homeEnv[`${prefix}APP_SECRET`]
|
||||
|| "";
|
||||
}
|
||||
|
||||
if (!appId) {
|
||||
appId = process.env.WECHAT_APP_ID || cwdEnv.WECHAT_APP_ID || homeEnv.WECHAT_APP_ID || "";
|
||||
}
|
||||
if (!appSecret) {
|
||||
appSecret = process.env.WECHAT_APP_SECRET || cwdEnv.WECHAT_APP_SECRET || homeEnv.WECHAT_APP_SECRET || "";
|
||||
}
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
const hint = account?.alias ? ` (account: ${account.alias})` : "";
|
||||
throw new Error(
|
||||
`Missing WECHAT_APP_ID or WECHAT_APP_SECRET${hint}.\n` +
|
||||
"Set via EXTEND.md account config, environment variables, or .baoyu-skills/.env file."
|
||||
);
|
||||
}
|
||||
|
||||
return { appId, appSecret };
|
||||
}
|
||||
|
||||
export function listAccounts(config: WechatExtendConfig): string[] {
|
||||
return (config.accounts || []).map(a => a.alias);
|
||||
}
|
||||
Loading…
Reference in New Issue