@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
package/src/plugins.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { abandon } from "./commands/abandon";
2
+ import { attachBody } from "./commands/attach-body";
3
+ import { attachBodyImages } from "./commands/attach-body-images";
4
+ import { attachNewspicSpec } from "./commands/attach-newspic-spec";
5
+ import { checkpoint } from "./commands/checkpoint";
6
+ import { configCommand } from "./commands/config";
7
+ import { cosUpload } from "./commands/cos-upload";
8
+ import { doctor } from "./commands/doctor";
9
+ import { findRun } from "./commands/find-run";
10
+ import { hermesMetrics } from "./commands/hermes-metrics";
11
+ import { ingestHandoff } from "./commands/ingest-handoff";
12
+ import { imgxCommand } from "./commands/imgx";
13
+ import { init } from "./commands/init";
14
+ import { prepareFinalize } from "./commands/prepare-finalize";
15
+ import { prepare } from "./commands/prepare";
16
+ import { publish } from "./commands/publish";
17
+ import { reconcile } from "./commands/reconcile";
18
+ import { render } from "./commands/render";
19
+ import { reset } from "./commands/reset";
20
+ import { review } from "./commands/review";
21
+ import { status } from "./commands/status";
22
+ import { syncBlog } from "./commands/sync-blog";
23
+ import { tasks } from "./commands/tasks";
24
+ import { wechatExport } from "./commands/wechat-export";
25
+
26
+ export interface CommandDefinition {
27
+ name: string;
28
+ summary: string;
29
+ plugin: string;
30
+ handler: (args: string[]) => Promise<void>;
31
+ }
32
+
33
+ export interface CommandPlugin {
34
+ name: string;
35
+ commands: CommandDefinition[];
36
+ }
37
+
38
+ export function getCommandPlugins(): CommandPlugin[] {
39
+ return [
40
+ {
41
+ name: "workflow",
42
+ commands: [
43
+ { name: "init", summary: "Create run state from intent classification", plugin: "workflow", handler: init },
44
+ { name: "ingest-handoff", summary: "Create or resume a task from a publish_handoff/workflow_handoff JSON file", plugin: "workflow", handler: ingestHandoff },
45
+ { name: "attach-body", summary: "Attach a source body file to a managed task", plugin: "workflow", handler: attachBody },
46
+ { name: "attach-body-images", summary: "Attach body image marker files to a managed task", plugin: "workflow", handler: attachBodyImages },
47
+ { name: "attach-newspic-spec", summary: "Attach or update newspic render intent", plugin: "workflow", handler: attachNewspicSpec },
48
+ { name: "prepare", summary: "Route + author + format + metadata", plugin: "workflow", handler: prepare },
49
+ { name: "prepare-finalize", summary: "Highlight words + asset save", plugin: "workflow", handler: prepareFinalize },
50
+ { name: "render", summary: "Image plan + imgx render", plugin: "workflow", handler: render },
51
+ { name: "publish", summary: "Execute publish routes", plugin: "workflow", handler: publish },
52
+ { name: "reconcile", summary: "Reconcile managed task materials and derived state", plugin: "workflow", handler: reconcile },
53
+ { name: "checkpoint", summary: "Read task state and validate current phase", plugin: "workflow", handler: checkpoint },
54
+ { name: "status", summary: "Read a managed task with gaps and next action", plugin: "workflow", handler: status },
55
+ { name: "find-run", summary: "Find the best matching managed task", plugin: "workflow", handler: findRun },
56
+ { name: "tasks", summary: "List managed tasks in the workspace", plugin: "workflow", handler: tasks },
57
+ { name: "reset", summary: "Reset phases for revision", plugin: "workflow", handler: reset },
58
+ { name: "review", summary: "Update content review status", plugin: "workflow", handler: review },
59
+ { name: "abandon", summary: "Mark one or more tasks as abandoned", plugin: "workflow", handler: abandon },
60
+ ],
61
+ },
62
+ {
63
+ name: "ops",
64
+ commands: [
65
+ { name: "sync-blog", summary: "Copy canonical markdown to the blog repo and publish there", plugin: "ops", handler: syncBlog },
66
+ { name: "imgx", summary: "Run bundled imgx renderer subcommands", plugin: "ops", handler: imgxCommand },
67
+ { name: "wechat-export", summary: "Render markdown to WeChat HTML with bundled preview styles", plugin: "ops", handler: wechatExport },
68
+ { name: "cos-upload", summary: "Upload a local image to configured COS CDN", plugin: "ops", handler: cosUpload },
69
+ { name: "config", summary: "Read or update pipeline config", plugin: "ops", handler: configCommand },
70
+ { name: "doctor", summary: "Inspect resolved paths and provider health", plugin: "ops", handler: doctor },
71
+ { name: "hermes-metrics", summary: "Show Hermes execution metrics per task", plugin: "ops", handler: hermesMetrics },
72
+ ],
73
+ },
74
+ ];
75
+ }
76
+
77
+ export function getCommandRegistry(): Record<string, CommandDefinition> {
78
+ return Object.fromEntries(
79
+ getCommandPlugins()
80
+ .flatMap((plugin) => plugin.commands)
81
+ .map((command) => [command.name, command]),
82
+ );
83
+ }
84
+
85
+ export function formatUsage(): string {
86
+ const sections = getCommandPlugins().map((plugin) => {
87
+ const lines = plugin.commands.map(
88
+ (command) => ` ${command.name.padEnd(18)} ${command.summary}`,
89
+ );
90
+ return [`${plugin.name}:`, ...lines].join("\n");
91
+ });
92
+
93
+ return [
94
+ "Usage: zzhub-pipeline <command> [options]",
95
+ "",
96
+ "Commands:",
97
+ ...sections,
98
+ "",
99
+ "Run 'zzhub-pipeline <command> --help' for command-specific options.",
100
+ ].join("\n");
101
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * profiles.ts — Author selection decision tree
3
+ *
4
+ * Replaces the older LLM-based author-selection step with deterministic rules.
5
+ * Deterministic rules for rewrite_allowed and style_mode.
6
+ */
7
+
8
+ import type {
9
+ ContentOrigin,
10
+ Authoring,
11
+ } from "./state";
12
+
13
+ // ── Decision tree ─────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Resolve authoring rules based on content_origin, style_hint, and user signals.
17
+ *
18
+ * Decision table used by prepare/orchestrator:
19
+ *
20
+ * | content_origin | user signal | rewrite_allowed | style_mode |
21
+ * |---------------|------------------------|-----------------|---------------|
22
+ * | user | 润色/改一下/按我风格 | yes | polish |
23
+ * | user | no style request | no | none |
24
+ * | external | any | yes (forced) | deep_rewrite |
25
+ * | user | style_hint=fact_report | yes | fact_report |
26
+ * | external | style_hint=fact_report | yes | fact_report |
27
+ * | unknown | — | ERROR | ERROR |
28
+ */
29
+ export function resolveAuthoring(params: {
30
+ contentOrigin: ContentOrigin;
31
+ styleHint: string | null;
32
+ hasStyleRequest: boolean;
33
+ }): Authoring {
34
+ const { contentOrigin, styleHint, hasStyleRequest } = params;
35
+
36
+ // fact_report overrides everything when explicitly requested
37
+ if (styleHint === "fact_report") {
38
+ return {
39
+ rewrite_allowed: true,
40
+ style_mode: "fact_report",
41
+ };
42
+ }
43
+
44
+ // external content always requires deep_rewrite
45
+ if (contentOrigin === "external") {
46
+ return {
47
+ rewrite_allowed: true,
48
+ style_mode: "deep_rewrite",
49
+ };
50
+ }
51
+
52
+ // user content with explicit style request
53
+ if (contentOrigin === "user" && hasStyleRequest) {
54
+ return {
55
+ rewrite_allowed: true,
56
+ style_mode: "polish",
57
+ };
58
+ }
59
+
60
+ // user content without style request
61
+ if (contentOrigin === "user") {
62
+ return {
63
+ rewrite_allowed: false,
64
+ style_mode: "none",
65
+ };
66
+ }
67
+
68
+ // unknown content_origin — should not proceed
69
+ // Caller (orchestrator) must confirm with user first
70
+ throw new Error(
71
+ "content_origin is unknown; orchestrator must confirm ownership before prepare",
72
+ );
73
+ }
74
+
75
+ // ── Style request detection ───────────────────────────────────────
76
+
77
+ /**
78
+ * Check if user text contains a style processing request.
79
+ * Keywords: 润色, 改一下, 用我的口吻, 按我的风格, etc.
80
+ */
81
+ const STYLE_KEYWORDS = [
82
+ "润色",
83
+ "改一下",
84
+ "用我的口吻",
85
+ "按我的风格",
86
+ "用我的写法",
87
+ "改写",
88
+ "重写",
89
+ "转成我的风格",
90
+ ];
91
+
92
+ export function hasStyleRequest(text: string): boolean {
93
+ return STYLE_KEYWORDS.some((kw) => text.includes(kw));
94
+ }
95
+
96
+ // ── Profile path resolution ───────────────────────────────────────
97
+
98
+ /**
99
+ * Resolve the rules profile path for a content_profile name.
100
+ * These are the constraint files loaded by the style skill.
101
+ */
102
+ const PROFILE_PATHS: Record<string, string> = {
103
+ user: "zzhub-media-style/references/user-profile.md",
104
+ storytelling: "zzhub-media-style/references/storytelling-profile.md",
105
+ };
106
+
107
+ export function getProfilePath(
108
+ contentProfile: string,
109
+ ): string | null {
110
+ return PROFILE_PATHS[contentProfile] ?? null;
111
+ }
112
+
113
+ // ── Requires derivation ───────────────────────────────────────────
114
+
115
+ /**
116
+ * Derive intent.requires flags from intent fields.
117
+ *
118
+ * Rules (from orchestrator prompt):
119
+ * - research: user asked "先搜/查资料" or material insufficient
120
+ * - style: content_origin=external OR style_hint=fact_report OR style request
121
+ * - render: content_form=newspic OR route.primary=wechat-article
122
+ * - publish: task_kind=publish
123
+ */
124
+ export function deriveRequires(params: {
125
+ taskKind: string;
126
+ contentForm: string;
127
+ contentOrigin: ContentOrigin;
128
+ styleHint: string | null;
129
+ routePrimary: string;
130
+ hasResearchRequest: boolean;
131
+ hasStyleRequest: boolean;
132
+ }): {
133
+ research: boolean;
134
+ style: boolean;
135
+ render: boolean;
136
+ publish: boolean;
137
+ } {
138
+ return {
139
+ research: params.hasResearchRequest,
140
+ style:
141
+ params.contentOrigin === "external" ||
142
+ params.styleHint === "fact_report" ||
143
+ params.hasStyleRequest,
144
+ render:
145
+ params.contentForm === "newspic" ||
146
+ params.routePrimary === "wechat-article",
147
+ publish: params.taskKind === "publish",
148
+ };
149
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { join } from "path";
3
+ import { extractLocalImagePaths, replaceLocalImagePaths } from "./blog";
4
+
5
+ describe("extractLocalImagePaths", () => {
6
+ test("extracts local image paths from markdown", () => {
7
+ const content = "![cover](./cover.jpg)\n\nSome text\n\n![other](images/other.png)";
8
+ const pathMap = extractLocalImagePaths(content, "/workspace/assets");
9
+ expect(pathMap.get("./cover.jpg")).toBe(join("/workspace/assets", "cover.jpg"));
10
+ expect(pathMap.get("images/other.png")).toBe(join("/workspace/assets", "images/other.png"));
11
+ });
12
+
13
+ test("skips http/https URLs", () => {
14
+ const content = "![remote](https://example.com/img.png)\n![local](./local.png)";
15
+ const pathMap = extractLocalImagePaths(content, "/workspace");
16
+ expect(pathMap.size).toBe(1);
17
+ expect(pathMap.has("./local.png")).toBe(true);
18
+ });
19
+
20
+ test("returns empty map for no images", () => {
21
+ expect(extractLocalImagePaths("just text", "/workspace").size).toBe(0);
22
+ });
23
+
24
+ test("handles multiple images", () => {
25
+ const content = "![a](a.png) ![b](b.png) ![c](c.png)";
26
+ const pathMap = extractLocalImagePaths(content, "/base");
27
+ expect(pathMap.size).toBe(3);
28
+ });
29
+ });
30
+
31
+ describe("replaceLocalImagePaths", () => {
32
+ test("replaces local paths with URLs from the map", () => {
33
+ const content = "![cover](./cover.jpg)";
34
+ const urlMap = new Map([["./cover.jpg", "https://cdn.com/cover.jpg"]]);
35
+ const result = replaceLocalImagePaths(content, urlMap);
36
+ expect(result).toBe("![cover](https://cdn.com/cover.jpg)");
37
+ });
38
+
39
+ test("leaves unmapped images unchanged", () => {
40
+ const content = "![a](./a.png) ![b](./b.png)";
41
+ const urlMap = new Map([["./a.png", "https://cdn.com/a.png"]]);
42
+ const result = replaceLocalImagePaths(content, urlMap);
43
+ expect(result).toBe("![a](https://cdn.com/a.png) ![b](./b.png)");
44
+ });
45
+
46
+ test("leaves http URLs untouched", () => {
47
+ const content = "![remote](https://example.com/img.png)";
48
+ const urlMap = new Map<string, string>();
49
+ expect(replaceLocalImagePaths(content, urlMap)).toBe(content);
50
+ });
51
+
52
+ test("preserves alt text", () => {
53
+ const content = "![my alt text](./img.png)";
54
+ const urlMap = new Map([["./img.png", "https://cdn.com/img.png"]]);
55
+ const result = replaceLocalImagePaths(content, urlMap);
56
+ expect(result).toBe("![my alt text](https://cdn.com/img.png)");
57
+ });
58
+ });
@@ -0,0 +1,154 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { join, resolve } from "path";
3
+ import { PipelineConfig, ResolvedWorkspacePaths } from "../config";
4
+ import { spawnSync } from "../spawn";
5
+ import { PublishResult, WorkflowState } from "../state";
6
+ import { stripFrontmatter } from "../text";
7
+ import { uploadFileToCos } from "./cos";
8
+
9
+ interface BlogPublishContext {
10
+ state: WorkflowState;
11
+ dryRun: boolean;
12
+ config: PipelineConfig;
13
+ workspacePaths: ResolvedWorkspacePaths;
14
+ }
15
+
16
+ // ![alt](path) — skips http(s):// URLs, captures [1]=alt [2]=rawPath
17
+ const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g;
18
+
19
+ export function extractLocalImagePaths(content: string, assetPath: string): Map<string, string> {
20
+ const pathMap = new Map<string, string>();
21
+ for (const match of content.matchAll(LOCAL_IMAGE_RE)) {
22
+ const rawPath = match[2].trim();
23
+ pathMap.set(rawPath, resolve(assetPath, rawPath));
24
+ }
25
+ return pathMap;
26
+ }
27
+
28
+ export function replaceLocalImagePaths(content: string, urlMap: Map<string, string>): string {
29
+ return content.replace(LOCAL_IMAGE_RE, (original, alt: string, rawPath: string) => {
30
+ const url = urlMap.get(rawPath.trim());
31
+ return url ? `![${alt}](${url})` : original;
32
+ });
33
+ }
34
+
35
+ async function uploadBlogImages(
36
+ content: string,
37
+ assetPath: string,
38
+ slug: string,
39
+ config: PipelineConfig,
40
+ dryRun: boolean,
41
+ ): Promise<{ content: string; uploadedCount: number }> {
42
+ const pathMap = extractLocalImagePaths(content, assetPath);
43
+ if (pathMap.size === 0) {
44
+ return { content, uploadedCount: 0 };
45
+ }
46
+
47
+ const cosPat = config.cos.pat;
48
+ if (!cosPat) {
49
+ throw new Error("No COS PAT configured. Set cos.pat in config.");
50
+ }
51
+
52
+ const folder = `blog/${slug}`;
53
+ const urlMap = new Map<string, string>();
54
+
55
+ for (const [rawPath, absPath] of pathMap) {
56
+ const filename = absPath.split("/").pop()!;
57
+ if (dryRun) {
58
+ console.error(`[dry-run] COS upload: ${absPath} -> ${folder}/${filename}`);
59
+ urlMap.set(rawPath, `${config.cos.publicBaseUrl}/${folder}/${filename}`);
60
+ continue;
61
+ }
62
+
63
+ const result = await uploadFileToCos({
64
+ localPath: absPath,
65
+ folder,
66
+ baseUrl: config.cos.baseUrl || config.wx.baseUrl,
67
+ cosPat,
68
+ publicBaseUrl: config.cos.publicBaseUrl,
69
+ });
70
+ urlMap.set(rawPath, result.url);
71
+ }
72
+
73
+ return {
74
+ content: replaceLocalImagePaths(content, urlMap),
75
+ uploadedCount: pathMap.size,
76
+ };
77
+ }
78
+
79
+ export async function publishBlogRoute({
80
+ state,
81
+ dryRun,
82
+ config,
83
+ workspacePaths,
84
+ }: BlogPublishContext): Promise<PublishResult> {
85
+ const postPath = join(state.asset_path, "post.md");
86
+ const postContent = await readFile(postPath, "utf-8");
87
+ const category = "posts";
88
+ const blogRoot = workspacePaths.blogRoot;
89
+ const blogPostPath = join(blogRoot, "content", category, `${state.metadata.slug}.md`);
90
+
91
+ const lines = ["---"];
92
+ lines.push(`title: "${state.metadata.title.replace(/"/g, '\\"')}"`);
93
+ lines.push(`date: ${state.metadata.date}`);
94
+ if (state.metadata.description) {
95
+ lines.push(`description: "${state.metadata.description.replace(/"/g, '\\"')}"`);
96
+ }
97
+ if (state.metadata.tags.length > 0) {
98
+ lines.push("tags:");
99
+ for (const tag of state.metadata.tags) {
100
+ lines.push(` - ${tag}`);
101
+ }
102
+ }
103
+ const author = state.route.content_profile || "default";
104
+ lines.push(`author: ${author}`);
105
+ lines.push("---");
106
+
107
+ const bodyWithFrontmatter = `${lines.join("\n")}\n\n${stripFrontmatter(postContent)}`;
108
+
109
+ const { content: blogContent, uploadedCount } = await uploadBlogImages(
110
+ bodyWithFrontmatter,
111
+ state.asset_path,
112
+ state.metadata.slug,
113
+ config,
114
+ dryRun,
115
+ );
116
+
117
+ if (dryRun) {
118
+ console.error(`[dry-run] Blog: ${uploadedCount} image(s) would be uploaded to COS`);
119
+ console.error(`[dry-run] Blog: write to ${blogPostPath}`);
120
+ console.error(`[dry-run] Blog: cd ${blogRoot} && ${config.commands.blogPublish.join(" ")}`);
121
+ return {
122
+ route: "blog",
123
+ status: "skipped",
124
+ detail: "dry-run",
125
+ published_at: null,
126
+ content_version: state.artifacts.content_version,
127
+ render_version: state.artifacts.render_version,
128
+ };
129
+ }
130
+
131
+ await mkdir(join(blogRoot, "content", category), { recursive: true });
132
+ await writeFile(blogPostPath, blogContent, "utf-8");
133
+ const result = spawnSync(config.commands.blogPublish, { cwd: blogRoot });
134
+
135
+ if (result.exitCode !== 0) {
136
+ return {
137
+ route: "blog",
138
+ status: "failed",
139
+ detail: `blog publish command failed: ${config.commands.blogPublish.join(" ")}`,
140
+ published_at: null,
141
+ content_version: state.artifacts.content_version,
142
+ render_version: state.artifacts.render_version,
143
+ };
144
+ }
145
+
146
+ return {
147
+ route: "blog",
148
+ status: "success",
149
+ detail: null,
150
+ published_at: new Date().toISOString(),
151
+ content_version: state.artifacts.content_version,
152
+ render_version: state.artifacts.render_version,
153
+ };
154
+ }
@@ -0,0 +1,183 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { basename } from "path";
3
+ import COS from "cos-nodejs-sdk-v5";
4
+
5
+ const STS_PATH = "/api/v1/upload/cos";
6
+ const STS_TIMEOUT = 30000;
7
+ const UPLOAD_TIMEOUT = 120000;
8
+
9
+ const EXTENSION_CONTENT_TYPES = new Map<string, string>([
10
+ [".jpg", "image/jpeg"],
11
+ [".jpeg", "image/jpeg"],
12
+ [".png", "image/png"],
13
+ [".gif", "image/gif"],
14
+ [".webp", "image/webp"],
15
+ ]);
16
+
17
+ export interface CosUploadResult {
18
+ localPath: string;
19
+ key: string;
20
+ url: string;
21
+ }
22
+
23
+ interface StsPayload {
24
+ TmpSecretId: string;
25
+ TmpSecretKey: string;
26
+ SessionToken: string;
27
+ StartTime: number;
28
+ ExpiredTime: number;
29
+ Bucket: string;
30
+ Region: string;
31
+ Key: string;
32
+ }
33
+
34
+ interface StsApiResponse {
35
+ code?: number;
36
+ message?: string;
37
+ data?: {
38
+ TmpSecretId?: string;
39
+ TmpSecretKey?: string;
40
+ SessionToken?: string;
41
+ StartTime?: number;
42
+ ExpiredTime?: number;
43
+ Bucket?: string;
44
+ Region?: string;
45
+ Key?: string;
46
+ };
47
+ }
48
+
49
+ async function requestJson(
50
+ url: string,
51
+ options: { method: string; headers: Record<string, string>; body?: unknown },
52
+ timeout: number,
53
+ ): Promise<unknown> {
54
+ const controller = new AbortController();
55
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
56
+ try {
57
+ const response = await fetch(url, {
58
+ method: options.method,
59
+ headers: options.headers,
60
+ body: options.body ? JSON.stringify(options.body) : undefined,
61
+ signal: controller.signal,
62
+ });
63
+ const text = await response.text();
64
+ if (!response.ok) {
65
+ throw new Error(`COS STS request failed: HTTP ${response.status} — ${text}`);
66
+ }
67
+ return text ? JSON.parse(text) : {};
68
+ } finally {
69
+ clearTimeout(timeoutId);
70
+ }
71
+ }
72
+
73
+ async function requestSts(
74
+ baseUrl: string,
75
+ pat: string,
76
+ filename: string,
77
+ folder: string,
78
+ ): Promise<StsPayload> {
79
+ const response = (await requestJson(
80
+ `${baseUrl.replace(/\/$/, "")}${STS_PATH}`,
81
+ {
82
+ method: "POST",
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ Authorization: `Bearer ${pat}`,
86
+ },
87
+ body: { filename, folder: { name: folder } },
88
+ },
89
+ STS_TIMEOUT,
90
+ )) as StsApiResponse;
91
+
92
+ const d = response?.data;
93
+ if (
94
+ !d?.TmpSecretId ||
95
+ !d?.TmpSecretKey ||
96
+ !d?.SessionToken ||
97
+ !d?.Bucket ||
98
+ !d?.Region ||
99
+ !d?.Key ||
100
+ d.StartTime === undefined ||
101
+ d.ExpiredTime === undefined
102
+ ) {
103
+ throw new Error(`COS STS response incomplete: ${JSON.stringify(response)}`);
104
+ }
105
+
106
+ return {
107
+ TmpSecretId: d.TmpSecretId,
108
+ TmpSecretKey: d.TmpSecretKey,
109
+ SessionToken: d.SessionToken,
110
+ StartTime: d.StartTime,
111
+ ExpiredTime: d.ExpiredTime,
112
+ Bucket: d.Bucket,
113
+ Region: d.Region,
114
+ Key: d.Key,
115
+ };
116
+ }
117
+
118
+ export async function uploadFileToCos(input: {
119
+ localPath: string;
120
+ folder: string;
121
+ baseUrl: string;
122
+ cosPat: string;
123
+ publicBaseUrl: string;
124
+ }): Promise<CosUploadResult> {
125
+ const { localPath, folder, baseUrl, cosPat, publicBaseUrl } = input;
126
+
127
+ if (!existsSync(localPath)) {
128
+ throw new Error(`File not found: ${localPath}`);
129
+ }
130
+
131
+ const filename = basename(localPath);
132
+ const ext = filename.toLowerCase().match(/\.[^.]+$/)?.[0] ?? "";
133
+ const contentType = EXTENSION_CONTENT_TYPES.get(ext);
134
+ if (!contentType) {
135
+ throw new Error(
136
+ `Unsupported image type: ${filename}. Supported: ${[...EXTENSION_CONTENT_TYPES.keys()].join(", ")}`,
137
+ );
138
+ }
139
+
140
+ const sts = await requestSts(baseUrl, cosPat, filename, folder);
141
+ const buffer = readFileSync(localPath);
142
+
143
+ const cos = new COS({
144
+ SecretId: sts.TmpSecretId,
145
+ SecretKey: sts.TmpSecretKey,
146
+ SecurityToken: sts.SessionToken,
147
+ });
148
+
149
+ await new Promise<void>((resolve, reject) => {
150
+ let settled = false;
151
+ const timeoutId = setTimeout(() => {
152
+ if (settled) return;
153
+ settled = true;
154
+ reject(new Error(`COS upload timed out: ${filename}`));
155
+ }, UPLOAD_TIMEOUT);
156
+
157
+ cos.putObject(
158
+ {
159
+ Bucket: sts.Bucket,
160
+ Region: sts.Region,
161
+ Key: sts.Key,
162
+ Body: buffer,
163
+ ContentType: contentType,
164
+ },
165
+ (err) => {
166
+ if (settled) return;
167
+ settled = true;
168
+ clearTimeout(timeoutId);
169
+ if (err) {
170
+ reject(new Error(`COS upload failed: ${err.message ?? JSON.stringify(err)}`));
171
+ } else {
172
+ resolve();
173
+ }
174
+ },
175
+ );
176
+ });
177
+
178
+ return {
179
+ localPath,
180
+ key: sts.Key,
181
+ url: `${publicBaseUrl}/${sts.Key}`,
182
+ };
183
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getPublishProvider, listPublishProviders } from "./index";
3
+
4
+ describe("listPublishProviders", () => {
5
+ test("returns registered routes", () => {
6
+ const routes = listPublishProviders();
7
+ expect(routes).toContain("wechat-article");
8
+ expect(routes).toContain("wechat-newspic");
9
+ });
10
+ });
11
+
12
+ describe("getPublishProvider", () => {
13
+ test("returns a function for wechat-article", () => {
14
+ const provider = getPublishProvider("wechat-article");
15
+ expect(typeof provider).toBe("function");
16
+ });
17
+
18
+ test("returns a function for wechat-newspic", () => {
19
+ const provider = getPublishProvider("wechat-newspic");
20
+ expect(typeof provider).toBe("function");
21
+ });
22
+
23
+ test("throws for unregistered route", () => {
24
+ expect(() => getPublishProvider("blog" as never)).toThrow();
25
+ });
26
+ });