@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.
- package/README.md +366 -170
- package/package.json +4 -7
- package/src/adapter-loader.ts +138 -0
- package/src/adapter-types.ts +172 -0
- package/src/adapters/builtin-image-renderer.ts +353 -0
- package/src/adapters/builtin-markdown-renderer.ts +63 -0
- package/src/adapters/index.ts +2 -0
- package/src/args.test.ts +118 -0
- package/src/args.ts +90 -0
- package/src/cli.ts +51 -0
- package/src/commands/abandon.test.ts +104 -0
- package/src/commands/abandon.ts +202 -0
- package/src/commands/attach-body-images.ts +88 -0
- package/src/commands/attach-body.ts +86 -0
- package/src/commands/attach-newspic-spec.ts +41 -0
- package/src/commands/checkpoint.test.ts +91 -0
- package/src/commands/checkpoint.ts +66 -0
- package/src/commands/config.ts +106 -0
- package/src/commands/cos-upload.ts +62 -0
- package/src/commands/doctor.ts +66 -0
- package/src/commands/find-run.ts +55 -0
- package/src/commands/hermes-metrics.ts +226 -0
- package/src/commands/imgx.ts +60 -0
- package/src/commands/ingest-handoff.ts +509 -0
- package/src/commands/init.ts +126 -0
- package/src/commands/prepare-finalize.ts +348 -0
- package/src/commands/prepare.ts +310 -0
- package/src/commands/publish.ts +147 -0
- package/src/commands/reconcile.ts +36 -0
- package/src/commands/render.ts +244 -0
- package/src/commands/reset.test.ts +151 -0
- package/src/commands/reset.ts +159 -0
- package/src/commands/review.ts +77 -0
- package/src/commands/status.ts +68 -0
- package/src/commands/sync-blog.ts +39 -0
- package/src/commands/tasks.ts +50 -0
- package/src/commands/wechat-export.ts +68 -0
- package/src/config.ts +0 -0
- package/src/imgx/ascii-portrait.ts +77 -0
- package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
- package/src/imgx/assets/icons/logo.png +0 -0
- package/src/imgx/assets/icons/logo.svg +8 -0
- package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
- package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
- package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
- package/src/imgx/cli.ts +57 -0
- package/src/imgx/geometry.ts +68 -0
- package/src/imgx/index.ts +5 -0
- package/src/imgx/longform-theme.ts +243 -0
- package/src/imgx/poster-recipe.ts +81 -0
- package/src/imgx/pretext-adapter.ts +14 -0
- package/src/imgx/pretext-runtime.ts +61 -0
- package/src/imgx/references/ascii-portrait-3-4.md +59 -0
- package/src/imgx/references/longform-3-4.md +173 -0
- package/src/imgx/references/poster-3-4.md +87 -0
- package/src/imgx/references/tips-3-4.md +69 -0
- package/src/imgx/references/wechat-cover-split.md +89 -0
- package/src/imgx/references/x-like-posts.md +139 -0
- package/src/imgx/render-article.test.ts +53 -0
- package/src/imgx/render-article.ts +1364 -0
- package/src/imgx/render-ascii-portrait.ts +53 -0
- package/src/imgx/render-card.ts +232 -0
- package/src/imgx/render-x-like-posts.ts +235 -0
- package/src/imgx/runtime.ts +403 -0
- package/src/imgx/typographic-scale.test.ts +56 -0
- package/src/imgx/typographic-scale.ts +44 -0
- package/src/output.ts +632 -0
- package/src/plugins.ts +101 -0
- package/src/profiles.ts +149 -0
- package/src/providers/blog.test.ts +58 -0
- package/src/providers/blog.ts +154 -0
- package/src/providers/cos.ts +183 -0
- package/src/providers/index.test.ts +26 -0
- package/src/providers/index.ts +185 -0
- package/src/providers/wechat.test.ts +215 -0
- package/src/providers/wechat.ts +548 -0
- package/src/routes.ts +287 -0
- package/src/runtime-paths.ts +179 -0
- package/src/schema/config.ts +133 -0
- package/src/schema/state.ts +376 -0
- package/src/spawn.ts +103 -0
- package/src/state.ts +265 -0
- package/src/task-manager.ts +657 -0
- package/src/task-views.ts +222 -0
- package/src/text.test.ts +689 -0
- package/src/text.ts +683 -0
- package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
- package/src/wechat-preview/browser/editor-export.css +241 -0
- package/src/wechat-preview/browser/editor-export.ts +103 -0
- package/src/wechat-preview/frontmatter-handler.ts +73 -0
- package/src/wechat-preview/index.test.ts +154 -0
- package/src/wechat-preview/index.ts +294 -0
- package/src/wechat-preview/themes.ts +141 -0
- package/src/wechat-preview/wechat-formatter.test.ts +288 -0
- package/src/wechat-preview/wechat-formatter.ts +1506 -0
- package/src/wechat-preview.test.ts +73 -0
- package/src/workflow-materials.test.ts +225 -0
- package/src/workflow-materials.ts +217 -0
- package/src/workflow.test.ts +2087 -0
- package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
- package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
- package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
- package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
- package/dist/cli.js +0 -432
- package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
- package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
- package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
- package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
- package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
- package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
- package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
- package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
- package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
- package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
- /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* publish — Execute publish routes.
|
|
3
|
+
*
|
|
4
|
+
* Reads state, determines which routes to publish, executes publish commands,
|
|
5
|
+
* records results. Supports idempotent skipping based on version matching.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* zzhub-pipeline publish \
|
|
9
|
+
* --state /path/to/state.json \
|
|
10
|
+
* [--route wechat-article] (override: only publish this route)
|
|
11
|
+
* [--dry-run]
|
|
12
|
+
*
|
|
13
|
+
* Output: Updated state with publish.results
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parseArgs, requireArg, optionalArg, flagArg } from "../args";
|
|
17
|
+
import { printResult, renderPublish } from "../output";
|
|
18
|
+
import { loadConfig, resolveWorkspacePaths } from "../config";
|
|
19
|
+
import { getPublishProvider } from "../providers";
|
|
20
|
+
import {
|
|
21
|
+
readState,
|
|
22
|
+
validateForPhase,
|
|
23
|
+
writeState,
|
|
24
|
+
type PublishResult,
|
|
25
|
+
type RoutePrimary,
|
|
26
|
+
} from "../state";
|
|
27
|
+
|
|
28
|
+
export async function publish(args: string[]): Promise<void> {
|
|
29
|
+
const parsed = parseArgs(args);
|
|
30
|
+
|
|
31
|
+
if (parsed.help) {
|
|
32
|
+
console.log(`
|
|
33
|
+
Usage: zzhub-pipeline publish [options]
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--state Path to state JSON (required)
|
|
37
|
+
--route Only publish this route (optional; default: all routes)
|
|
38
|
+
--dry-run Print commands without executing (optional)
|
|
39
|
+
`.trim());
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
44
|
+
const routeOverride = optionalArg(parsed, "route") as RoutePrimary | undefined;
|
|
45
|
+
const dryRun = flagArg(parsed, "dry-run");
|
|
46
|
+
|
|
47
|
+
const state = await readState(statePath);
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
const workspacePaths = resolveWorkspacePaths(state.workspace_root, config);
|
|
50
|
+
const validationErrors = validateForPhase(state, "publish");
|
|
51
|
+
|
|
52
|
+
if (!state.asset_path) {
|
|
53
|
+
throw new Error("asset_path not set. Cannot publish.");
|
|
54
|
+
}
|
|
55
|
+
if (validationErrors.length > 0) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Publish validation failed: ${validationErrors.map((item) => `${item.field}: ${item.message}`).join("; ")}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Collect all routes to publish
|
|
62
|
+
const routes: RoutePrimary[] = routeOverride
|
|
63
|
+
? [routeOverride]
|
|
64
|
+
: [state.route.primary, ...state.route.extras];
|
|
65
|
+
|
|
66
|
+
const results: PublishResult[] = [...state.publish.results];
|
|
67
|
+
let hasUnfinishedPublish = false;
|
|
68
|
+
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
// ── Idempotency check ──
|
|
71
|
+
const existing = results.find((r) => r.route === route);
|
|
72
|
+
if (
|
|
73
|
+
existing &&
|
|
74
|
+
existing.status === "success" &&
|
|
75
|
+
existing.content_version === state.artifacts.content_version &&
|
|
76
|
+
existing.render_version === state.artifacts.render_version
|
|
77
|
+
) {
|
|
78
|
+
console.error(`[publish] Skipping ${route}: already published at current version`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const provider = getPublishProvider(route);
|
|
84
|
+
const result = await provider({
|
|
85
|
+
state,
|
|
86
|
+
dryRun,
|
|
87
|
+
config,
|
|
88
|
+
workspacePaths,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Upsert result
|
|
92
|
+
const idx = results.findIndex((r) => r.route === route);
|
|
93
|
+
if (idx >= 0) {
|
|
94
|
+
results[idx] = result;
|
|
95
|
+
} else {
|
|
96
|
+
results.push(result);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.status !== "success" && result.status !== "skipped") {
|
|
100
|
+
hasUnfinishedPublish = true;
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
104
|
+
const failResult: PublishResult = {
|
|
105
|
+
route,
|
|
106
|
+
status: "failed",
|
|
107
|
+
detail,
|
|
108
|
+
published_at: null,
|
|
109
|
+
content_version: state.artifacts.content_version,
|
|
110
|
+
render_version: state.artifacts.render_version,
|
|
111
|
+
};
|
|
112
|
+
const idx = results.findIndex((r) => r.route === route);
|
|
113
|
+
if (idx >= 0) {
|
|
114
|
+
results[idx] = failResult;
|
|
115
|
+
} else {
|
|
116
|
+
results.push(failResult);
|
|
117
|
+
}
|
|
118
|
+
hasUnfinishedPublish = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Update state
|
|
123
|
+
state.publish.results = results;
|
|
124
|
+
|
|
125
|
+
const allDone = routes.every((route) => {
|
|
126
|
+
const r = results.find((x) => x.route === route);
|
|
127
|
+
return r && (r.status === "success" || r.status === "skipped");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (allDone) {
|
|
131
|
+
state.mode = "done";
|
|
132
|
+
state.phase.publish = { status: "done", error: null };
|
|
133
|
+
state.phase.current = "done";
|
|
134
|
+
} else if (hasUnfinishedPublish) {
|
|
135
|
+
state.mode = "active";
|
|
136
|
+
state.phase.publish = { status: "pending", error: null };
|
|
137
|
+
state.phase.current = "publish";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await writeState(statePath, state);
|
|
141
|
+
|
|
142
|
+
printResult({
|
|
143
|
+
publish_results: results,
|
|
144
|
+
mode: state.mode,
|
|
145
|
+
phase: state.phase.current,
|
|
146
|
+
}, renderPublish);
|
|
147
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parseArgs, requireArg } from "../args";
|
|
2
|
+
import { printResult, renderTaskShape } from "../output";
|
|
3
|
+
import { readState, writeState } from "../state";
|
|
4
|
+
import { getTaskByStatePath } from "../task-manager";
|
|
5
|
+
import { reconcileStateArtifacts } from "../workflow-materials";
|
|
6
|
+
|
|
7
|
+
export async function reconcile(args: string[]): Promise<void> {
|
|
8
|
+
const parsed = parseArgs(args);
|
|
9
|
+
|
|
10
|
+
if (parsed.help) {
|
|
11
|
+
console.log(`
|
|
12
|
+
Usage: zzhub-pipeline reconcile [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--state Path to state JSON (required)
|
|
16
|
+
`.trim());
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
21
|
+
const state = await readState(statePath);
|
|
22
|
+
await reconcileStateArtifacts(state);
|
|
23
|
+
await writeState(statePath, state);
|
|
24
|
+
|
|
25
|
+
const task = await getTaskByStatePath(statePath);
|
|
26
|
+
printResult(
|
|
27
|
+
{
|
|
28
|
+
summary: task.summary,
|
|
29
|
+
validation: task.validation,
|
|
30
|
+
gaps: task.gaps,
|
|
31
|
+
blockers: task.blockers,
|
|
32
|
+
next_action: task.next_action,
|
|
33
|
+
},
|
|
34
|
+
renderTaskShape,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render — Execute Render phase: image-plan + adapter-based rendering.
|
|
3
|
+
*
|
|
4
|
+
* 1. Reads state + post.md
|
|
5
|
+
* 2. Runs image-plan logic (template selection, cover_title, output_dir)
|
|
6
|
+
* 3. Invokes image renderer adapter (pluggable via config.plugins)
|
|
7
|
+
* 4. Records render_assets and bumps render_version
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* zzhub-pipeline render \
|
|
11
|
+
* --state /path/to/state.json \
|
|
12
|
+
* [--skip-render] (plan only, don't invoke renderer)
|
|
13
|
+
*
|
|
14
|
+
* Output: Updated state with images.plan, images.render_assets
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFile } from "fs/promises";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
|
|
20
|
+
import { parseArgs, requireArg, flagArg } from "../args";
|
|
21
|
+
import { printResult, renderRender } from "../output";
|
|
22
|
+
import { loadConfig } from "../config";
|
|
23
|
+
import { resolveImageRenderer } from "../adapter-loader";
|
|
24
|
+
import {
|
|
25
|
+
defaultBodyInputs,
|
|
26
|
+
normalizeNewspicRenderSpec,
|
|
27
|
+
readState,
|
|
28
|
+
writeState,
|
|
29
|
+
type NewspicRenderSpec,
|
|
30
|
+
} from "../state";
|
|
31
|
+
import {
|
|
32
|
+
stripFrontmatter,
|
|
33
|
+
generateCoverTitle,
|
|
34
|
+
} from "../text";
|
|
35
|
+
import { getLongformTheme } from "../routes";
|
|
36
|
+
import { collectNewspicRequiredMarkers } from "../workflow-materials";
|
|
37
|
+
|
|
38
|
+
function getNewspicRenderSpec(state: Awaited<ReturnType<typeof readState>>): NewspicRenderSpec {
|
|
39
|
+
const spec = normalizeNewspicRenderSpec(state.intent.newspic_render);
|
|
40
|
+
return {
|
|
41
|
+
...spec,
|
|
42
|
+
min_pages: Math.max(spec.min_pages, spec.pagination_mode === "multi" ? 2 : 1),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function render(args: string[]): Promise<void> {
|
|
47
|
+
const parsed = parseArgs(args);
|
|
48
|
+
|
|
49
|
+
if (parsed.help) {
|
|
50
|
+
console.log(`
|
|
51
|
+
Usage: zzhub-pipeline render [options]
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--state Path to state JSON (required)
|
|
55
|
+
--skip-render Plan only, don't invoke renderer (optional)
|
|
56
|
+
`.trim());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
61
|
+
const skipRender = flagArg(parsed, "skip-render");
|
|
62
|
+
|
|
63
|
+
const state = await readState(statePath);
|
|
64
|
+
|
|
65
|
+
// Validate prerequisites
|
|
66
|
+
if (!state.asset_path) {
|
|
67
|
+
throw new Error("asset_path not set. Run prepare-finalize first.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const postPath = join(state.asset_path, "post.md");
|
|
71
|
+
const postContent = await readFile(postPath, "utf-8");
|
|
72
|
+
const cleanBody = stripFrontmatter(postContent);
|
|
73
|
+
|
|
74
|
+
// ── Image Plan ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const routePrimary = state.route.primary;
|
|
77
|
+
|
|
78
|
+
if (routePrimary === "blog") {
|
|
79
|
+
// Blog doesn't need images by default
|
|
80
|
+
state.images.plan = {
|
|
81
|
+
needed: false,
|
|
82
|
+
template: null,
|
|
83
|
+
cover_template: null,
|
|
84
|
+
cover_title: null,
|
|
85
|
+
output_dir: null,
|
|
86
|
+
preview_required: false,
|
|
87
|
+
status: "skipped",
|
|
88
|
+
};
|
|
89
|
+
state.images.render_assets = [];
|
|
90
|
+
state.phase.render = { status: "done", error: null };
|
|
91
|
+
state.phase.current = state.intent.requires.publish ? "publish" : "done";
|
|
92
|
+
await writeState(statePath, state);
|
|
93
|
+
printResult({
|
|
94
|
+
plan: state.images.plan,
|
|
95
|
+
render_assets: [],
|
|
96
|
+
phase: state.phase.current,
|
|
97
|
+
}, renderRender);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let template: string;
|
|
102
|
+
let coverTemplate: string | null = null;
|
|
103
|
+
let coverTitle: string | null = null;
|
|
104
|
+
let outputDir: string;
|
|
105
|
+
const newspicRenderSpec = getNewspicRenderSpec(state);
|
|
106
|
+
|
|
107
|
+
if (routePrimary === "wechat-article") {
|
|
108
|
+
template = "wechat-cover-split";
|
|
109
|
+
outputDir = join(state.asset_path, "images", "wechat");
|
|
110
|
+
} else {
|
|
111
|
+
// wechat-newspic: determine short vs long
|
|
112
|
+
const isLong = newspicRenderSpec.pagination_mode === "multi";
|
|
113
|
+
|
|
114
|
+
if (isLong) {
|
|
115
|
+
template = "longform-3-4";
|
|
116
|
+
coverTemplate = "poster-3-4";
|
|
117
|
+
coverTitle = generateCoverTitle(state.metadata.title);
|
|
118
|
+
} else {
|
|
119
|
+
template = "poster-3-4";
|
|
120
|
+
coverTitle = generateCoverTitle(state.metadata.title);
|
|
121
|
+
}
|
|
122
|
+
outputDir = join(state.asset_path, "images", "newspic");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
state.images.plan = {
|
|
126
|
+
needed: true,
|
|
127
|
+
template,
|
|
128
|
+
cover_template: coverTemplate,
|
|
129
|
+
cover_title: coverTitle,
|
|
130
|
+
output_dir: outputDir,
|
|
131
|
+
preview_required: false,
|
|
132
|
+
status: "planned",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// For longform, set up body_inputs
|
|
136
|
+
if (template === "longform-3-4") {
|
|
137
|
+
const markers = collectNewspicRequiredMarkers(cleanBody, state);
|
|
138
|
+
const previousInputs =
|
|
139
|
+
state.images.body_inputs.scope === "newspic-longform"
|
|
140
|
+
? state.images.body_inputs
|
|
141
|
+
: null;
|
|
142
|
+
const markerSet = new Set(markers);
|
|
143
|
+
const received = previousInputs
|
|
144
|
+
? previousInputs.received.filter((item) => markerSet.has(item.marker))
|
|
145
|
+
: [];
|
|
146
|
+
state.images.body_inputs = {
|
|
147
|
+
scope: "newspic-longform",
|
|
148
|
+
expected: markers.length,
|
|
149
|
+
received,
|
|
150
|
+
status:
|
|
151
|
+
markers.length === 0
|
|
152
|
+
? "none"
|
|
153
|
+
: received.length >= markers.length
|
|
154
|
+
? "ready"
|
|
155
|
+
: "pending",
|
|
156
|
+
layout: previousInputs?.layout ?? newspicRenderSpec.default_image_layout,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// If body inputs are pending, we need to pause for user
|
|
160
|
+
if (state.images.body_inputs.status === "pending") {
|
|
161
|
+
state.images.plan.status = "planned";
|
|
162
|
+
await writeState(statePath, state);
|
|
163
|
+
printResult({
|
|
164
|
+
plan: state.images.plan,
|
|
165
|
+
body_inputs: state.images.body_inputs,
|
|
166
|
+
newspic_render: newspicRenderSpec,
|
|
167
|
+
waiting_for_user: true,
|
|
168
|
+
message: `Waiting for ${markers.length} body image(s)`,
|
|
169
|
+
phase: state.phase.current,
|
|
170
|
+
}, renderRender);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
} else if (state.images.body_inputs.scope === "newspic-longform") {
|
|
174
|
+
state.images.body_inputs = defaultBodyInputs();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (skipRender) {
|
|
178
|
+
await writeState(statePath, state);
|
|
179
|
+
printResult({
|
|
180
|
+
plan: state.images.plan,
|
|
181
|
+
newspic_render: routePrimary === "wechat-newspic" ? newspicRenderSpec : null,
|
|
182
|
+
skip_render: true,
|
|
183
|
+
phase: state.phase.current,
|
|
184
|
+
}, renderRender);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Invoke image renderer adapter ───────────────────────────────
|
|
189
|
+
|
|
190
|
+
const config = loadConfig();
|
|
191
|
+
const imageRenderer = await resolveImageRenderer(config);
|
|
192
|
+
|
|
193
|
+
const bodyImages = (
|
|
194
|
+
state.images.body_inputs.scope === "newspic-longform" &&
|
|
195
|
+
state.images.body_inputs.status === "ready"
|
|
196
|
+
)
|
|
197
|
+
? state.images.body_inputs.received
|
|
198
|
+
: [];
|
|
199
|
+
|
|
200
|
+
const renderResult = await imageRenderer.render({
|
|
201
|
+
state,
|
|
202
|
+
bodyText: cleanBody,
|
|
203
|
+
outputDir,
|
|
204
|
+
title: state.metadata.title,
|
|
205
|
+
route: routePrimary,
|
|
206
|
+
pageSpecs: newspicRenderSpec.page_specs.map((ps) => ({
|
|
207
|
+
page: ps.page,
|
|
208
|
+
imageMarkers: ps.image_markers,
|
|
209
|
+
imageLayout: ps.image_layout ?? undefined,
|
|
210
|
+
targetFillRatio: ps.target_fill_ratio ?? undefined,
|
|
211
|
+
note: ps.note ?? undefined,
|
|
212
|
+
})),
|
|
213
|
+
bodyImages,
|
|
214
|
+
minPages: newspicRenderSpec.min_pages,
|
|
215
|
+
maxPages: newspicRenderSpec.max_pages,
|
|
216
|
+
accountVisualParams: state.route.account_visual_params
|
|
217
|
+
? {
|
|
218
|
+
footer: state.route.account_visual_params.footer,
|
|
219
|
+
bg: state.route.account_visual_params.bg,
|
|
220
|
+
highlight: state.route.account_visual_params.highlight,
|
|
221
|
+
fallbackIcon: state.route.account_visual_params.fallback_icon,
|
|
222
|
+
}
|
|
223
|
+
: undefined,
|
|
224
|
+
theme: routePrimary === "wechat-newspic" ? getLongformTheme(state.route.account) : undefined,
|
|
225
|
+
template,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ── Update state ────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
state.images.render_assets = renderResult.assets;
|
|
231
|
+
state.images.plan.status = "rendered";
|
|
232
|
+
state.phase.render = { status: "done", error: null };
|
|
233
|
+
state.phase.current = state.intent.requires.publish ? "publish" : "done";
|
|
234
|
+
state.artifacts.render_version += 1;
|
|
235
|
+
|
|
236
|
+
await writeState(statePath, state);
|
|
237
|
+
|
|
238
|
+
printResult({
|
|
239
|
+
plan: state.images.plan,
|
|
240
|
+
render_assets: renderResult.assets,
|
|
241
|
+
render_version: state.artifacts.render_version,
|
|
242
|
+
phase: state.phase.current,
|
|
243
|
+
}, renderRender);
|
|
244
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, writeFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
|
|
6
|
+
import { reset } from "./reset";
|
|
7
|
+
import { defaultState, readState, writeState } from "../state";
|
|
8
|
+
|
|
9
|
+
const TEST_CONFIG_PATH = join(tmpdir(), `zzhub-pipeline-test-config-reset-${process.pid}.json`);
|
|
10
|
+
process.env.ZZHUB_PIPELINE_CONFIG = TEST_CONFIG_PATH;
|
|
11
|
+
|
|
12
|
+
async function makeTempDir(prefix: string): Promise<string> {
|
|
13
|
+
return mkdtemp(join(tmpdir(), prefix));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function captureJsonOutput<T>(fn: () => Promise<void>): Promise<T> {
|
|
17
|
+
const original = console.log;
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
console.log = (...args: unknown[]) => {
|
|
20
|
+
lines.push(args.map((value) => String(value)).join(" "));
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
await fn();
|
|
24
|
+
} finally {
|
|
25
|
+
console.log = original;
|
|
26
|
+
}
|
|
27
|
+
return JSON.parse(lines.join("\n").trim()) as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
await writeFile(TEST_CONFIG_PATH, "{}", "utf-8");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("reset", () => {
|
|
35
|
+
test("mode=content resets all three phases and clears content_review", async () => {
|
|
36
|
+
const workspace = await makeTempDir("zzhub-reset-content-");
|
|
37
|
+
const statePath = join(workspace, "state.json");
|
|
38
|
+
|
|
39
|
+
const state = defaultState();
|
|
40
|
+
state.workspace_root = workspace;
|
|
41
|
+
state.state_path = statePath;
|
|
42
|
+
state.run_id = "run-reset-content";
|
|
43
|
+
state.phase.prepare = { status: "done", error: null };
|
|
44
|
+
state.phase.render = { status: "done", error: null };
|
|
45
|
+
state.phase.publish = { status: "done", error: null };
|
|
46
|
+
state.phase.current = "done";
|
|
47
|
+
state.content_review = { status: "passed", feedback: null };
|
|
48
|
+
await writeState(statePath, state);
|
|
49
|
+
|
|
50
|
+
const output = await captureJsonOutput<{ reset_mode: string; start_step: string | null }>(() =>
|
|
51
|
+
reset(["--state", statePath, "--mode", "content"]),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(output.reset_mode).toBe("content");
|
|
55
|
+
expect(output.start_step).toBe("writer");
|
|
56
|
+
|
|
57
|
+
const updated = await readState(statePath);
|
|
58
|
+
expect(updated.phase.prepare.status).toBe("pending");
|
|
59
|
+
expect(updated.phase.render.status).toBe("pending");
|
|
60
|
+
expect(updated.phase.publish.status).toBe("pending");
|
|
61
|
+
expect(updated.phase.current).toBe("prepare");
|
|
62
|
+
expect(updated.mode).toBe("active");
|
|
63
|
+
expect(updated.content_review.status).toBe("unchecked");
|
|
64
|
+
expect(updated.redo_hint).toBe("writer");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("mode=render keeps prepare=done but resets render and publish", async () => {
|
|
68
|
+
const workspace = await makeTempDir("zzhub-reset-render-");
|
|
69
|
+
const statePath = join(workspace, "state.json");
|
|
70
|
+
|
|
71
|
+
const state = defaultState();
|
|
72
|
+
state.workspace_root = workspace;
|
|
73
|
+
state.state_path = statePath;
|
|
74
|
+
state.run_id = "run-reset-render";
|
|
75
|
+
state.phase.prepare = { status: "done", error: null };
|
|
76
|
+
state.phase.render = { status: "done", error: null };
|
|
77
|
+
state.phase.publish = { status: "done", error: null };
|
|
78
|
+
state.phase.current = "done";
|
|
79
|
+
await writeState(statePath, state);
|
|
80
|
+
|
|
81
|
+
await captureJsonOutput(() => reset(["--state", statePath, "--mode", "render"]));
|
|
82
|
+
|
|
83
|
+
const updated = await readState(statePath);
|
|
84
|
+
expect(updated.phase.prepare.status).toBe("done");
|
|
85
|
+
expect(updated.phase.render.status).toBe("pending");
|
|
86
|
+
expect(updated.phase.publish.status).toBe("pending");
|
|
87
|
+
expect(updated.phase.current).toBe("render");
|
|
88
|
+
expect(updated.mode).toBe("active");
|
|
89
|
+
expect(updated.redo_hint).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("mode=redo.style sets redo_hint to style", async () => {
|
|
93
|
+
const workspace = await makeTempDir("zzhub-reset-redo-");
|
|
94
|
+
const statePath = join(workspace, "state.json");
|
|
95
|
+
|
|
96
|
+
const state = defaultState();
|
|
97
|
+
state.workspace_root = workspace;
|
|
98
|
+
state.state_path = statePath;
|
|
99
|
+
state.run_id = "run-reset-redo";
|
|
100
|
+
state.phase.prepare = { status: "done", error: null };
|
|
101
|
+
state.phase.render = { status: "done", error: null };
|
|
102
|
+
await writeState(statePath, state);
|
|
103
|
+
|
|
104
|
+
const output = await captureJsonOutput<{ start_step: string | null }>(() =>
|
|
105
|
+
reset(["--state", statePath, "--mode", "redo.style"]),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(output.start_step).toBe("style");
|
|
109
|
+
|
|
110
|
+
const updated = await readState(statePath);
|
|
111
|
+
expect(updated.redo_hint).toBe("style");
|
|
112
|
+
expect(updated.phase.prepare.status).toBe("pending");
|
|
113
|
+
expect(updated.phase.render.status).toBe("pending");
|
|
114
|
+
expect(updated.phase.publish.status).toBe("pending");
|
|
115
|
+
expect(updated.phase.current).toBe("prepare");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("mode=full marks state as failed", async () => {
|
|
119
|
+
const workspace = await makeTempDir("zzhub-reset-full-");
|
|
120
|
+
const statePath = join(workspace, "state.json");
|
|
121
|
+
|
|
122
|
+
const state = defaultState();
|
|
123
|
+
state.workspace_root = workspace;
|
|
124
|
+
state.state_path = statePath;
|
|
125
|
+
state.run_id = "run-reset-full";
|
|
126
|
+
state.phase.prepare = { status: "done", error: null };
|
|
127
|
+
await writeState(statePath, state);
|
|
128
|
+
|
|
129
|
+
await captureJsonOutput(() => reset(["--state", statePath, "--mode", "full"]));
|
|
130
|
+
|
|
131
|
+
const updated = await readState(statePath);
|
|
132
|
+
expect(updated.mode).toBe("failed");
|
|
133
|
+
expect(updated.phase.current).toBe("failed");
|
|
134
|
+
expect(updated.content_review.status).toBe("unchecked");
|
|
135
|
+
expect(updated.redo_hint).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("invalid mode throws descriptive error", async () => {
|
|
139
|
+
const workspace = await makeTempDir("zzhub-reset-invalid-");
|
|
140
|
+
const statePath = join(workspace, "state.json");
|
|
141
|
+
|
|
142
|
+
const state = defaultState();
|
|
143
|
+
state.workspace_root = workspace;
|
|
144
|
+
state.state_path = statePath;
|
|
145
|
+
await writeState(statePath, state);
|
|
146
|
+
|
|
147
|
+
expect(
|
|
148
|
+
reset(["--state", statePath, "--mode", "bogus"]),
|
|
149
|
+
).rejects.toThrow("Invalid reset mode: bogus");
|
|
150
|
+
});
|
|
151
|
+
});
|