@zzclub/pipeline 0.3.0 → 0.4.0

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 (194) hide show
  1. package/README.md +366 -170
  2. package/package.json +4 -7
  3. package/src/adapter-loader.ts +138 -0
  4. package/src/adapter-types.ts +172 -0
  5. package/src/adapters/builtin-image-renderer.ts +353 -0
  6. package/src/adapters/builtin-markdown-renderer.ts +63 -0
  7. package/src/adapters/index.ts +2 -0
  8. package/src/args.test.ts +118 -0
  9. package/src/args.ts +90 -0
  10. package/src/cli.ts +51 -0
  11. package/src/commands/abandon.test.ts +104 -0
  12. package/src/commands/abandon.ts +202 -0
  13. package/src/commands/attach-body-images.ts +88 -0
  14. package/src/commands/attach-body.ts +86 -0
  15. package/src/commands/attach-newspic-spec.ts +41 -0
  16. package/src/commands/checkpoint.test.ts +91 -0
  17. package/src/commands/checkpoint.ts +66 -0
  18. package/src/commands/config.ts +106 -0
  19. package/src/commands/cos-upload.ts +62 -0
  20. package/src/commands/doctor.ts +66 -0
  21. package/src/commands/find-run.ts +55 -0
  22. package/src/commands/hermes-metrics.ts +226 -0
  23. package/src/commands/imgx.ts +60 -0
  24. package/src/commands/ingest-handoff.ts +509 -0
  25. package/src/commands/init.ts +126 -0
  26. package/src/commands/prepare-finalize.ts +348 -0
  27. package/src/commands/prepare.ts +310 -0
  28. package/src/commands/publish.ts +147 -0
  29. package/src/commands/reconcile.ts +36 -0
  30. package/src/commands/render.ts +244 -0
  31. package/src/commands/reset.test.ts +151 -0
  32. package/src/commands/reset.ts +159 -0
  33. package/src/commands/review.ts +77 -0
  34. package/src/commands/status.ts +68 -0
  35. package/src/commands/sync-blog.ts +39 -0
  36. package/src/commands/tasks.ts +50 -0
  37. package/src/commands/wechat-export.ts +68 -0
  38. package/src/config.ts +0 -0
  39. package/src/imgx/ascii-portrait.ts +77 -0
  40. package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
  41. package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
  42. package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
  43. package/src/imgx/assets/icons/logo.png +0 -0
  44. package/src/imgx/assets/icons/logo.svg +8 -0
  45. package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
  46. package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
  47. package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
  48. package/src/imgx/cli.ts +57 -0
  49. package/src/imgx/geometry.ts +68 -0
  50. package/src/imgx/index.ts +5 -0
  51. package/src/imgx/longform-theme.ts +243 -0
  52. package/src/imgx/poster-recipe.ts +81 -0
  53. package/src/imgx/pretext-adapter.ts +14 -0
  54. package/src/imgx/pretext-runtime.ts +61 -0
  55. package/src/imgx/references/ascii-portrait-3-4.md +59 -0
  56. package/src/imgx/references/longform-3-4.md +173 -0
  57. package/src/imgx/references/poster-3-4.md +87 -0
  58. package/src/imgx/references/tips-3-4.md +69 -0
  59. package/src/imgx/references/wechat-cover-split.md +89 -0
  60. package/src/imgx/references/x-like-posts.md +139 -0
  61. package/src/imgx/render-article.test.ts +53 -0
  62. package/src/imgx/render-article.ts +1364 -0
  63. package/src/imgx/render-ascii-portrait.ts +53 -0
  64. package/src/imgx/render-card.ts +232 -0
  65. package/src/imgx/render-x-like-posts.ts +235 -0
  66. package/src/imgx/runtime.ts +403 -0
  67. package/src/imgx/typographic-scale.test.ts +56 -0
  68. package/src/imgx/typographic-scale.ts +44 -0
  69. package/src/output.ts +632 -0
  70. package/src/plugins.ts +101 -0
  71. package/src/profiles.ts +149 -0
  72. package/src/providers/blog.test.ts +58 -0
  73. package/src/providers/blog.ts +154 -0
  74. package/src/providers/cos.ts +183 -0
  75. package/src/providers/index.test.ts +26 -0
  76. package/src/providers/index.ts +185 -0
  77. package/src/providers/wechat.test.ts +215 -0
  78. package/src/providers/wechat.ts +548 -0
  79. package/src/routes.ts +287 -0
  80. package/src/runtime-paths.ts +179 -0
  81. package/src/schema/config.ts +133 -0
  82. package/src/schema/state.ts +376 -0
  83. package/src/spawn.ts +103 -0
  84. package/src/state.ts +265 -0
  85. package/src/task-manager.ts +657 -0
  86. package/src/task-views.ts +222 -0
  87. package/src/text.test.ts +689 -0
  88. package/src/text.ts +683 -0
  89. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  90. package/src/wechat-preview/browser/editor-export.css +241 -0
  91. package/src/wechat-preview/browser/editor-export.ts +103 -0
  92. package/src/wechat-preview/frontmatter-handler.ts +73 -0
  93. package/src/wechat-preview/index.test.ts +154 -0
  94. package/src/wechat-preview/index.ts +294 -0
  95. package/src/wechat-preview/themes.ts +141 -0
  96. package/src/wechat-preview/wechat-formatter.test.ts +288 -0
  97. package/src/wechat-preview/wechat-formatter.ts +1506 -0
  98. package/src/wechat-preview.test.ts +73 -0
  99. package/src/workflow-materials.test.ts +225 -0
  100. package/src/workflow-materials.ts +217 -0
  101. package/src/workflow.test.ts +2087 -0
  102. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  103. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  104. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  105. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  106. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
  107. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  108. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
  109. package/dist/cli.js +0 -431
  110. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
  111. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
  112. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
  113. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
  114. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
  115. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
  116. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
  117. package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
  118. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
  119. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
  120. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
  121. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
  122. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
  123. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
  124. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
  125. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
  126. /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
  127. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
  128. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
  129. /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
  130. /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
  131. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
  132. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  133. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  134. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  135. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  136. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  137. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  138. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  139. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  140. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  141. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  142. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  143. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  144. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  145. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  146. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  147. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  148. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  149. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  150. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  151. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  152. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  153. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  154. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  155. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  156. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  157. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  158. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  159. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  160. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  161. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  162. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  163. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  164. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  165. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  166. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  167. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  168. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  169. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  170. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  171. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  172. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  173. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  174. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  175. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  176. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  177. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  178. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  179. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  180. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  181. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  182. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  183. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  184. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  185. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  186. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  187. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  188. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  189. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  190. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  191. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  192. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
  193. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
  194. /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
package/src/routes.ts ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * routes.ts — Channel routing table
3
+ *
4
+ * Replaces the LLM-based zzhub-media-channel-route skill.
5
+ * All routing rules are deterministic lookup tables.
6
+ */
7
+
8
+ import type {
9
+ RoutePrimary,
10
+ ContentForm,
11
+ AccountVisualParams,
12
+ Route,
13
+ Target,
14
+ } from "./state";
15
+ import type { PipelineConfig } from "./config";
16
+ import { loadConfig } from "./config";
17
+
18
+ // ── Route keyword matching ────────────────────────────────────────
19
+
20
+ /**
21
+ * Route keyword patterns.
22
+ * Each entry maps user-facing keywords to a primary route.
23
+ */
24
+ const ROUTE_PATTERNS: { keywords: string[]; route: RoutePrimary }[] = [
25
+ {
26
+ keywords: [
27
+ "公众号文章",
28
+ "公众号草稿箱",
29
+ "草稿箱",
30
+ "wechat-article",
31
+ "微信文章",
32
+ ],
33
+ route: "wechat-article",
34
+ },
35
+ {
36
+ keywords: [
37
+ "小绿书",
38
+ "newspic",
39
+ "贴图",
40
+ "公众号贴图",
41
+ "wechat-newspic",
42
+ ],
43
+ route: "wechat-newspic",
44
+ },
45
+ ];
46
+
47
+ function isAmbiguousPublicRouteIntent(intentText: string): boolean {
48
+ const text = intentText.toLowerCase();
49
+ return (
50
+ text.includes("公众号") &&
51
+ !text.includes("文章") &&
52
+ !text.includes("贴图") &&
53
+ !text.includes("草稿") &&
54
+ !text.includes("newspic") &&
55
+ !text.includes("小绿书")
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Resolve primary route from user intent text.
61
+ * Returns null if ambiguous (e.g. just "发公众号" without specifying article vs newspic).
62
+ */
63
+ export function resolveRoute(intentText: string): RoutePrimary | null {
64
+ const text = intentText.toLowerCase();
65
+
66
+ for (const { keywords, route } of ROUTE_PATTERNS) {
67
+ for (const kw of keywords) {
68
+ if (text.includes(kw.toLowerCase())) {
69
+ return route;
70
+ }
71
+ }
72
+ }
73
+
74
+ // "发公众号" alone is ambiguous — could be article or newspic
75
+ if (isAmbiguousPublicRouteIntent(text)) {
76
+ return null; // Ambiguous
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ // ── Account resolution ────────────────────────────────────────────
83
+
84
+ /**
85
+ * Account keyword patterns.
86
+ */
87
+ const ACCOUNT_PATTERNS: { keywords: string[]; account: string }[] = [
88
+ {
89
+ keywords: ["大号", "早早集市", "default"],
90
+ account: "default",
91
+ },
92
+ {
93
+ keywords: ["古一", "小号", "ancientone", "storytelling", "story"],
94
+ account: "ancientone",
95
+ },
96
+ ];
97
+
98
+ /**
99
+ * Resolve account from user intent text.
100
+ * Returns "default" if no explicit account specified.
101
+ */
102
+ export function resolveAccount(intentText: string): string {
103
+ const text = intentText.toLowerCase();
104
+
105
+ for (const { keywords, account } of ACCOUNT_PATTERNS) {
106
+ for (const kw of keywords) {
107
+ if (text.toLowerCase().includes(kw.toLowerCase())) {
108
+ return account;
109
+ }
110
+ }
111
+ }
112
+
113
+ return "default";
114
+ }
115
+
116
+ // ── Content profile registry ──────────────────────────────────────
117
+
118
+ /**
119
+ * Account -> content profile mapping.
120
+ */
121
+ const CONTENT_PROFILES: Record<string, string> = {
122
+ default: "user",
123
+ ancientone: "storytelling",
124
+ };
125
+
126
+ /**
127
+ * Get content profile for an account.
128
+ */
129
+ export function getContentProfile(account: string): string {
130
+ return CONTENT_PROFILES[account] ?? "none";
131
+ }
132
+
133
+ // ── Account visual params registry ────────────────────────────────
134
+
135
+ /**
136
+ * Account visual parameters for imgx rendering.
137
+ * Only used by poster-3-4 template (newspic).
138
+ * wechat-cover-split uses its own color scheme.
139
+ */
140
+ const VISUAL_PARAMS: Record<string, AccountVisualParams> = {
141
+ default: {
142
+ footer: "公众号 · 早早集市",
143
+ bg: "#e6f5ef",
144
+ highlight: "#22a854",
145
+ fallback_icon: "assets/icons/logo.png",
146
+ },
147
+ ancientone: {
148
+ footer: "公众号 · 古一软件",
149
+ bg: "#faf5f8",
150
+ highlight: "#ca6093",
151
+ fallback_icon: "assets/icons/logo.png",
152
+ },
153
+ };
154
+
155
+ /**
156
+ * Get visual params for an account.
157
+ * If config.imgx.icon is set, it overrides the default fallback_icon.
158
+ * Returns null if account not found.
159
+ */
160
+ export function getVisualParams(
161
+ account: string,
162
+ config?: PipelineConfig,
163
+ ): AccountVisualParams | null {
164
+ const base = VISUAL_PARAMS[account] ?? null;
165
+ if (!base) return null;
166
+ if (config?.imgx?.icon) {
167
+ return { ...base, fallback_icon: config.imgx.icon };
168
+ }
169
+ return base;
170
+ }
171
+
172
+ // ── Theme registry (for longform-3-4) ─────────────────────────────
173
+
174
+ const LONGFORM_THEMES: Record<string, string> = {
175
+ default: "paper-sage",
176
+ ancientone: "linen-news",
177
+ };
178
+
179
+ /**
180
+ * Get longform-3-4 theme for an account.
181
+ */
182
+ export function getLongformTheme(account: string): string {
183
+ return LONGFORM_THEMES[account] ?? "paper-sage";
184
+ }
185
+
186
+ // ── Content-form → route mapping ──────────────────────────────────
187
+
188
+ /**
189
+ * Derive primary route from content_form when keyword matching fails.
190
+ * Returns null for "unknown" — caller should fall back to default.
191
+ */
192
+ const CONTENT_FORM_ROUTE: Record<ContentForm, RoutePrimary | null> = {
193
+ article: "wechat-article",
194
+ newspic: "wechat-newspic",
195
+ unknown: null,
196
+ };
197
+
198
+ export function routeFromContentForm(form: ContentForm): RoutePrimary | null {
199
+ return CONTENT_FORM_ROUTE[form] ?? null;
200
+ }
201
+
202
+ export function routePlanFromTargets(
203
+ form: ContentForm,
204
+ targets: Target[],
205
+ ): { primary: RoutePrimary; extras: RoutePrimary[] } | null {
206
+ const uniqueTargets = [...new Set(targets)];
207
+ const wantsWechat = uniqueTargets.includes("wechat");
208
+
209
+ if (form === "article") {
210
+ if (wantsWechat) {
211
+ return {
212
+ primary: "wechat-article",
213
+ extras: [],
214
+ };
215
+ }
216
+ }
217
+
218
+ if (form === "newspic" && wantsWechat) {
219
+ return {
220
+ primary: "wechat-newspic",
221
+ extras: [],
222
+ };
223
+ }
224
+
225
+ return null;
226
+ }
227
+
228
+ // ── Full route resolution ─────────────────────────────────────────
229
+
230
+ /**
231
+ * Resolve the complete Route object from intent text and explicit overrides.
232
+ * highlight_words are NOT set here — they come from prepare-finalize after title is known.
233
+ *
234
+ * Fallback chain for primary route:
235
+ * 1. Explicit --route override
236
+ * 2. Keyword matching from intent text
237
+ * 3. content_form from state (article→wechat-article, newspic→wechat-newspic)
238
+ * 4. Default: "wechat-article"
239
+ */
240
+ export function resolveFullRoute(
241
+ intentText: string,
242
+ overrides?: {
243
+ primary?: RoutePrimary;
244
+ extras?: RoutePrimary[];
245
+ account?: string;
246
+ contentForm?: ContentForm;
247
+ targets?: Target[];
248
+ },
249
+ config?: PipelineConfig,
250
+ ): Route {
251
+ const resolvedConfig = config ?? loadConfig();
252
+ const routeFromIntent = resolveRoute(intentText);
253
+ const routePlan = overrides?.contentForm && overrides?.targets
254
+ ? routePlanFromTargets(overrides.contentForm, overrides.targets)
255
+ : null;
256
+ const routeFromForm = overrides?.contentForm
257
+ ? routeFromContentForm(overrides.contentForm)
258
+ : null;
259
+
260
+ let primary =
261
+ overrides?.primary ??
262
+ routeFromIntent ??
263
+ routePlan?.primary ??
264
+ routeFromForm;
265
+ if (!primary) {
266
+ if (intentText.trim() && isAmbiguousPublicRouteIntent(intentText)) {
267
+ throw new Error(
268
+ "Ambiguous route: user requested 公众号 but did not specify article or newspic",
269
+ );
270
+ }
271
+ throw new Error(
272
+ "Unable to resolve route from intent or content_form; orchestrator must classify first",
273
+ );
274
+ }
275
+ const account = overrides?.account ?? resolveAccount(intentText);
276
+ const contentProfile = getContentProfile(account);
277
+ const visualParams = getVisualParams(account, resolvedConfig);
278
+
279
+ return {
280
+ primary,
281
+ extras: overrides?.extras ?? routePlan?.extras ?? [],
282
+ account,
283
+ content_profile: contentProfile,
284
+ account_visual_params: visualParams,
285
+ highlight_words: [], // Set later by prepare-finalize
286
+ };
287
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Centralized asset path resolution for dev and compiled modes.
3
+ *
4
+ * Dev mode: running via `bun run src/cli.ts` — assets in source tree
5
+ * Compiled mode: running a `bun build --compile` binary — assets next to binary
6
+ *
7
+ * Override: set ZZHUB_PIPELINE_ROOT env var to force a specific root.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ // ── Dev mode: resolve from source tree ──────────────────────────
16
+
17
+ const _filename = fileURLToPath(import.meta.url);
18
+ const _dirname = dirname(_filename);
19
+
20
+ // In dev mode, this file is at src/runtime-paths.ts, so PACKAGE_ROOT = ..
21
+ const DEV_PACKAGE_ROOT = resolve(_dirname, "..");
22
+
23
+ // ── Mode detection ──────────────────────────────────────────────
24
+
25
+ function getBinaryDir(): string | null {
26
+ const binPath = process.argv[0];
27
+ if (!binPath || !existsSync(binPath)) return null;
28
+ return dirname(binPath);
29
+ }
30
+
31
+ function isCompiledMode(): boolean {
32
+ if (process.env.ZZHUB_PIPELINE_ROOT) return false;
33
+ const binDir = getBinaryDir();
34
+ if (!binDir) return false;
35
+ // In compiled mode, the binary is standalone — check for assets/ sibling
36
+ return existsSync(join(binDir, "assets")) && !existsSync(join(binDir, "cli.js"));
37
+ }
38
+
39
+ // ── Root resolution ─────────────────────────────────────────────
40
+
41
+ function resolveRoot(): string {
42
+ const envOverride = process.env.ZZHUB_PIPELINE_ROOT;
43
+ if (envOverride && envOverride.trim()) {
44
+ return resolve(envOverride.trim());
45
+ }
46
+
47
+ if (isCompiledMode()) {
48
+ return getBinaryDir()!;
49
+ }
50
+
51
+ return DEV_PACKAGE_ROOT;
52
+ }
53
+
54
+ const _root = resolveRoot();
55
+ const _isDistMode = isCompiledMode();
56
+
57
+ // ── Path exports ────────────────────────────────────────────────
58
+
59
+ /** Project root (source tree in dev, binary dir in compiled) */
60
+ export const PACKAGE_ROOT = _root;
61
+
62
+ /** imgx module directory */
63
+ export const IMGX_DIR = join(_root, _isDistMode ? "assets/imgx" : "src/imgx");
64
+
65
+ /** imgx assets directory */
66
+ export const ASSETS_DIR = join(IMGX_DIR, "assets");
67
+
68
+ /** imgx templates directory */
69
+ export const TEMPLATES_DIR = join(ASSETS_DIR, "templates");
70
+
71
+ /** imgx icons directory */
72
+ export const ICONS_DIR = join(ASSETS_DIR, "icons");
73
+
74
+ /** imgx styles directory */
75
+ export const STYLES_DIR = join(ASSETS_DIR, "styles");
76
+
77
+ /** wechat-preview module directory */
78
+ export const WECHAT_PREVIEW_DIR = join(_root, _isDistMode ? "assets/wechat-preview" : "src/wechat-preview");
79
+
80
+ /** wechat-preview template */
81
+ export const TEMPLATE_PATH = join(WECHAT_PREVIEW_DIR, "assets/templates/export-shell.html");
82
+
83
+ /** wechat-preview browser-dist directory */
84
+ export const DIST_DIR = join(WECHAT_PREVIEW_DIR, "assets/browser-dist");
85
+
86
+ /** wechat-preview Vite manifest */
87
+ export const MANIFEST_PATH = join(DIST_DIR, ".vite/manifest.json");
88
+
89
+ /** Vite config path (dev only) */
90
+ export const VITE_CONFIG_PATH = join(PACKAGE_ROOT, "vite.wechat-preview.config.ts");
91
+
92
+ /** @chenglou/pretext layout.js path */
93
+ export const PRETEXT_MODULE_PATH = join(PACKAGE_ROOT, "node_modules/@chenglou/pretext/dist/layout.js");
94
+
95
+ /** Helper: read a UTF-8 file */
96
+ export function readUtf8(path: string): string {
97
+ return readFileSync(path, "utf8");
98
+ }
99
+
100
+ // ── Font cache ──────────────────────────────────────────────────
101
+
102
+ const FONT_FILES = [
103
+ "AlimamaShuHeiTi-Bold.ttf",
104
+ "LXGWNeoZhiSongPlus.ttf",
105
+ "LXGWWenKai-Regular.ttf",
106
+ ];
107
+
108
+ function getFontCacheDir(): string {
109
+ const platform = process.platform;
110
+ if (platform === "win32") {
111
+ const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
112
+ return join(appData, "zzhub-pipeline", "fonts");
113
+ }
114
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
115
+ return join(base, "zzhub-pipeline", "fonts");
116
+ }
117
+
118
+ function resolveFontsDir(): string {
119
+ // Fonts in assets/ (dev/compiled mode) or cache
120
+ const assetsFonts = join(ASSETS_DIR, "fonts");
121
+ if (existsSync(join(assetsFonts, "AlimamaShuHeiTi-Bold.ttf"))) {
122
+ return assetsFonts;
123
+ }
124
+ const cacheDir = getFontCacheDir();
125
+ if (existsSync(join(cacheDir, "AlimamaShuHeiTi-Bold.ttf"))) {
126
+ return cacheDir;
127
+ }
128
+ return cacheDir;
129
+ }
130
+
131
+ /** imgx fonts directory (assets or cache) */
132
+ export const FONTS_DIR = resolveFontsDir();
133
+
134
+ /**
135
+ * Ensure CJK fonts are available locally.
136
+ * Fonts are in assets/ by default; downloads from CDN if not present and env is set.
137
+ *
138
+ * @returns The fonts directory path
139
+ */
140
+ export async function ensureFonts(): Promise<string> {
141
+ const dir = FONTS_DIR;
142
+ const allExist = FONT_FILES.every((f) => existsSync(join(dir, f)));
143
+ if (allExist) return dir;
144
+
145
+ const cdnBase = process.env.ZZHUB_FONT_CDN_BASE_URL;
146
+ if (!cdnBase) {
147
+ throw new Error(
148
+ [
149
+ "CJK fonts not found and ZZHUB_FONT_CDN_BASE_URL is not set.",
150
+ "",
151
+ `Fonts directory: ${dir}`,
152
+ "",
153
+ "To fix, either:",
154
+ " 1. Set ZZHUB_FONT_CDN_BASE_URL to download fonts automatically,",
155
+ ` 2. Manually place these files in ${dir}:`,
156
+ ...FONT_FILES.map((f) => ` - ${f}`),
157
+ ].join("\n"),
158
+ );
159
+ }
160
+
161
+ mkdirSync(dir, { recursive: true });
162
+
163
+ for (const file of FONT_FILES) {
164
+ const dest = join(dir, file);
165
+ if (existsSync(dest)) continue;
166
+
167
+ const url = `${cdnBase.replace(/\/$/, "")}/${file}`;
168
+ console.error(`[zzhub-pipeline] Downloading ${file}...`);
169
+ const resp = await fetch(url);
170
+ if (!resp.ok) {
171
+ throw new Error(`Failed to download ${url}: HTTP ${resp.status}`);
172
+ }
173
+ const buf = Buffer.from(await resp.arrayBuffer());
174
+ writeFileSync(dest, buf);
175
+ console.error(`[zzhub-pipeline] Saved ${file} (${(buf.length / 1024 / 1024).toFixed(1)}MB)`);
176
+ }
177
+
178
+ return dir;
179
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Zod schemas for PipelineConfig.
3
+ *
4
+ * Replaces hand-written interfaces + normalize functions + default values.
5
+ */
6
+
7
+ import { z } from "zod";
8
+
9
+ // ── Helpers ───────────────────────────────────────────────────────
10
+
11
+ const trimmedNonEmptyString = (fallback: string) =>
12
+ z.preprocess(
13
+ (val) => (typeof val === "string" && val.trim().length > 0 ? val.trim() : undefined),
14
+ z.string().default(fallback),
15
+ );
16
+
17
+ const nullableTrimmedString = z.preprocess(
18
+ (val) => {
19
+ if (typeof val === "string") {
20
+ const trimmed = val.trim();
21
+ return trimmed.length > 0 ? trimmed : null;
22
+ }
23
+ return val;
24
+ },
25
+ z.string().nullable().default(null),
26
+ );
27
+
28
+ function withObjectDefault<T extends z.ZodObject<any>>(schema: T) {
29
+ // All fields in the schema have defaults, so parse({}) always succeeds.
30
+ return schema.optional().default(() => schema.parse({}) as any);
31
+ }
32
+
33
+ // ── Sub-schemas ───────────────────────────────────────────────────
34
+
35
+ const WxAccountConfigSchema = z.object({
36
+ pat: z.string().default(""),
37
+ appId: z.string().default(""),
38
+ appSecret: z.string().default(""),
39
+ customCss: z.string().nullable().default(null),
40
+ });
41
+
42
+ const WxConfigSchema = withObjectDefault(
43
+ z.object({
44
+ baseUrl: trimmedNonEmptyString(""),
45
+ timeout: z.preprocess(
46
+ (val) => {
47
+ if (typeof val === "number" && Number.isFinite(val) && val > 0) return val;
48
+ return undefined;
49
+ },
50
+ z.number().default(30000),
51
+ ),
52
+ defaultAccount: trimmedNonEmptyString("default"),
53
+ accounts: z
54
+ .record(z.string(), WxAccountConfigSchema)
55
+ .default({ default: { pat: "", appId: "", appSecret: "", customCss: null } }),
56
+ }),
57
+ );
58
+
59
+ const PipelinePathConfigSchema = withObjectDefault(
60
+ z.object({
61
+ workspaceRoot: nullableTrimmedString,
62
+ postsDirName: trimmedNonEmptyString("posts"),
63
+ postsPathPattern: trimmedNonEmptyString("{date}-{slug}"),
64
+ blogRoot: nullableTrimmedString,
65
+ zotepadExportHtml: nullableTrimmedString,
66
+ }),
67
+ );
68
+
69
+ const PipelineServiceConfigSchema = withObjectDefault(
70
+ z.object({
71
+ zotepadBaseUrl: trimmedNonEmptyString("http://127.0.0.1:54577"),
72
+ zotepadToken: z.string().default(""),
73
+ }),
74
+ );
75
+
76
+ const PipelineCommandConfigSchema = withObjectDefault(
77
+ z.object({
78
+ blogPublish: z
79
+ .array(z.string())
80
+ .default(["pnpm", "publish:post"])
81
+ .pipe(z.array(z.string()).min(1)),
82
+ }),
83
+ );
84
+
85
+ const CosConfigSchema = withObjectDefault(
86
+ z.object({
87
+ pat: z.string().default(""),
88
+ baseUrl: nullableTrimmedString,
89
+ publicBaseUrl: trimmedNonEmptyString(""),
90
+ }),
91
+ );
92
+
93
+ const PluginsConfigSchema = withObjectDefault(
94
+ z.object({
95
+ imageRenderer: z.string().nullable().default(null),
96
+ markdownRenderer: z.string().nullable().default(null),
97
+ }),
98
+ );
99
+
100
+ const ImgxConfigSchema = withObjectDefault(
101
+ z.object({
102
+ icon: z.string().nullable().default(null),
103
+ }),
104
+ );
105
+
106
+ // ── Top-level schema ──────────────────────────────────────────────
107
+
108
+ export const PipelineConfigSchema = z.object({
109
+ paths: PipelinePathConfigSchema,
110
+ services: PipelineServiceConfigSchema,
111
+ commands: PipelineCommandConfigSchema,
112
+ wx: WxConfigSchema,
113
+ cos: CosConfigSchema,
114
+ plugins: PluginsConfigSchema,
115
+ imgx: ImgxConfigSchema,
116
+ });
117
+
118
+ export type PipelineConfig = z.infer<typeof PipelineConfigSchema>;
119
+
120
+ // ── Sub-type exports ──────────────────────────────────────────────
121
+
122
+ export type WxAccountConfig = z.infer<typeof WxAccountConfigSchema>;
123
+ export type WxConfig = z.infer<typeof WxConfigSchema>;
124
+ export type PipelinePathConfig = z.infer<typeof PipelinePathConfigSchema>;
125
+ export type PipelineServiceConfig = z.infer<typeof PipelineServiceConfigSchema>;
126
+ export type PipelineCommandConfig = z.infer<typeof PipelineCommandConfigSchema>;
127
+ export type CosConfig = z.infer<typeof CosConfigSchema>;
128
+ export type PluginsConfig = z.infer<typeof PluginsConfigSchema>;
129
+ export type ImgxConfig = z.infer<typeof ImgxConfigSchema>;
130
+
131
+ // ── Schema instances for reuse ────────────────────────────────────
132
+
133
+ export { WxAccountConfigSchema, WxConfigSchema, PipelinePathConfigSchema };