@zzclub/pipeline 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +71 -8
  2. package/package.json +3 -4
  3. package/src/commands/prepare.ts +8 -5
  4. package/src/commands/wechat-export.test.ts +41 -0
  5. package/src/commands/wechat-export.ts +22 -8
  6. package/src/config.ts +12 -0
  7. package/src/providers/index.ts +6 -2
  8. package/src/publish-targets.ts +4 -5
  9. package/src/routes.ts +9 -1
  10. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1 -302
  11. package/src/wechat-preview/assets/browser-dist/editor-export.js +444 -958
  12. package/src/wechat-preview/browser/assets.d.ts +9 -0
  13. package/src/wechat-preview/browser/editor-export.css +276 -236
  14. package/src/wechat-preview/browser/editor-export.ts +24 -63
  15. package/src/wechat-preview/fixtures/all-nodes.md +68 -0
  16. package/src/wechat-preview/index.test.ts +17 -0
  17. package/src/wechat-preview/index.ts +44 -107
  18. package/src/wechat-preview/themes.ts +50 -45
  19. package/src/wechat-preview/wechat-export.integration.test.ts +94 -0
  20. package/src/wechat-preview/wechat-renderer.test.ts +48 -0
  21. package/src/wechat-preview/wechat-renderer.ts +652 -0
  22. package/src/wechat-preview.test.ts +18 -41
  23. package/src/workflow.test.ts +44 -5
  24. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  25. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  26. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  27. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  28. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  29. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  30. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  31. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  32. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  33. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  34. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  35. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  36. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  37. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  38. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  39. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  40. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  41. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  42. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  43. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  44. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  45. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  46. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  47. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  48. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  49. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  50. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  51. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  52. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  53. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  54. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  55. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  56. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  57. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  58. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  59. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  60. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  61. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  62. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  63. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  64. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  65. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  66. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  67. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  68. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  69. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  70. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  71. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  72. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  73. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  74. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  75. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  76. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  77. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  78. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  79. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  80. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  81. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  82. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  83. package/src/wechat-preview/assets/browser-dist/assets/style-vhHYSP8T.css +0 -1
  84. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  85. package/src/wechat-preview/wechat-formatter.test.ts +0 -287
  86. package/src/wechat-preview/wechat-formatter.ts +0 -1531
package/README.md CHANGED
@@ -38,12 +38,13 @@ bun install --global .
38
38
 
39
39
  这是一个状态机模型,输入是任务意图、正文、图片、路由和发布要求,输出是可恢复的工作流状态、渲染产物和发布结果。
40
40
 
41
- 当前只有两条主工作流路由:
41
+ 当前有三条主工作流路由:
42
42
 
43
43
  - `wechat-article` — 公众号文章草稿(HTML)
44
44
  - `wechat-newspic` — 公众号图片消息(PNG 卡片/图集)
45
+ - `blog` — 将 Markdown 同步到配置的博客项目并执行发布命令
45
46
 
46
- `blog` 只做事后同步(`sync-blog`),不参与主工作流分支,但同步结果会写回 `workflow-state.json` 的 `publish.results`。
47
+ `blog` 参与统一的 `prepare → publish` 工作流;作为唯一目标时跳过 render。`sync-blog` 继续保留为事后同步命令,两种路径都会把结果写回 `workflow-state.json` 的 `publish.results`。
47
48
 
48
49
  状态文件分两类:
49
50
 
@@ -374,7 +375,7 @@ src/
374
375
  wechat-preview/ # 微信文章 HTML 预览,Milkdown + 主题
375
376
  index.ts
376
377
  themes.ts
377
- wechat-formatter.ts
378
+ wechat-renderer.ts # 元素 renderer 注册表、CSS 内联和微信安全清理
378
379
  frontmatter-handler.ts
379
380
  browser/
380
381
  editor-export.ts
@@ -409,11 +410,12 @@ src/
409
410
  | `review` | Update content review status |
410
411
  | `abandon` | Mark one or more tasks as abandoned |
411
412
 
412
- `ops` 组,11 个命令:
413
+ `ops` 组,12 个命令:
413
414
 
414
415
  | 命令 | 说明 |
415
416
  | --- | --- |
416
417
  | `sync-blog` | Copy post.md to blog repo, publish, and record result in state JSON |
418
+ | `republish` | Publish a completed task to an additional account or platform |
417
419
  | `imgx` | Run bundled imgx renderer subcommands |
418
420
  | `wechat-export` | Render markdown to WeChat HTML with bundled preview styles |
419
421
  | `cos-upload` | Upload a local image to configured COS CDN |
@@ -618,9 +620,57 @@ bun run src/cli.ts hermes-metrics --workspace /abs/workspace
618
620
  ### 微信 HTML 预览导出
619
621
 
620
622
  ```bash
621
- bun run src/cli.ts wechat-export --body /abs/path/body.md --account default
623
+ bun run src/cli.ts wechat-export \
624
+ --markdown /abs/path/body.md \
625
+ --out /abs/path/article.html \
626
+ --account default \
627
+ --preview-shell-out /abs/path/article-preview.html
622
628
  ```
623
629
 
630
+ `--preview-shell-out` 生成最终发布 HTML 的精确预览:预览页直接嵌入 `--out`
631
+ 中的内联样式片段,不再加载 Milkdown 编辑器或复制浏览器 bundle。
632
+
633
+ #### 自定义 CSS
634
+
635
+ 可以通过账号配置或 CLI 覆盖内置样式:
636
+
637
+ ```json
638
+ {
639
+ "wx": {
640
+ "accounts": {
641
+ "default": {
642
+ "customCss": "./themes/default-wechat.css"
643
+ }
644
+ }
645
+ }
646
+ }
647
+ ```
648
+
649
+ ```bash
650
+ bun run src/cli.ts wechat-export \
651
+ --markdown /abs/path/body.md \
652
+ --out /abs/path/article.html \
653
+ --custom-css ./local-wechat.css
654
+ ```
655
+
656
+ 样式优先级固定为:内置排版 CSS → 账号主题令牌 → 账号 `customCss`。传入
657
+ `--custom-css` 时,它会替代账号 `customCss`。CLI 相对路径按当前工作目录解析,
658
+ 配置中的相对路径按 `config.json` 所在目录解析。
659
+
660
+ 推荐使用语义标签或稳定节点钩子:
661
+
662
+ ```css
663
+ .milkdown .editor p { line-height: 1.9; }
664
+ .milkdown .editor h2 { color: #a94473; }
665
+ .milkdown .editor [data-wechat-node="inline-code"] { background: #f7eef2; }
666
+ .milkdown .editor [data-wechat-node="image-caption"] { font-size: 13px; }
667
+ ```
668
+
669
+ `.milkdown .editor` 只是 CSS 内联阶段的兼容包装;最终 HTML 会移除 `class` 和
670
+ `data-wechat-node`。外部 CSS 支持变量、层叠和常规选择器,随后由 Juice 解析为
671
+ 内联样式。最终片段只保留微信稳定标签、图片/链接/表格必要属性和保守 CSS;
672
+ 脚本、事件属性、外部 `<style>`、未解析变量、`flex`/`grid`、定位和伪元素都会被移除。
673
+
624
674
  ### COS 图片上传
625
675
 
626
676
  ```bash
@@ -796,10 +846,23 @@ bun run src/cli.ts init ... --note-id NOTE_ID
796
846
 
797
847
  ## wechat-preview 子系统
798
848
 
799
- - 基于 Milkdown markdown 到微信 HTML 转换
800
- - 支持多主题
849
+ - 基于 Milkdown core + CommonMark/GFM 生成语义 HTML,不读取 Crepe/CodeMirror 可视 DOM
850
+ - 固定管线:Markdown → Milkdown HTML → 元素 renderer prepare → Juice 内联 → renderer finalize → 微信白名单清理
851
+ - 元素 renderer 覆盖段落/标题、强调、行内代码、代码块、引用、图片标题、链接脚注、列表、表格、分隔线和页脚
852
+ - 内置默认采用“静默编辑”:深色标题、低装饰引用和中性行内代码,品牌色只用于链接等识别点
853
+ - `default` 与 `ancientone` 共用排版骨架,只通过主题令牌切换品牌色、字体栈和页脚
801
854
  - 构建命令:`bun run build:wechat-preview`
802
- - `wechat-export` 会直接使用这套预览样式
855
+ - `wechat-export` 和微信草稿发布使用同一份最终内联 HTML
856
+
857
+ `src/wechat-preview/browser/editor-export.css` 会通过 Vite 以 raw CSS 打入浏览器
858
+ bundle。修改该 CSS 或浏览器 renderer 后必须执行:
859
+
860
+ ```bash
861
+ bun run build:wechat-preview
862
+ bun install --global .
863
+ ```
864
+
865
+ 全局 `zzp` 使用编译产物;只修改源码而不重新全局安装不会更新全局命令。
803
866
 
804
867
  ## 插件系统
805
868
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzclub/pipeline",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zzp": "./src/cli.ts",
@@ -34,12 +34,11 @@
34
34
  "cos-nodejs-sdk-v5": "^2.15.4"
35
35
  },
36
36
  "devDependencies": {
37
- "@milkdown/crepe": "^7.18.0",
38
- "@milkdown/plugin-listener": "^7.18.0",
39
- "@milkdown/utils": "^7.18.0",
37
+ "@milkdown/kit": "^7.18.0",
40
38
  "@types/bun": "latest",
41
39
  "@types/pngjs": "^6.0.5",
42
40
  "changelogen": "^0.6.2",
41
+ "juice": "11.1.1",
43
42
  "typescript": "^5.7.0",
44
43
  "vite": "^7.1.10"
45
44
  }
@@ -69,15 +69,16 @@ function getManagedFormattedBodyPath(workspaceRoot: string, runId: string): stri
69
69
  }
70
70
 
71
71
  function shouldPreserveExistingRoute(params: {
72
- intentText: string;
72
+ hasExplicitPublishTargets: boolean;
73
+ hasIntentOverride: boolean;
73
74
  routeOverride?: RoutePrimary;
74
75
  extrasRaw?: string;
75
76
  accountOverride?: string;
76
77
  assetPath: string;
77
78
  }): boolean {
78
79
  return (
79
- Boolean(params.assetPath) &&
80
- !params.intentText.trim() &&
80
+ (params.hasExplicitPublishTargets || Boolean(params.assetPath)) &&
81
+ !params.hasIntentOverride &&
81
82
  !params.routeOverride &&
82
83
  !params.extrasRaw &&
83
84
  !params.accountOverride
@@ -133,7 +134,8 @@ Options:
133
134
  const resolved = await readResolvedState(requestedStatePath);
134
135
  const statePath = resolved.path;
135
136
  const state = resolved.state;
136
- const intentText = optionalArg(parsed, "intent-text") ?? state.intent.intent_text ?? "";
137
+ const intentTextOverride = optionalArg(parsed, "intent-text");
138
+ const intentText = intentTextOverride ?? state.intent.intent_text ?? "";
137
139
  const resolvedBodyPath = bodyPath
138
140
  ? await stageManagedBodyFile(state.workspace_root, state.run_id, bodyPath)
139
141
  : state.source_body_path;
@@ -154,7 +156,8 @@ Options:
154
156
 
155
157
  // ── Step 1: Channel route (1st pass) ──
156
158
  const route = shouldPreserveExistingRoute({
157
- intentText,
159
+ hasExplicitPublishTargets: state.publish_targets.length > 0,
160
+ hasIntentOverride: intentTextOverride !== undefined,
158
161
  routeOverride,
159
162
  extrasRaw,
160
163
  accountOverride,
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolveConfigRelativePath } from "../config";
3
+ import { resolveWechatExportCustomCss } from "./wechat-export";
4
+
5
+ describe("wechat-export custom CSS path precedence", () => {
6
+ const configPath = "/workspace/config/config.json";
7
+
8
+ test("resolves configured CSS relative to the config file", () => {
9
+ expect(resolveConfigRelativePath("themes/account.css", configPath)).toBe(
10
+ "/workspace/config/themes/account.css",
11
+ );
12
+ });
13
+
14
+ test("keeps an absolute configured CSS path", () => {
15
+ expect(resolveConfigRelativePath("/opt/zzhub/account.css", configPath)).toBe(
16
+ "/opt/zzhub/account.css",
17
+ );
18
+ });
19
+
20
+ test("uses account CSS when the CLI override is absent", () => {
21
+ expect(resolveWechatExportCustomCss(
22
+ undefined,
23
+ "themes/account.css",
24
+ "/workspace/post",
25
+ configPath,
26
+ )).toBe("/workspace/config/themes/account.css");
27
+ });
28
+
29
+ test("CLI CSS replaces account CSS and resolves from the current directory", () => {
30
+ expect(resolveWechatExportCustomCss(
31
+ "./local.css",
32
+ "themes/account.css",
33
+ "/workspace/post",
34
+ configPath,
35
+ )).toBe("/workspace/post/local.css");
36
+ });
37
+
38
+ test("returns null when neither source configures CSS", () => {
39
+ expect(resolveWechatExportCustomCss(undefined, null, "/workspace/post", configPath)).toBeNull();
40
+ });
41
+ });
@@ -1,13 +1,12 @@
1
+ import { resolve } from "path";
1
2
  import { optionalArg, parseArgs, requireArg } from "../args";
2
3
  import { printResult, renderWechatExport } from "../output";
3
- import { loadConfig } from "../config";
4
+ import { loadConfig, resolveConfigRelativePath } from "../config";
4
5
  import { resolveMarkdownRenderer } from "../adapter-loader";
5
6
 
6
7
  const CSS_DEMO = `
7
- Custom CSS is injected as a <style> tag after the built-in Milkdown styles
8
- and before the browser renders. WeChat requires inline styles, so the
9
- rendering pipeline captures computed styles from the DOM — your CSS
10
- cascades naturally and the final inline-styled HTML will reflect it.
8
+ Custom CSS is cascaded after the built-in theme and converted to conservative
9
+ inline styles. Use semantic selectors or stable data-wechat-node hooks.
11
10
 
12
11
  Example custom.css:
13
12
 
@@ -26,6 +25,17 @@ Example custom.css:
26
25
  }
27
26
  `.trim();
28
27
 
28
+ export function resolveWechatExportCustomCss(
29
+ cliCustomCss: string | undefined,
30
+ accountCustomCss: string | null | undefined,
31
+ cwd: string = process.cwd(),
32
+ configPath?: string,
33
+ ): string | null {
34
+ return cliCustomCss
35
+ ? resolve(cwd, cliCustomCss)
36
+ : resolveConfigRelativePath(accountCustomCss, configPath);
37
+ }
38
+
29
39
  export async function wechatExport(args: string[]): Promise<void> {
30
40
  const parsed = parseArgs(args);
31
41
 
@@ -38,8 +48,8 @@ Options:
38
48
  --out Path to exported html file (required)
39
49
  --account Account/theme key (optional; default: default)
40
50
  --title Page title for the render shell (optional)
41
- --preview-shell-out Path to full Milkdown preview html (optional)
42
- --custom-css Path to custom CSS file for style overrides (optional)
51
+ --preview-shell-out Path to exact final-HTML preview (optional)
52
+ --custom-css CSS override path; replaces account customCss (optional)
43
53
 
44
54
  ${CSS_DEMO}
45
55
  `.trim());
@@ -51,10 +61,14 @@ ${CSS_DEMO}
51
61
  const account = optionalArg(parsed, "account") ?? "default";
52
62
  const title = optionalArg(parsed, "title");
53
63
  const previewShellOutPath = optionalArg(parsed, "preview-shell-out");
54
- const customCss = optionalArg(parsed, "custom-css");
64
+ const cliCustomCss = optionalArg(parsed, "custom-css");
55
65
 
56
66
  const config = loadConfig();
57
67
  const wxAccount = config.wx.accounts[account] ?? config.wx.accounts[config.wx.defaultAccount];
68
+ const customCss = resolveWechatExportCustomCss(
69
+ cliCustomCss,
70
+ wxAccount?.customCss,
71
+ );
58
72
  const markdownRenderer = await resolveMarkdownRenderer(config);
59
73
  const result = await markdownRenderer.render({
60
74
  markdownPath,
package/src/config.ts CHANGED
@@ -85,6 +85,18 @@ export function getPipelineConfigPath(): string {
85
85
  return join(getBaseConfigDir(PIPELINE_CONFIG_DIR), PIPELINE_CONFIG_FILE);
86
86
  }
87
87
 
88
+ /** Resolve a file path stored in config relative to the config file directory. */
89
+ export function resolveConfigRelativePath(
90
+ value: string | null | undefined,
91
+ configPath: string = getPipelineConfigPath(),
92
+ ): string | null {
93
+ const trimmed = value?.trim();
94
+ if (!trimmed) return null;
95
+ return isAbsolute(trimmed)
96
+ ? resolve(trimmed)
97
+ : resolve(dirname(configPath), trimmed);
98
+ }
99
+
88
100
  export function getLegacyZCliConfigPath(): string {
89
101
  return join(getBaseConfigDir(LEGACY_ZCLI_CONFIG_DIR), PIPELINE_CONFIG_FILE);
90
102
  }
@@ -1,6 +1,10 @@
1
1
  import { readFile, writeFile } from "fs/promises";
2
2
  import { join } from "path";
3
- import { PipelineConfig, ResolvedWorkspacePaths } from "../config";
3
+ import {
4
+ resolveConfigRelativePath,
5
+ type PipelineConfig,
6
+ type ResolvedWorkspacePaths,
7
+ } from "../config";
4
8
  import { PublishResult, RenderAsset, RoutePrimary, WorkflowState } from "../state";
5
9
  import {
6
10
  injectIllustrationImages,
@@ -80,7 +84,7 @@ async function publishWechatArticleRoute({
80
84
  outPath: workspacePaths.zotepadExportHtml,
81
85
  account,
82
86
  title: state.metadata.title,
83
- customCss: wxAccount?.customCss ?? null,
87
+ customCss: resolveConfigRelativePath(wxAccount?.customCss),
84
88
  themeOverrides: wxAccount?.theme,
85
89
  });
86
90
  const response = await createWechatDraft({
@@ -126,13 +126,12 @@ export function getStatePublishTargets(state: {
126
126
  publish_targets: PublishTarget[];
127
127
  route: { primary: RoutePrimary; extras: RoutePrimary[]; account: string };
128
128
  }): PublishTarget[] {
129
- const targets = [
130
- ...[state.route.primary, ...state.route.extras].map((route) => ({
129
+ const targets = state.publish_targets.length > 0
130
+ ? state.publish_targets
131
+ : [state.route.primary, ...state.route.extras].map((route) => ({
131
132
  route,
132
133
  account: state.route.account,
133
- })),
134
- ...state.publish_targets,
135
- ];
134
+ }));
136
135
  const seen = new Set<string>();
137
136
  return targets.filter((target) => {
138
137
  const key = `${target.route}@${target.account}`;
package/src/routes.ts CHANGED
@@ -205,12 +205,13 @@ export function routePlanFromTargets(
205
205
  ): { primary: RoutePrimary; extras: RoutePrimary[] } | null {
206
206
  const uniqueTargets = [...new Set(targets)];
207
207
  const wantsWechat = uniqueTargets.includes("wechat");
208
+ const wantsBlog = uniqueTargets.includes("blog");
208
209
 
209
210
  if (form === "article") {
210
211
  if (wantsWechat) {
211
212
  return {
212
213
  primary: "wechat-article",
213
- extras: [],
214
+ extras: wantsBlog ? ["blog"] : [],
214
215
  };
215
216
  }
216
217
  }
@@ -218,6 +219,13 @@ export function routePlanFromTargets(
218
219
  if (form === "newspic" && wantsWechat) {
219
220
  return {
220
221
  primary: "wechat-newspic",
222
+ extras: wantsBlog ? ["blog"] : [],
223
+ };
224
+ }
225
+
226
+ if (wantsBlog) {
227
+ return {
228
+ primary: "blog",
221
229
  extras: [],
222
230
  };
223
231
  }