@zzclub/pipeline 0.2.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 -432
  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,159 @@
1
+ /**
2
+ * reset — Reset phases for revision re-entry.
3
+ *
4
+ * Implements the revision protocol from revision-protocol.md.
5
+ *
6
+ * Usage:
7
+ * zzhub-pipeline reset --state /path/to/state.json --mode <mode>
8
+ *
9
+ * Modes:
10
+ * content — Reset prepare+render+publish (for re-writing body)
11
+ * redo.style — Reset prepare+render+publish, start from style step
12
+ * redo.format — Reset prepare+render+publish, start from format step
13
+ * redo.metadata — Reset prepare+render+publish, start from metadata step
14
+ * redo.route — Reset prepare+render+publish, start from route step
15
+ * render — Reset render+publish (for re-generating images)
16
+ * publish — Reset publish only (for re-publishing)
17
+ * full — Mark current as abandoned, prepare for full re-run
18
+ *
19
+ * Output: Updated state JSON summary.
20
+ */
21
+
22
+ import { parseArgs, requireArg } from "../args";
23
+ import { printResult, renderReset } from "../output";
24
+ import { readState, writeState, defaultContentReview } from "../state";
25
+
26
+ type ResetMode =
27
+ | "content"
28
+ | "redo.style"
29
+ | "redo.format"
30
+ | "redo.metadata"
31
+ | "redo.route"
32
+ | "render"
33
+ | "publish"
34
+ | "full";
35
+
36
+ const VALID_MODES: ResetMode[] = [
37
+ "content",
38
+ "redo.style",
39
+ "redo.format",
40
+ "redo.metadata",
41
+ "redo.route",
42
+ "render",
43
+ "publish",
44
+ "full",
45
+ ];
46
+
47
+ export async function reset(args: string[]): Promise<void> {
48
+ const parsed = parseArgs(args);
49
+
50
+ if (parsed.help) {
51
+ console.log(`
52
+ Usage: zzhub-pipeline reset [options]
53
+
54
+ Options:
55
+ --state Path to state JSON (required)
56
+ --mode Reset mode (required): ${VALID_MODES.join(" | ")}
57
+
58
+ Modes:
59
+ content Reset all phases (for body rewrite, requires writer re-run)
60
+ redo.style From style step (skip writer)
61
+ redo.format From format step
62
+ redo.metadata From metadata step
63
+ redo.route From route step
64
+ render Reset render+publish (for image re-generation)
65
+ publish Reset publish only (for re-publish)
66
+ full Abandon current, prepare for full re-run
67
+ `.trim());
68
+ return;
69
+ }
70
+
71
+ const statePath = requireArg(parsed, "state", "state JSON path");
72
+ const mode = requireArg(parsed, "mode", "reset mode") as ResetMode;
73
+
74
+ if (!VALID_MODES.includes(mode)) {
75
+ throw new Error(
76
+ `Invalid reset mode: ${mode}. Valid modes: ${VALID_MODES.join(", ")}`,
77
+ );
78
+ }
79
+
80
+ const state = await readState(statePath);
81
+
82
+ switch (mode) {
83
+ case "content":
84
+ case "redo.style":
85
+ case "redo.format":
86
+ case "redo.metadata":
87
+ case "redo.route":
88
+ // Reset prepare + render + publish
89
+ state.phase.prepare = { status: "pending", error: null };
90
+ state.phase.render = { status: "pending", error: null };
91
+ state.phase.publish = { status: "pending", error: null };
92
+ state.phase.current = "prepare";
93
+ state.mode = "active";
94
+ // Persist redo hint so orchestrator can recover start step even after context loss
95
+ state.redo_hint =
96
+ mode === "redo.style"
97
+ ? "style"
98
+ : mode === "redo.format"
99
+ ? "format"
100
+ : mode === "redo.metadata"
101
+ ? "asset-meta"
102
+ : mode === "redo.route"
103
+ ? "channel-route"
104
+ : "writer"; // content
105
+ // Reset content_review for content rewrite (writer will produce new body)
106
+ if (mode === "content") {
107
+ state.content_review = defaultContentReview();
108
+ }
109
+ break;
110
+
111
+ case "render":
112
+ // Reset render + publish, keep prepare done
113
+ state.phase.render = { status: "pending", error: null };
114
+ state.phase.publish = { status: "pending", error: null };
115
+ state.phase.current = "render";
116
+ state.mode = "active";
117
+ state.redo_hint = null;
118
+ // Reset image plan status
119
+ if (state.images.plan.needed) {
120
+ state.images.plan.status = "planned";
121
+ }
122
+ break;
123
+
124
+ case "publish":
125
+ // Reset publish only
126
+ state.phase.publish = { status: "pending", error: null };
127
+ state.phase.current = "publish";
128
+ state.mode = "active";
129
+ state.redo_hint = null;
130
+ // Don't change content_version or render_version
131
+ break;
132
+
133
+ case "full":
134
+ // Mark current state as failed/abandoned
135
+ state.mode = "failed";
136
+ state.phase.current = "failed";
137
+ state.content_review = defaultContentReview();
138
+ state.redo_hint = null;
139
+ break;
140
+ }
141
+
142
+ await writeState(statePath, state);
143
+
144
+ const output = {
145
+ state_path: statePath,
146
+ reset_mode: mode,
147
+ mode: state.mode,
148
+ phase: {
149
+ current: state.phase.current,
150
+ prepare: state.phase.prepare.status,
151
+ render: state.phase.render.status,
152
+ publish: state.phase.publish.status,
153
+ },
154
+ // redo_hint is persisted in state; also echoed here for convenience
155
+ start_step: state.redo_hint ?? null,
156
+ };
157
+
158
+ printResult(output, renderReset);
159
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * review — Update content_review state after orchestrator quality check.
3
+ *
4
+ * The actual quality review is done by the orchestrator (LLM judgment).
5
+ * This command only persists the review decision to the state file.
6
+ *
7
+ * Usage:
8
+ * zzhub-pipeline review --state /path/to/state.json --status passed
9
+ * zzhub-pipeline review --state /path/to/state.json --status needs_revision --feedback "AI味过重,建议..."
10
+ *
11
+ * Statuses:
12
+ * passed — Content passed quality check, proceed to next step
13
+ * needs_revision — Content has issues, will be sent back to Writer
14
+ *
15
+ * Output: Updated content_review state as JSON.
16
+ */
17
+
18
+ import { parseArgs, requireArg, optionalArg } from "../args";
19
+ import { printResult, renderReview } from "../output";
20
+ import { readState, writeState, type ContentReviewStatus } from "../state";
21
+
22
+ const VALID_STATUSES: ContentReviewStatus[] = [
23
+ "passed",
24
+ "needs_revision",
25
+ ];
26
+
27
+ export async function review(args: string[]): Promise<void> {
28
+ const parsed = parseArgs(args);
29
+
30
+ if (parsed.help) {
31
+ console.log(`
32
+ Usage: zzhub-pipeline review [options]
33
+
34
+ Options:
35
+ --state Path to state JSON (required)
36
+ --status passed | needs_revision (required)
37
+ --feedback Revision feedback for Writer (required when status=needs_revision)
38
+
39
+ Statuses:
40
+ passed Content passed quality check
41
+ needs_revision Content has issues, send back to Writer with feedback
42
+ `.trim());
43
+ return;
44
+ }
45
+
46
+ const statePath = requireArg(parsed, "state", "state JSON path");
47
+ const status = requireArg(parsed, "status", "review status") as ContentReviewStatus;
48
+ const feedback = optionalArg(parsed, "feedback") ?? null;
49
+
50
+ if (!VALID_STATUSES.includes(status)) {
51
+ throw new Error(
52
+ `Invalid review status: ${status}. Valid statuses: ${VALID_STATUSES.join(", ")}`,
53
+ );
54
+ }
55
+
56
+ if (status === "needs_revision" && !feedback) {
57
+ throw new Error(
58
+ "Feedback is required when status is needs_revision (--feedback \"...\")",
59
+ );
60
+ }
61
+
62
+ const state = await readState(statePath);
63
+
64
+ state.content_review = {
65
+ status,
66
+ feedback: status === "needs_revision" ? feedback : null,
67
+ };
68
+
69
+ await writeState(statePath, state);
70
+
71
+ const output = {
72
+ state_path: statePath,
73
+ content_review: state.content_review,
74
+ };
75
+
76
+ printResult(output, renderReview);
77
+ }
@@ -0,0 +1,68 @@
1
+ import { parseArgs, optionalArg, requireArg } from "../args";
2
+ import { printResult, renderTaskShape } from "../output";
3
+ import { findTask, getTaskByStatePath } from "../task-manager";
4
+ import { buildAgentTaskShape, parseTaskViewMode, renderTaskStatusMarkdown } from "../task-views";
5
+
6
+ export async function status(args: string[]): Promise<void> {
7
+ const parsed = parseArgs(args);
8
+ const positional = JSON.parse(String(parsed._ ?? "[]")) as string[];
9
+
10
+ if (parsed.help) {
11
+ console.log(`
12
+ Usage: zzhub-pipeline status [options]
13
+
14
+ Options:
15
+ --state Path to state JSON
16
+ --run-id Run id to resolve inside workspace
17
+ --workspace Workspace root (optional; when used alone, resolves the current active task)
18
+ --full Include the full state payload
19
+ --view json | markdown | agent | agent-json (default: json)
20
+ `.trim());
21
+ return;
22
+ }
23
+
24
+ if (positional.length > 0) {
25
+ throw new Error(`Unexpected positional arguments: ${positional.join(" ")}. Use --state or --run-id.`);
26
+ }
27
+
28
+ const statePath = optionalArg(parsed, "state");
29
+ const workspace = optionalArg(parsed, "workspace");
30
+ const includeFull = parsed.full === true || parsed.full === "true";
31
+ const view = parseTaskViewMode(optionalArg(parsed, "view"));
32
+
33
+ const task = statePath
34
+ ? await getTaskByStatePath(statePath)
35
+ : await findTask(
36
+ workspace,
37
+ optionalArg(parsed, "run-id")
38
+ ? { run_id: requireArg(parsed, "run-id", "run id") }
39
+ : { active_only: true },
40
+ );
41
+
42
+ if (!task) {
43
+ throw new Error("Task not found");
44
+ }
45
+
46
+ if (view === "agent-json") {
47
+ console.log(JSON.stringify(buildAgentTaskShape(task), null, 2));
48
+ return;
49
+ }
50
+
51
+ if (view !== "json") {
52
+ console.log(renderTaskStatusMarkdown(task, view));
53
+ return;
54
+ }
55
+
56
+ printResult(
57
+ includeFull
58
+ ? task
59
+ : {
60
+ summary: task.summary,
61
+ validation: task.validation,
62
+ gaps: task.gaps,
63
+ blockers: task.blockers,
64
+ next_action: task.next_action,
65
+ },
66
+ renderTaskShape,
67
+ );
68
+ }
@@ -0,0 +1,39 @@
1
+ import { parseArgs, requireArg, flagArg } from "../args";
2
+ import { printResult, renderSyncBlog } from "../output";
3
+ import { loadConfig, resolveWorkspacePaths } from "../config";
4
+ import { readState } from "../state";
5
+ import { publishBlogRoute } from "../providers/blog";
6
+
7
+ export async function syncBlog(args: string[]): Promise<void> {
8
+ const parsed = parseArgs(args);
9
+
10
+ if (parsed.help) {
11
+ console.log(`
12
+ Usage: zzhub-pipeline sync-blog [options]
13
+
14
+ Options:
15
+ --state Path to state JSON (required)
16
+ --dry-run Print the target copy/publish command without executing
17
+ `.trim());
18
+ return;
19
+ }
20
+
21
+ const statePath = requireArg(parsed, "state", "state JSON path");
22
+ const dryRun = flagArg(parsed, "dry-run");
23
+ const state = await readState(statePath);
24
+
25
+ if (!state.asset_path) {
26
+ throw new Error("asset_path not set. Run prepare-finalize first.");
27
+ }
28
+
29
+ const config = loadConfig();
30
+ const workspacePaths = resolveWorkspacePaths(state.workspace_root, config);
31
+ const result = await publishBlogRoute({
32
+ state,
33
+ dryRun,
34
+ config,
35
+ workspacePaths,
36
+ });
37
+
38
+ printResult(result, renderSyncBlog);
39
+ }
@@ -0,0 +1,50 @@
1
+ import { parseArgs, optionalArg } from "../args";
2
+ import { printResult, renderTasks } from "../output";
3
+ import { filterActiveTasks, listTasks } from "../task-manager";
4
+ import { buildAgentTasksShape, parseTaskViewMode, renderTasksView } from "../task-views";
5
+
6
+ export async function tasks(args: string[]): Promise<void> {
7
+ const parsed = parseArgs(args);
8
+
9
+ if (parsed.help) {
10
+ console.log(`
11
+ Usage: zzhub-pipeline tasks [options]
12
+
13
+ Options:
14
+ --workspace Workspace root (optional)
15
+ --active Only return in-flight tasks (active/handoff)
16
+ --full Include the full state payload for every task
17
+ --view json | markdown | agent | agent-json (default: json)
18
+ `.trim());
19
+ return;
20
+ }
21
+
22
+ const workspace = optionalArg(parsed, "workspace");
23
+ const includeFull = parsed.full === true || parsed.full === "true";
24
+ const activeOnly = parsed.active === true || parsed.active === "true";
25
+ const view = parseTaskViewMode(optionalArg(parsed, "view"));
26
+ const allTasks = await listTasks(workspace);
27
+ const visibleTasks = activeOnly ? filterActiveTasks(allTasks) : allTasks;
28
+
29
+ if (view === "agent-json") {
30
+ console.log(JSON.stringify(buildAgentTasksShape(visibleTasks), null, 2));
31
+ return;
32
+ }
33
+
34
+ if (view !== "json") {
35
+ console.log(renderTasksView(visibleTasks, view));
36
+ return;
37
+ }
38
+
39
+ printResult(
40
+ includeFull
41
+ ? visibleTasks
42
+ : visibleTasks.map((task) => ({
43
+ summary: task.summary,
44
+ validation: task.validation,
45
+ blockers: task.blockers,
46
+ next_action: task.next_action,
47
+ })),
48
+ renderTasks,
49
+ );
50
+ }
@@ -0,0 +1,68 @@
1
+ import { optionalArg, parseArgs, requireArg } from "../args";
2
+ import { printResult, renderWechatExport } from "../output";
3
+ import { loadConfig } from "../config";
4
+ import { resolveMarkdownRenderer } from "../adapter-loader";
5
+
6
+ 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.
11
+
12
+ Example custom.css:
13
+
14
+ /* Tweak headings */
15
+ h1 { color: #1a1a2e; font-size: 22px; }
16
+ h2 { color: #333; border-bottom: 1px solid #eee; }
17
+
18
+ /* Wider paragraph spacing */
19
+ p { margin: 1.2em 0; line-height: 2; }
20
+
21
+ /* Subtle blockquote */
22
+ blockquote {
23
+ border-left: 3px solid #b35d85;
24
+ background: #fdf6f9;
25
+ padding: 8px 14px;
26
+ }
27
+ `.trim();
28
+
29
+ export async function wechatExport(args: string[]): Promise<void> {
30
+ const parsed = parseArgs(args);
31
+
32
+ if (parsed.help) {
33
+ console.log(`
34
+ Usage: zzhub-pipeline wechat-export [options]
35
+
36
+ Options:
37
+ --markdown Path to markdown file (required)
38
+ --out Path to exported html file (required)
39
+ --account Account/theme key (optional; default: default)
40
+ --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)
43
+
44
+ ${CSS_DEMO}
45
+ `.trim());
46
+ return;
47
+ }
48
+
49
+ const markdownPath = requireArg(parsed, "markdown", "markdown file path");
50
+ const outPath = requireArg(parsed, "out", "html output path");
51
+ const account = optionalArg(parsed, "account") ?? "default";
52
+ const title = optionalArg(parsed, "title");
53
+ const previewShellOutPath = optionalArg(parsed, "preview-shell-out");
54
+ const customCss = optionalArg(parsed, "custom-css");
55
+
56
+ const config = loadConfig();
57
+ const markdownRenderer = await resolveMarkdownRenderer(config);
58
+ const result = await markdownRenderer.render({
59
+ markdownPath,
60
+ outPath,
61
+ account,
62
+ title,
63
+ previewShellOutPath,
64
+ customCss,
65
+ });
66
+
67
+ printResult(result, renderWechatExport);
68
+ }
package/src/config.ts ADDED
Binary file
@@ -0,0 +1,77 @@
1
+ import { join } from "node:path";
2
+ import {
3
+ escapeHtml,
4
+ FONTS_DIR,
5
+ ICONS_DIR,
6
+ readUtf8,
7
+ renderTemplate,
8
+ resolveInputPath,
9
+ screenshotHtml,
10
+ TEMPLATES_DIR,
11
+ } from "./runtime";
12
+
13
+ export type AsciiPortraitOptions = {
14
+ avatarPath: string;
15
+ bg: string;
16
+ chars: string;
17
+ columns: number;
18
+ fontSize: number;
19
+ lineHeight: number;
20
+ title?: string;
21
+ footer?: string;
22
+ iconPath?: string;
23
+ width?: number;
24
+ height?: number;
25
+ templateName?: "ascii-portrait-3-4" | "ascii-portrait-tile";
26
+ };
27
+
28
+ function serializeConfig(options: AsciiPortraitOptions): string {
29
+ return JSON.stringify({
30
+ avatarPath: options.avatarPath,
31
+ bgColor: options.bg,
32
+ chars: options.chars,
33
+ columns: options.columns,
34
+ fontSize: options.fontSize,
35
+ lineHeight: options.lineHeight,
36
+ })
37
+ .replaceAll("<", "\\u003c")
38
+ .replaceAll(">", "\\u003e")
39
+ .replaceAll("&", "\\u0026")
40
+ .replaceAll("</script", "<\\/script");
41
+ }
42
+
43
+ export function buildAsciiPortraitHtml(options: AsciiPortraitOptions): string {
44
+ const templateName = options.templateName ?? "ascii-portrait-3-4";
45
+ const template = readUtf8(join(TEMPLATES_DIR, `${templateName}.html`));
46
+ const width = options.width ?? 900;
47
+ const height = options.height ?? 1200;
48
+
49
+ return renderTemplate(template, {
50
+ "{{BG_COLOR}}": options.bg,
51
+ "{{TITLE}}": escapeHtml(options.title ?? "ASCII Portrait"),
52
+ "{{FOOTER_TEXT}}": escapeHtml(options.footer ?? "公众号 · 早早集市"),
53
+ "{{ICON_PATH}}": resolveInputPath(options.iconPath ?? join(ICONS_DIR, "logo.png")),
54
+ "{{AVATAR_PATH}}": resolveInputPath(options.avatarPath),
55
+ "{{ASCII_CONFIG_JSON}}": serializeConfig(options),
56
+ "{{FONT_PATH}}": join(FONTS_DIR, "AlimamaShuHeiTi-Bold.ttf"),
57
+ "{{BODY_FONT_PATH}}": join(FONTS_DIR, "LXGWWenKai-Regular.ttf"),
58
+ "{{CANVAS_WIDTH}}": String(width),
59
+ "{{CANVAS_HEIGHT}}": String(height),
60
+ });
61
+ }
62
+
63
+ export function renderAsciiPortraitPng(options: AsciiPortraitOptions & {
64
+ chromePath: string;
65
+ outPath: string;
66
+ }): void {
67
+ const width = options.width ?? 900;
68
+ const height = options.height ?? 1200;
69
+ screenshotHtml({
70
+ chromePath: options.chromePath,
71
+ html: buildAsciiPortraitHtml(options),
72
+ outPath: options.outPath,
73
+ width,
74
+ height,
75
+ virtualTimeBudgetMs: 1800,
76
+ });
77
+ }
Binary file
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <!-- Zigzag S logo - top chevron -->
3
+ <path d="M72 20 L30 44" stroke="#22a854" stroke-width="16" stroke-linecap="round" fill="none"/>
4
+ <!-- Zigzag S logo - bottom chevron -->
5
+ <path d="M30 56 L72 80" stroke="#22a854" stroke-width="16" stroke-linecap="round" fill="none"/>
6
+ <!-- Small connecting gap indicator -->
7
+ <line x1="30" y1="44" x2="30" y2="56" stroke="#22a854" stroke-width="4" stroke-linecap="round" fill="none" opacity="0.5"/>
8
+ </svg>
@@ -112,7 +112,7 @@
112
112
  position: absolute;
113
113
  object-fit: cover;
114
114
  border-radius: var(--image-radius);
115
- box-shadow: 0 16px 30px rgb(27 26 24 / 0.12);
115
+ box-shadow: 0 var(--img-shadow-y, 16px) var(--img-shadow-blur, 30px) rgb(27 26 24 / 0.12);
116
116
  background: #ebe7de;
117
117
  }
118
118
  .body-caption {