Add the ability to specify the original article URL ("阅读原文" / "Read
Original" link) when publishing to WeChat Official Account draft via API.
Changes:
- ArticleOptions interface: add contentSourceUrl optional field
- publishToDraft: conditionally write content_source_url for news articles
- CLI: add --source-url parameter
- Frontmatter: extract sourceUrl / contentSourceUrl / content_source_url
- Priority chain: CLI --source-url → frontmatter → (none)
- SKILL.md: document the new option and draft/add payload rule
The field is only written for article_type=news per WeChat API spec.
content_source_url is optional (max 1KB) per the official draft/add API.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to the previous newspic conversion commit:
- `“` / `”` mapped to `"""` (three straight double-quotes),
producing an unterminated string literal that prevented wechat-api.ts
from parsing. Replaced with the intended curly Unicode characters.
- `‘` / `’` similarly mapped to straight `'`; switched to the
curly singles to match the entity meaning.
- Numeric entities used `String.fromCharCode`, which mangles code points
above 0xFFFF (e.g. emoji like `😀` → empty string). Switched to
`String.fromCodePoint`.
- Added hexadecimal numeric entity support (`😀` etc.); the
previous regex only matched decimal forms.
- Anchored the entity sub-patterns with `^...$` so the callback cannot
accidentally re-match a substring of the captured entity.
The previous fix used a simple regex to strip HTML tags for newspic
content, but it had several issues:
1. HTML entities ( , <, >, &, etc.) were not decoded
2. Block-level tags (</p>, </div>, </h1> etc.) did not produce line breaks
3. Multiple consecutive whitespace characters were not collapsed
This commit introduces a dedicated htmlToPlainText() function that:
- Converts <br> and block-level closing tags to line breaks
- Strips all remaining HTML tags
- Decodes common HTML entities (including numeric entities)
- Collapses consecutive whitespace into single spaces
- Trims whitespace from each line
This ensures the content field for newspic articles is properly
formatted plain text that complies with WeChat API requirements,
preventing error 45166 (invalid content) especially when publishing
with multiple images.
Closes#163
Merges #164
The initial remote-api implementation (3b29f3c) relied on
`https.request({ agent: SocksProxyAgent })` to route token/upload/draft
calls through the SSH tunnel. Bun's `https.request` does not honor
Node's `http.Agent` contract, so the agent was silently bypassed and
requests still originated from the local IP — defeating the entire
IP-allowlist purpose. Two follow-on issues compounded it: tests read
the real `~/.baoyu-skills/.env` because Bun's `os.homedir()` ignores
test-time `process.env.HOME` mutations, and invalid config values were
silently coerced to defaults.
P1 — Bun-portable SOCKS routing:
- Drop `socks-proxy-agent` dependency. Add `socks` direct dep.
- New `wechat-socks-http.ts`: raw TCP via `SocksClient.createConnection`
+ `tls.connect({ socket, servername })` + hand-built HTTP/1.1 (status
line parser, case-insensitive headers, chunked & content-length body
framing). Works identically under Node and Bun because it avoids
`http.Agent` entirely.
- Rewrite `wechat-http.ts` as a fetch-based local client and expose
a `WechatClient = (url, init?) => Promise<WechatHttpResponse>`
functional abstraction.
- `wechat-api.ts`: replace `agent?: http.Agent` with
`client: WechatClient = wechatHttp` on the five HTTP-touching
functions; `withSshTunnel` now yields a `WechatClient`.
- New `wechat-socks-http.test.ts` stands up a real SOCKS5 server
stub + HTTP echo server and asserts `connectionCount === 1`,
proving bytes actually traverse the proxy under both runtimes.
P2 — `HOME` honored under Bun:
- `homeDir()` reads `process.env.HOME` / `USERPROFILE` first, falling
back to `os.homedir()`. `loadWechatExtendConfig` and `loadCredentials`
use it, restoring test isolation.
P3 — Strict config validation:
- Replace lenient `toOptional*` helpers with `parsePort` /
`parsePositiveInt` / `parseStrictHostKeyChecking` that throw with
the key name. `loadWechatExtendConfig` only catches file-read
errors so parse errors surface to the caller. Flip the corresponding
test cases.
Verification:
- `npm test`: 261/261 pass.
- `bun test` in `scripts/`: 39/39 pass.
Co-authored-by: Dame5211 <1079825614@qq.com>
Re-implements PR #156 (remote WeChat API publishing for IP-allowlist
constraints) with a fully local code path: spawn a background `ssh -N -D`
SOCKS5 dynamic forward, wrap a `SocksProxyAgent`, and thread it through
the existing token/upload/draft flow. No Python helper, no SCP, no
remote files, no `AppSecret` ever leaving the local process.
Reusing `uploadImagesInHtml` (which replaces the matched `<img>` fullTag
rather than substring-replacing `src`) naturally avoids the original
PR's `html.replace(src, url)` HTML-replacement bug.
Changes:
- New `wechat-remote-publish.ts`: typed-whitelist SSH args, free-port
allocation, SOCKS readiness polling, signal-handler-backed cleanup.
- New `wechat-http.ts`: minimal `https.request`-backed HTTP helper so a
`SocksProxyAgent` can be passed through (undici `fetch` ignores agent).
- New `wechat-image-loader.ts`: extracts `loadUploadAsset` for reuse.
- `wechat-api.ts`: thread optional `agent?` through token/upload/draft;
add `--remote*` CLI flags; dispatch via `withSshTunnel` when remote
mode is selected by flag or `default_publish_method: remote-api`.
- `wechat-extend-config.ts`: add typed `remote_publish_*` keys with
account-over-global fallback and value validation.
- Docs: SKILL.md, references/multi-account.md, references/config/
first-time-setup.md, README.md, README.zh.md.
- Tests: 17 new node:test cases covering config parsing, SSH arg
whitelisting, free-port allocation, multipart assembly, and HTTP
agent threading.
Co-authored-by: Dame5211 <1079825614@qq.com>
CDNs may serve WebP for URLs with .png extension. Detect real format
from magic bytes and correct content-type/extension before upload.
Also treat .webp as PNG-preferred for transparency handling.
Enable bottom citations by default for markdown articles. Add --no-cite
flag to all posting scripts (wechat-api, wechat-article, md-to-wechat)
to optionally disable. WeChat mp links stay inline.
- Add ArticleOptions interface with news/newspic types
- Track all image media IDs for newspic format
- Restructure publishToDraft for dual article type handling
- Add wechat-api.ts for direct API-based article publishing
- Support plain text, HTML, and markdown inputs with auto-conversion
- Add external theme discovery from MD_THEME_DIR environment variable
- Improve CSS inlining with juice library integration
- Support WeChat API credentials via .baoyu-skills/.env