@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
@@ -0,0 +1,185 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { PipelineConfig, ResolvedWorkspacePaths } from "../config";
4
+ import { PublishResult, RenderAsset, RoutePrimary, WorkflowState } from "../state";
5
+ import {
6
+ injectIllustrationImages,
7
+ prepareBodyForNewspic,
8
+ } from "../text";
9
+ import { resolveMarkdownRenderer } from "../adapter-loader";
10
+ import { createWechatDraft, createWechatNewspic, extractImageUrls, mergePhotoLists } from "./wechat";
11
+
12
+ export interface PublishRouteContext {
13
+ state: WorkflowState;
14
+ dryRun: boolean;
15
+ config: PipelineConfig;
16
+ workspacePaths: ResolvedWorkspacePaths;
17
+ }
18
+
19
+ export type PublishProvider = (ctx: PublishRouteContext) => Promise<PublishResult>;
20
+
21
+ async function publishWechatArticleRoute({
22
+ state,
23
+ dryRun,
24
+ config,
25
+ workspacePaths,
26
+ }: PublishRouteContext): Promise<PublishResult> {
27
+ const postPath = join(state.asset_path, "post.md");
28
+ let exportPostPath = postPath;
29
+ const cover = (state.images.render_assets as RenderAsset[]).find(
30
+ (asset) => asset.route === "wechat-article" && asset.kind === "cover",
31
+ );
32
+
33
+ const photos: string[] = [];
34
+ if (cover) {
35
+ photos.push(cover.path);
36
+ }
37
+ if (
38
+ state.images.body_inputs.scope === "article" &&
39
+ state.images.body_inputs.status === "ready"
40
+ ) {
41
+ const postContent = await readFile(postPath, "utf-8");
42
+ const draftContent = injectIllustrationImages(
43
+ postContent,
44
+ state.images.body_inputs.received,
45
+ );
46
+ exportPostPath = join(state.asset_path, "post-wechat-draft.md");
47
+ await writeFile(exportPostPath, draftContent, "utf-8");
48
+
49
+ for (const img of state.images.body_inputs.received) {
50
+ photos.push(img.path);
51
+ }
52
+ }
53
+
54
+ if (dryRun) {
55
+ console.error(`[dry-run] Zotepad export: ${exportPostPath}`);
56
+ console.error(`[dry-run] Wechat draft: ${state.metadata.title}`);
57
+ return {
58
+ route: "wechat-article",
59
+ status: "skipped",
60
+ detail: "dry-run",
61
+ published_at: null,
62
+ content_version: state.artifacts.content_version,
63
+ render_version: state.artifacts.render_version,
64
+ };
65
+ }
66
+
67
+ try {
68
+ const markdownRenderer = await resolveMarkdownRenderer(config);
69
+ const wxAccount = config.wx.accounts[state.route.account] ?? config.wx.accounts[config.wx.defaultAccount];
70
+ const { html } = await markdownRenderer.render({
71
+ markdownPath: exportPostPath,
72
+ outPath: workspacePaths.zotepadExportHtml,
73
+ account: state.route.account,
74
+ title: state.metadata.title,
75
+ customCss: wxAccount?.customCss ?? null,
76
+ });
77
+ await createWechatDraft({
78
+ account: state.route.account,
79
+ title: state.metadata.title,
80
+ html,
81
+ photos,
82
+ config,
83
+ });
84
+ } catch (error) {
85
+ return {
86
+ route: "wechat-article",
87
+ status: "failed",
88
+ detail: error instanceof Error ? error.message : String(error),
89
+ published_at: null,
90
+ content_version: state.artifacts.content_version,
91
+ render_version: state.artifacts.render_version,
92
+ };
93
+ }
94
+
95
+ return {
96
+ route: "wechat-article",
97
+ status: "success",
98
+ detail: null,
99
+ published_at: new Date().toISOString(),
100
+ content_version: state.artifacts.content_version,
101
+ render_version: state.artifacts.render_version,
102
+ };
103
+ }
104
+
105
+ async function publishWechatNewspicRoute({
106
+ state,
107
+ dryRun,
108
+ config,
109
+ }: PublishRouteContext): Promise<PublishResult> {
110
+ const postPath = join(state.asset_path, "post.md");
111
+ const postContent = await readFile(postPath, "utf-8");
112
+ const bodyImageUrls = extractImageUrls(postContent);
113
+ const cleanContent = prepareBodyForNewspic(postContent);
114
+ const cleanPath = join(state.asset_path, "post-clean.md");
115
+ await writeFile(cleanPath, cleanContent, "utf-8");
116
+
117
+ const assets = (state.images.render_assets as RenderAsset[])
118
+ .filter((asset) => asset.route === "wechat-newspic")
119
+ .sort((a, b) => {
120
+ if (a.kind === "cover" && b.kind !== "cover") return -1;
121
+ if (a.kind !== "cover" && b.kind === "cover") return 1;
122
+ return (a.index ?? 0) - (b.index ?? 0);
123
+ });
124
+
125
+ const renderPhotos = assets.map((asset) => asset.path);
126
+ const photos = mergePhotoLists(renderPhotos, bodyImageUrls);
127
+
128
+ if (dryRun) {
129
+ console.error(`[dry-run] Wechat newspic: ${state.metadata.title}`);
130
+ console.error(`[dry-run] Content file: ${cleanPath}`);
131
+ return {
132
+ route: "wechat-newspic",
133
+ status: "skipped",
134
+ detail: "dry-run",
135
+ published_at: null,
136
+ content_version: state.artifacts.content_version,
137
+ render_version: state.artifacts.render_version,
138
+ };
139
+ }
140
+
141
+ try {
142
+ await createWechatNewspic({
143
+ account: state.route.account,
144
+ title: state.metadata.title,
145
+ content: cleanContent,
146
+ photos,
147
+ config,
148
+ });
149
+ } catch (error) {
150
+ return {
151
+ route: "wechat-newspic",
152
+ status: "failed",
153
+ detail: error instanceof Error ? error.message : String(error),
154
+ published_at: null,
155
+ content_version: state.artifacts.content_version,
156
+ render_version: state.artifacts.render_version,
157
+ };
158
+ }
159
+
160
+ return {
161
+ route: "wechat-newspic",
162
+ status: "success",
163
+ detail: null,
164
+ published_at: new Date().toISOString(),
165
+ content_version: state.artifacts.content_version,
166
+ render_version: state.artifacts.render_version,
167
+ };
168
+ }
169
+
170
+ const PUBLISH_PROVIDERS: Partial<Record<RoutePrimary, PublishProvider>> = {
171
+ "wechat-article": publishWechatArticleRoute,
172
+ "wechat-newspic": publishWechatNewspicRoute,
173
+ };
174
+
175
+ export function getPublishProvider(route: RoutePrimary): PublishProvider {
176
+ const provider = PUBLISH_PROVIDERS[route];
177
+ if (!provider) {
178
+ throw new Error(`Unsupported workflow publish route: ${route}`);
179
+ }
180
+ return provider;
181
+ }
182
+
183
+ export function listPublishProviders(): RoutePrimary[] {
184
+ return Object.keys(PUBLISH_PROVIDERS) as RoutePrimary[];
185
+ }
@@ -0,0 +1,215 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ normalizeAccountName,
4
+ resolveTimeout,
5
+ parsePhotos,
6
+ mergePhotoLists,
7
+ extractImageUrls,
8
+ replaceImageUrls,
9
+ isRetryableTokenFetchError,
10
+ resolveFilenameFromUrl,
11
+ } from "./wechat";
12
+
13
+ describe("normalizeAccountName", () => {
14
+ test("accepts valid account names", () => {
15
+ expect(normalizeAccountName("default")).toBe("default");
16
+ expect(normalizeAccountName("my-account")).toBe("my-account");
17
+ expect(normalizeAccountName("account_v2")).toBe("account_v2");
18
+ expect(normalizeAccountName("test.account")).toBe("test.account");
19
+ });
20
+
21
+ test("trims whitespace", () => {
22
+ expect(normalizeAccountName(" default ")).toBe("default");
23
+ });
24
+
25
+ test("throws on empty string", () => {
26
+ expect(() => normalizeAccountName("")).toThrow("wx account cannot be empty");
27
+ expect(() => normalizeAccountName(" ")).toThrow("wx account cannot be empty");
28
+ });
29
+
30
+ test("throws on invalid characters", () => {
31
+ expect(() => normalizeAccountName("my account")).toThrow("invalid wx account");
32
+ expect(() => normalizeAccountName("account@name")).toThrow("invalid wx account");
33
+ });
34
+ });
35
+
36
+ describe("resolveTimeout", () => {
37
+ test("returns override when valid", () => {
38
+ expect(resolveTimeout(5000, 10000, 15000)).toBe(15000);
39
+ });
40
+
41
+ test("returns config timeout when override is absent", () => {
42
+ expect(resolveTimeout(5000, 10000)).toBe(10000);
43
+ });
44
+
45
+ test("returns default when config and override are absent", () => {
46
+ expect(resolveTimeout(5000, 0)).toBe(5000);
47
+ });
48
+
49
+ test("ignores invalid override (zero, negative, non-finite)", () => {
50
+ expect(resolveTimeout(5000, 10000, 0)).toBe(10000);
51
+ expect(resolveTimeout(5000, 10000, -1)).toBe(10000);
52
+ expect(resolveTimeout(5000, 10000, Infinity)).toBe(10000);
53
+ expect(resolveTimeout(5000, 10000, NaN)).toBe(10000);
54
+ });
55
+
56
+ test("ignores invalid config timeout", () => {
57
+ expect(resolveTimeout(5000, -1)).toBe(5000);
58
+ expect(resolveTimeout(5000, 0)).toBe(5000);
59
+ });
60
+ });
61
+
62
+ describe("parsePhotos", () => {
63
+ test("trims and filters empty entries", () => {
64
+ expect(parsePhotos([" img1.png ", "", " img2.png "])).toEqual(["img1.png", "img2.png"]);
65
+ });
66
+
67
+ test("returns empty array for undefined", () => {
68
+ expect(parsePhotos(undefined)).toEqual([]);
69
+ });
70
+
71
+ test("returns empty array for empty array", () => {
72
+ expect(parsePhotos([])).toEqual([]);
73
+ });
74
+
75
+ test("filters whitespace-only entries", () => {
76
+ expect(parsePhotos(["img.png", " ", "other.png"])).toEqual(["img.png", "other.png"]);
77
+ });
78
+ });
79
+
80
+ describe("mergePhotoLists", () => {
81
+ test("deduplicates across multiple lists", () => {
82
+ const result = mergePhotoLists(
83
+ ["a.png", "b.png"],
84
+ ["b.png", "c.png"],
85
+ ["c.png", "d.png"],
86
+ );
87
+ expect(result).toEqual(["a.png", "b.png", "c.png", "d.png"]);
88
+ });
89
+
90
+ test("handles empty lists", () => {
91
+ expect(mergePhotoLists([], ["a.png"], [])).toEqual(["a.png"]);
92
+ });
93
+
94
+ test("trims entries before deduplication", () => {
95
+ expect(mergePhotoLists([" a.png "], ["a.png"])).toEqual(["a.png"]);
96
+ });
97
+
98
+ test("filters empty entries", () => {
99
+ expect(mergePhotoLists(["", "a.png", " "])).toEqual(["a.png"]);
100
+ });
101
+ });
102
+
103
+ describe("extractImageUrls", () => {
104
+ test("extracts markdown image URLs", () => {
105
+ const input = "![alt](https://example.com/img.png) and ![other](https://cdn.com/other.jpg)";
106
+ const urls = extractImageUrls(input);
107
+ expect(urls).toContain("https://example.com/img.png");
108
+ expect(urls).toContain("https://cdn.com/other.jpg");
109
+ });
110
+
111
+ test("extracts HTML img src URLs", () => {
112
+ const input = '<img src="https://example.com/img.png" alt="test">';
113
+ const urls = extractImageUrls(input);
114
+ expect(urls).toContain("https://example.com/img.png");
115
+ });
116
+
117
+ test("deduplicates URLs", () => {
118
+ const input = "![a](https://example.com/img.png) ![b](https://example.com/img.png)";
119
+ const urls = extractImageUrls(input);
120
+ expect(urls.filter((u) => u === "https://example.com/img.png")).toHaveLength(1);
121
+ });
122
+
123
+ test("handles mixed markdown and HTML images", () => {
124
+ const input = '![md](https://a.com/1.png) <img src="https://b.com/2.png">';
125
+ const urls = extractImageUrls(input);
126
+ expect(urls).toHaveLength(2);
127
+ });
128
+
129
+ test("returns empty array for no images", () => {
130
+ expect(extractImageUrls("just text")).toEqual([]);
131
+ });
132
+
133
+ test("handles image with title attribute", () => {
134
+ const input = '![alt](https://example.com/img.png "title")';
135
+ const urls = extractImageUrls(input);
136
+ expect(urls).toContain("https://example.com/img.png");
137
+ });
138
+ });
139
+
140
+ describe("replaceImageUrls", () => {
141
+ test("replaces URLs using the mapping", () => {
142
+ const html = '<img src="https://old.com/img.png">';
143
+ const result = replaceImageUrls(html, { "https://old.com/img.png": "https://new.com/img.png" });
144
+ expect(result).toBe('<img src="https://new.com/img.png">');
145
+ });
146
+
147
+ test("escapes special regex characters in URLs", () => {
148
+ const html = '<img src="https://old.com/img.png?v=1&size=large">';
149
+ const result = replaceImageUrls(html, {
150
+ "https://old.com/img.png?v=1&size=large": "https://new.com/img.png",
151
+ });
152
+ expect(result).toBe('<img src="https://new.com/img.png">');
153
+ });
154
+
155
+ test("replaces multiple occurrences", () => {
156
+ const html = '<img src="https://a.com/1.png"><img src="https://a.com/1.png">';
157
+ const result = replaceImageUrls(html, { "https://a.com/1.png": "https://b.com/1.png" });
158
+ expect(result).toBe('<img src="https://b.com/1.png"><img src="https://b.com/1.png">');
159
+ });
160
+
161
+ test("skips empty keys or values", () => {
162
+ const html = '<img src="https://a.com/1.png">';
163
+ const result = replaceImageUrls(html, { "": "https://b.com/1.png", "https://a.com/1.png": "" });
164
+ expect(result).toBe(html);
165
+ });
166
+ });
167
+
168
+ describe("isRetryableTokenFetchError", () => {
169
+ test("returns true for fetch failed errors", () => {
170
+ expect(isRetryableTokenFetchError(new Error("fetch failed"))).toBe(true);
171
+ expect(isRetryableTokenFetchError(new Error("Request failed: fetch failed ECONNREFUSED"))).toBe(true);
172
+ });
173
+
174
+ test("returns true case-insensitively", () => {
175
+ expect(isRetryableTokenFetchError(new Error("Fetch Failed"))).toBe(true);
176
+ expect(isRetryableTokenFetchError(new Error("FETCH FAILED"))).toBe(true);
177
+ });
178
+
179
+ test("returns false for non-fetch errors", () => {
180
+ expect(isRetryableTokenFetchError(new Error("timeout"))).toBe(false);
181
+ expect(isRetryableTokenFetchError(new Error("unauthorized"))).toBe(false);
182
+ });
183
+
184
+ test("returns false for non-Error values", () => {
185
+ expect(isRetryableTokenFetchError("fetch failed")).toBe(false);
186
+ expect(isRetryableTokenFetchError(null)).toBe(false);
187
+ expect(isRetryableTokenFetchError(undefined)).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe("resolveFilenameFromUrl", () => {
192
+ test("uses filename from URL path", () => {
193
+ expect(resolveFilenameFromUrl("https://cdn.com/photo.png", "image/png", 0)).toBe("photo.png");
194
+ });
195
+
196
+ test("strips query parameters", () => {
197
+ expect(resolveFilenameFromUrl("https://cdn.com/photo.png?v=1&size=large", "image/png", 0)).toBe("photo.png");
198
+ });
199
+
200
+ test("adds extension when missing", () => {
201
+ expect(resolveFilenameFromUrl("https://cdn.com/photo", "image/jpeg", 0)).toBe("photo.jpg");
202
+ });
203
+
204
+ test("corrects mismatched extension", () => {
205
+ expect(resolveFilenameFromUrl("https://cdn.com/photo.gif", "image/png", 0)).toBe("photo.png");
206
+ });
207
+
208
+ test("uses fallback name when URL has no filename", () => {
209
+ expect(resolveFilenameFromUrl("https://cdn.com/", "image/png", 0)).toBe("image_1.png");
210
+ });
211
+
212
+ test("uses index for fallback naming", () => {
213
+ expect(resolveFilenameFromUrl("https://cdn.com/", "image/jpeg", 2)).toBe("image_3.jpg");
214
+ });
215
+ });