* fix(baoyu-post-to-x): render CJK-adjacent bold/italics in articles
md-to-html.ts preprocessed markdown with remark + remark-cjk-friendly, then re-stringified and re-parsed with marked. remark-cjk-friendly parses emphasis adjacent to CJK correctly, but remark-stringify re-emits the **bold*/*italic* markers, which marked then FAILS to parse: its flanking test does not treat a CJK character after the closing delimiter as valid punctuation/whitespace. Result: literal **...** asterisks leak into the pasted X Article body whenever the closing delimiter is directly followed by a CJK character.
Fix: convert strong/emphasis mdast nodes (already identified correctly by remark-cjk-friendly) into raw inline <strong>/<em> HTML before stringify, so marked passes them through untouched. A small recursive serializer preserves nested links, inline code, images, etc.
- add remarkStrongEmToHtml remark plugin + inline mdast serializer
- add unist-util-visit as an explicit dependency
- add regression test covering CJK-adjacent bold and italics
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(baoyu-post-to-x): 修复紧贴中文的加粗/斜体在 X Articles 渲染为字面 ** 的问题
按维护者建议改用 remark-cjk-friendly 标准用法,替代自定义插件/序列化器:
- 删除 remarkStrongEmToHtml 插件与 serializeMdastNode 序列化器
- preprocessCjkMarkdown 简化为 remarkParse + remarkCjkFriendly + remarkStringify 往返
- 移除原 markdown 阶段的 &#x...; 实体解码(该解码会还原 cjk-friendly 为
骗过 marked flanking 判定而生成的实体,正是 CJK 加粗失败的根因)
- 改在 marked 渲染出最终 HTML 后再 decodeHtmlEntities,此时 markdown 语法
已不存在,解码安全且保持输出干净
- 移除不再使用的 unist-util-visit 显式依赖
- 测试补充引用式链接 **[docs][d]** 在强调内的回归断言(顺带修复 Codex
指出的 linkReference 渲染为纯文本的 P2 问题)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(baoyu-post-to-x): 移除渲染后无差别实体解码,避免篡改作者字面实体
回应 Codex review (P2):decodeHtmlEntities 对整个渲染 HTML 做无差别
&#x...; 解码,会把作者为显示字面 HTML 而写的实体(如 <b>)
解码成真实标签,存在内容篡改与 HTML 注入风险。
该解码本就只是为了让输出 HTML「好看」,而 remark-cjk-friendly 引入的
实体是合法 HTML 字符引用,粘进 X 编辑器时能正确渲染;且这些实体只会
出现在强调 span 的外侧(边界字符),不会出现在 span 内部。因此直接
移除解码,既消除过度解码风险,又无功能损失。
- 删除 decodeHtmlEntities 及其在 convertMarkdownToHtml 的调用
- 新增测试:作者字面实体 <b> 不被解码为真实 <b>,CJK 加粗
仍正常渲染
- bun test 8 个用例全部通过;真实文章端到端验证 17 处加粗、0 字面 **、
0 真实标签注入
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: ruofeng <ruofeng.wang@rd.group>
Co-authored-by: Claude <noreply@anthropic.com>
Gemini 3 stopped emitting the legacy `image_generation_content` marker in
the candidate text and moved the generated image into a later response
part. The `wants_generated` gate only checked the candidate text / a single
structured field, so on Gemini 3 it was never true and the image was
silently dropped: generate_content returned an empty result even though the
gg-dl image URL was present in the raw response.
- Broaden `wants_generated` to also fire when the raw response contains a
`gg-dl/` generated-image URL or an `image_generation_content` marker.
- When a candidate has no image part, skip extraction instead of throwing
ImageGenerationError, so multi-candidate responses don't fail wholesale.
Verified against a live Gemini 3 account: image generation returns the
generated image again with the default gemini-3-pro model.
Stop Codex from satisfying a request by copying an unrelated pre-existing
image from generated_images instead of generating a new one. Verification
now requires a PNG in this thread's generated_images dir (or an image_gen
stream item, kept as a forward-compatible signal), and the spawned-agent
instruction forbids reading or reusing history images before image_gen is
called. Removes the findCpToTarget fallback that allowed the shortcut.
Validated against a real codex exec run: image_gen leaves no stream item,
so the filesystem check is the load-bearing signal; locked in via a
condensed real-stream regression fixture.
Closes#185
On Windows, Bun throws EEXIST for mkdir(dir, { recursive: true }) when the
directory already exists, contradicting Node's documented contract (it should
resolve silently). Image generation then succeeds but the file save fails
whenever the output directory already exists (e.g. the Desktop):
EEXIST: file already exists, mkdir 'C:\Users\...\Desktop'
Add an ensureDir() helper that tolerates EEXIST only when the path really is a
directory (rethrowing otherwise, so a genuine EEXIST against an existing file
is not swallowed), and route writeImage() and migrateLegacyExtendConfig()
through it. Covered by a cross-platform unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Obsidian creates markdown image links with URL-encoded filenames
(e.g. `Pasted%20image%2020260524.png`) but the actual file has spaces.
Add `decodeURIComponent()` before path resolution in both baoyu-md's
shared `resolveImagePath()` and baoyu-post-to-x's independent version.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add the outline.md + prompts/ → batch.json one-liner to SKILL.md's Usage section so the
build-batch helper is discoverable next to --batchfile, and update build-batch.ts --help
to print the bun / npx-y bun invocations with the {baseDir}/scripts/... layout used by
the rest of the skill.
Add a `Codex via codex exec` branch to each skill's backend-selection
ladder and ship a per-skill references/codex-imagegen.md with the
invocation contract (preferred baoyu-image-gen --provider codex-cli
path, direct-wrapper fallback, parameter notes, stdout schema, batch
semantics).
Replace the long inline `scripts/codex-imagegen.sh` invocation block in
SKILL.md with a pointer to references/codex-imagegen.md. The reference
documents the preferred `baoyu-image-gen --provider codex-cli` path and
keeps the direct-wrapper fallback for runtimes without baoyu-image-gen.
Expose Codex CLI's built-in image_gen tool through baoyu-image-gen's
standard CLI + batch flow as a dedicated provider. The provider spawns
the bundled scripts/codex-imagegen/main.ts (synced from
packages/baoyu-codex-imagegen/src/) so the skill remains self-contained
and inherits retry, cache, file-lock, and JSONL logging. No
OPENAI_API_KEY required — uses the user's Codex subscription.
New env vars: BAOYU_CODEX_IMAGEGEN_{BIN,CACHE_DIR,TIMEOUT_MS,RETRIES,LOG_FILE}.
Hyphenated provider names now resolve under-scored env vars (e.g.,
BAOYU_IMAGE_GEN_CODEX_CLI_CONCURRENCY). codex-cli is never auto-selected
— pin via --provider or default_provider in EXTEND.md.
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
BREAKING CHANGE: removed `baoyu-imagine` and `baoyu-image-cards`. All
functionality now lives under `baoyu-image-gen` and `baoyu-xhs-images`
respectively. Cross-skill `## Image Generation Tools` examples updated
across baoyu-article-illustrator, baoyu-comic, baoyu-cover-image,
baoyu-infographic, and baoyu-slide-deck.
Migration: existing `~/.baoyu-skills/baoyu-imagine/EXTEND.md` configs are
auto-renamed to `…/baoyu-image-gen/EXTEND.md` on first run via the
legacy-path resolver in `scripts/main.ts`.
Extracts resources and JavaScript from any installed Electron app's
app.asar. When .js.map files embed sourcesContent, restores the
original source tree (TypeScript/JSX included); otherwise formats
minified JS/CSS with Prettier in place. Always skips node_modules
and webpack/runtime/* entries.
Source-map paths are resolved relative to each .js.map file first,
so bundler-relative entries like ../../src/main.ts restore to
readable paths under restored/ instead of hashed placeholders.
Auto-discovers apps on macOS (/Applications, ~/Applications) and
Windows (%LOCALAPPDATA%\Programs, %PROGRAMFILES%, %PROGRAMFILES(X86)%,
%APPDATA%). On other platforms pass --asar <path> explicitly.
Safety: refuses to write to /, the user home, or the current working
directory, and refuses non-empty existing output dirs without --force.
Two improvements so the wrapper works the same regardless of caller cwd
or whether cwd is a git repo:
1. main.ts: resolve all filesystem path args (--prompt-file, --ref,
--cache-dir, --log-file) to absolute paths up front. Previously only
--image was resolved. Agents passing relative paths from arbitrary
working directories (e.g. when SKILL.md is interpreted from a plugin
install dir) now produce correct file lookups.
2. spawn.ts: pass --skip-git-repo-check to 'codex exec'. Without it,
codex refuses to run outside a trusted directory:
'Not inside a trusted directory and --skip-git-repo-check was not
specified.' This made the wrapper fail when invoked from /tmp or
from a non-git project tree.
3. baoyu-cover-image/SKILL.md: explicit path resolution note —
scripts/codex-imagegen.sh lives at plugin/repo root (not shell
cwd-relative). From the skill base directory it is at
'../../scripts/codex-imagegen.sh'. Agents should resolve to an
absolute path before invoking.
Verified:
- 16 unit tests pass
- e2e from /tmp (non-git, relative-path args) → 45s, 1.6MB PNG, status:ok
Composes with the existing cover-image wiring (commit e3932e4).
Per review feedback on #158: a backend wrapper alone is not enough —
skills need explicit invocation guidance so the agent can find and
call it.
Updates skills/baoyu-cover-image/SKILL.md in two places:
1. ## Image Generation Tools — new bullet under the backend resolution
list: 'Codex via codex exec (codex-imagegen)' with the exact command
shape (--image / --prompt-file / --aspect / --ref / --cache-dir /
--log-file), JSON output handling, and prerequisites.
2. ### Step 4: Generate Image, step 5 — added a concrete example
command for the codex-imagegen branch alongside the existing generic
instruction.
End-to-end verified: with EXTEND.md set to preferred_image_backend:
codex-imagegen, calling the command shape written in SKILL.md produces
a 1672x941 PNG in 83s (status: ok, attempts: 1, cached: false).
Output: /tmp/cover-e2e/cover.png. Structured log:
/tmp/cover-e2e/run.jsonl.
baoyu-cover-image is the validation skill; if reviewers want the same
guidance in baoyu-article-illustrator / baoyu-comic / baoyu-image-cards
/ baoyu-infographic / baoyu-slide-deck, happy to mirror the change.
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>
Split aliases into group_nicknames (user's own prior names) and
aliases (nicknames from other members). Add tags field for
cross-cutting attributes. Sync SKILL.md version.