@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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prepare-finalize — Complete the Prepare phase:
|
|
3
|
+
* 1. channel-route 2nd pass: generate highlight_words from final title
|
|
4
|
+
* 2. asset-save: create asset directory, write post.md with frontmatter, write canonical state
|
|
5
|
+
*
|
|
6
|
+
* Called after style (if needed) and format are done.
|
|
7
|
+
* This is the boundary between temp run state and canonical asset state.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* zzhub-pipeline prepare-finalize \
|
|
11
|
+
* --state /path/to/state.json \
|
|
12
|
+
* --body /path/to/body_formatted.md \
|
|
13
|
+
* [--workspace /abs/workspace]
|
|
14
|
+
*
|
|
15
|
+
* Output: asset_path, canonical state_path
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { access, copyFile, readFile, writeFile, mkdir } from "fs/promises";
|
|
19
|
+
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
|
20
|
+
import { parseArgs, requireArg, optionalArg } from "../args";
|
|
21
|
+
import { printResult, renderPrepareFinalize } from "../output";
|
|
22
|
+
import {
|
|
23
|
+
loadConfig,
|
|
24
|
+
renderPostsRelativePath,
|
|
25
|
+
resolveWorkspacePaths,
|
|
26
|
+
resolveWorkspaceRoot,
|
|
27
|
+
} from "../config";
|
|
28
|
+
import {
|
|
29
|
+
readState,
|
|
30
|
+
writeState,
|
|
31
|
+
getCanonicalStatePath,
|
|
32
|
+
} from "../state";
|
|
33
|
+
import {
|
|
34
|
+
extractHighlightWords,
|
|
35
|
+
buildFrontmatter,
|
|
36
|
+
formatArticle,
|
|
37
|
+
stripLeadingTitleHeading,
|
|
38
|
+
removePageMarkers,
|
|
39
|
+
} from "../text";
|
|
40
|
+
|
|
41
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
await access(path);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isExternalAssetRef(value: string): boolean {
|
|
51
|
+
return /^(https?:|data:|blob:|\/\/)/i.test(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeSafeRelativeAssetPath(rawPath: string): string | null {
|
|
55
|
+
const normalized = rawPath
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/^file:\/\//i, "")
|
|
58
|
+
.replaceAll("\\", "/")
|
|
59
|
+
.replace(/^[.]\//, "")
|
|
60
|
+
.replace(/[?#].*$/, "");
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
!normalized ||
|
|
64
|
+
normalized.startsWith("/") ||
|
|
65
|
+
normalized.startsWith("../") ||
|
|
66
|
+
normalized.includes("/../")
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatRelativeAssetRef(relativePath: string): string {
|
|
75
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function uniqueRelativeAssetPath(
|
|
79
|
+
targetRelativePath: string,
|
|
80
|
+
sourcePath: string,
|
|
81
|
+
usedTargetPaths: Map<string, string>,
|
|
82
|
+
): string {
|
|
83
|
+
const normalizedTarget = targetRelativePath.replaceAll("\\", "/");
|
|
84
|
+
const existingSource = usedTargetPaths.get(normalizedTarget);
|
|
85
|
+
if (!existingSource || existingSource === sourcePath) {
|
|
86
|
+
usedTargetPaths.set(normalizedTarget, sourcePath);
|
|
87
|
+
return normalizedTarget;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fileName = basename(normalizedTarget);
|
|
91
|
+
const extensionIndex = fileName.lastIndexOf(".");
|
|
92
|
+
const name = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
|
|
93
|
+
const ext = extensionIndex > 0 ? fileName.slice(extensionIndex) : "";
|
|
94
|
+
const directory = dirname(normalizedTarget);
|
|
95
|
+
|
|
96
|
+
let suffix = 2;
|
|
97
|
+
while (true) {
|
|
98
|
+
const candidateLeaf = `${name}-${suffix}${ext}`;
|
|
99
|
+
const candidate = directory === "."
|
|
100
|
+
? candidateLeaf
|
|
101
|
+
: join(directory, candidateLeaf).replaceAll("\\", "/");
|
|
102
|
+
const candidateSource = usedTargetPaths.get(candidate);
|
|
103
|
+
if (!candidateSource || candidateSource === sourcePath) {
|
|
104
|
+
usedTargetPaths.set(candidate, sourcePath);
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
suffix += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function materializeInlineAssets(options: {
|
|
112
|
+
body: string;
|
|
113
|
+
bodySourceDir: string;
|
|
114
|
+
assetPath: string;
|
|
115
|
+
}): Promise<string> {
|
|
116
|
+
const usedTargetPaths = new Map<string, string>();
|
|
117
|
+
const copyJobs = new Map<string, { sourcePath: string; targetRelativePath: string }>();
|
|
118
|
+
|
|
119
|
+
const registerAsset = (rawSource: string): string | null => {
|
|
120
|
+
const trimmed = rawSource.trim();
|
|
121
|
+
if (!trimmed || isExternalAssetRef(trimmed)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sourcePath = trimmed.startsWith("file://")
|
|
126
|
+
? new URL(trimmed).pathname
|
|
127
|
+
: isAbsolute(trimmed)
|
|
128
|
+
? trimmed
|
|
129
|
+
: resolve(options.bodySourceDir, trimmed);
|
|
130
|
+
|
|
131
|
+
const safeRelativePath = normalizeSafeRelativeAssetPath(trimmed);
|
|
132
|
+
const targetRelativePath = uniqueRelativeAssetPath(
|
|
133
|
+
safeRelativePath ?? join("_inline-assets", basename(sourcePath)).replaceAll("\\", "/"),
|
|
134
|
+
sourcePath,
|
|
135
|
+
usedTargetPaths,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
copyJobs.set(sourcePath, { sourcePath, targetRelativePath });
|
|
139
|
+
return targetRelativePath;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
let rewritten = options.body.replace(
|
|
143
|
+
/!\[([^\]]*)\]\((<)?([^)]+?)(>)?\)/g,
|
|
144
|
+
(match, alt, open, src) => {
|
|
145
|
+
const targetRelativePath = registerAsset(String(src ?? ""));
|
|
146
|
+
if (!targetRelativePath) {
|
|
147
|
+
return match;
|
|
148
|
+
}
|
|
149
|
+
return `}${open ? ">" : ""})`;
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
rewritten = rewritten.replace(
|
|
154
|
+
/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
|
|
155
|
+
(match, prefix, src, suffix) => {
|
|
156
|
+
const targetRelativePath = registerAsset(String(src ?? ""));
|
|
157
|
+
if (!targetRelativePath) {
|
|
158
|
+
return match;
|
|
159
|
+
}
|
|
160
|
+
return `${prefix}${formatRelativeAssetRef(targetRelativePath)}${suffix}`;
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
for (const job of copyJobs.values()) {
|
|
165
|
+
const destinationPath = join(options.assetPath, job.targetRelativePath);
|
|
166
|
+
await mkdir(dirname(destinationPath), { recursive: true });
|
|
167
|
+
await copyFile(job.sourcePath, destinationPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return rewritten;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendSuffixToRelativePath(relativePath: string, suffix: number): string {
|
|
174
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
175
|
+
const parent = dirname(normalized);
|
|
176
|
+
const leaf = normalized.split("/").filter(Boolean).pop() || "untitled";
|
|
177
|
+
const suffixedLeaf = `${leaf}-v${suffix}`;
|
|
178
|
+
return parent === "." ? suffixedLeaf : join(parent, suffixedLeaf);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function prepareFinalize(args: string[]): Promise<void> {
|
|
182
|
+
const parsed = parseArgs(args);
|
|
183
|
+
|
|
184
|
+
if (parsed.help) {
|
|
185
|
+
console.log(`
|
|
186
|
+
Usage: zzhub-pipeline prepare-finalize [options]
|
|
187
|
+
|
|
188
|
+
Options:
|
|
189
|
+
--state Path to state JSON (required)
|
|
190
|
+
--body Path to body text file (optional; defaults to state.formatted_body_path)
|
|
191
|
+
--workspace Workspace root (optional; defaults to state.workspace_root)
|
|
192
|
+
`.trim());
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
197
|
+
const explicitBodyPath = optionalArg(parsed, "body");
|
|
198
|
+
const workspaceOverride = optionalArg(parsed, "workspace");
|
|
199
|
+
|
|
200
|
+
const state = await readState(statePath);
|
|
201
|
+
if (state.content_review.status !== "passed") {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`content_review must be passed before prepare-finalize (current: ${state.content_review.status})`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const bodyPath = explicitBodyPath ?? state.formatted_body_path;
|
|
207
|
+
if (!bodyPath) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Missing required argument: --body (body text file path). Run prepare first or pass --body explicitly.",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
let body = await readFile(bodyPath, "utf-8");
|
|
213
|
+
|
|
214
|
+
// If style was applied but format wasn't re-applied, apply format now
|
|
215
|
+
// (format is idempotent, safe to re-apply)
|
|
216
|
+
body = formatArticle(body);
|
|
217
|
+
body = stripLeadingTitleHeading(body, state.metadata.title);
|
|
218
|
+
|
|
219
|
+
const config = loadConfig();
|
|
220
|
+
const workspace = resolveWorkspaceRoot(workspaceOverride ?? state.workspace_root, config);
|
|
221
|
+
const workspacePaths = resolveWorkspacePaths(workspace, config);
|
|
222
|
+
|
|
223
|
+
// ── Step 1: Channel route 2nd pass — highlight_words ──
|
|
224
|
+
// If highlight_words were already set explicitly via `prepare --highlight-words`,
|
|
225
|
+
// respect that and skip the auto-extract algorithm.
|
|
226
|
+
const highlightWords =
|
|
227
|
+
state.route.highlight_words && state.route.highlight_words.length > 0
|
|
228
|
+
? state.route.highlight_words
|
|
229
|
+
: extractHighlightWords(state.metadata.title);
|
|
230
|
+
state.route.highlight_words = highlightWords;
|
|
231
|
+
|
|
232
|
+
// ── Step 2: Asset save ──
|
|
233
|
+
|
|
234
|
+
// Build asset path from configured posts pattern.
|
|
235
|
+
let assetPath = state.asset_path;
|
|
236
|
+
if (!assetPath) {
|
|
237
|
+
const patternUsesSlug = config.paths.postsPathPattern.includes("{slug}");
|
|
238
|
+
const patternUsesTitle = config.paths.postsPathPattern.includes("{title}");
|
|
239
|
+
const baseSlug = state.metadata.slug;
|
|
240
|
+
const baseTitle = state.metadata.title;
|
|
241
|
+
let nextSlug = baseSlug;
|
|
242
|
+
let nextTitle = baseTitle;
|
|
243
|
+
let relativePath = renderPostsRelativePath(config, {
|
|
244
|
+
date: state.metadata.date,
|
|
245
|
+
slug: nextSlug,
|
|
246
|
+
title: nextTitle,
|
|
247
|
+
});
|
|
248
|
+
let candidatePath = join(workspacePaths.postsRoot, relativePath);
|
|
249
|
+
let suffix = 2;
|
|
250
|
+
|
|
251
|
+
while (await pathExists(candidatePath)) {
|
|
252
|
+
if (patternUsesSlug) {
|
|
253
|
+
nextSlug = `${baseSlug}-v${suffix}`;
|
|
254
|
+
relativePath = renderPostsRelativePath(config, {
|
|
255
|
+
date: state.metadata.date,
|
|
256
|
+
slug: nextSlug,
|
|
257
|
+
title: baseTitle,
|
|
258
|
+
});
|
|
259
|
+
} else if (patternUsesTitle) {
|
|
260
|
+
nextTitle = `${baseTitle}-v${suffix}`;
|
|
261
|
+
relativePath = renderPostsRelativePath(config, {
|
|
262
|
+
date: state.metadata.date,
|
|
263
|
+
slug: baseSlug,
|
|
264
|
+
title: nextTitle,
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
relativePath = appendSuffixToRelativePath(relativePath, suffix);
|
|
268
|
+
}
|
|
269
|
+
candidatePath = join(workspacePaths.postsRoot, relativePath);
|
|
270
|
+
suffix += 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (patternUsesSlug && nextSlug !== state.metadata.slug) {
|
|
274
|
+
state.metadata.slug = nextSlug;
|
|
275
|
+
}
|
|
276
|
+
assetPath = candidatePath;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create directories
|
|
280
|
+
await mkdir(assetPath, { recursive: true });
|
|
281
|
+
|
|
282
|
+
const bodySourceDir = dirname(state.source_body_path ?? bodyPath);
|
|
283
|
+
body = await materializeInlineAssets({
|
|
284
|
+
body,
|
|
285
|
+
bodySourceDir,
|
|
286
|
+
assetPath,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Create image directories based on route
|
|
290
|
+
if (
|
|
291
|
+
state.route.primary === "wechat-article" ||
|
|
292
|
+
state.route.extras.includes("wechat-article")
|
|
293
|
+
) {
|
|
294
|
+
await mkdir(join(assetPath, "images", "wechat"), { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
state.route.primary === "wechat-newspic" ||
|
|
298
|
+
state.route.extras.includes("wechat-newspic")
|
|
299
|
+
) {
|
|
300
|
+
await mkdir(join(assetPath, "images", "newspic"), { recursive: true });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Build frontmatter
|
|
304
|
+
const frontmatter = buildFrontmatter({
|
|
305
|
+
title: state.metadata.title,
|
|
306
|
+
date: state.metadata.date,
|
|
307
|
+
platform: state.route.primary,
|
|
308
|
+
description: state.metadata.description,
|
|
309
|
+
tags: state.metadata.tags,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Write post.md
|
|
313
|
+
const postContent = `${frontmatter}\n\n${removePageMarkers(body)}`;
|
|
314
|
+
const postPath = join(assetPath, "post.md");
|
|
315
|
+
await writeFile(postPath, postContent, "utf-8");
|
|
316
|
+
|
|
317
|
+
// Update state
|
|
318
|
+
state.asset_path = assetPath;
|
|
319
|
+
state.state_path = getCanonicalStatePath(assetPath);
|
|
320
|
+
|
|
321
|
+
// Phase transitions
|
|
322
|
+
state.phase.prepare = { status: "done", error: null };
|
|
323
|
+
state.phase.current = state.intent.requires.render ? "render" :
|
|
324
|
+
state.intent.requires.publish ? "publish" : "done";
|
|
325
|
+
|
|
326
|
+
// Clear redo_hint — prepare sub-sequence is complete
|
|
327
|
+
state.redo_hint = null;
|
|
328
|
+
|
|
329
|
+
// Bump content version
|
|
330
|
+
state.artifacts.content_version += 1;
|
|
331
|
+
|
|
332
|
+
// Write canonical state
|
|
333
|
+
await writeState(state.state_path, state);
|
|
334
|
+
|
|
335
|
+
// Also update the temp run state to point to canonical
|
|
336
|
+
await writeState(statePath, state);
|
|
337
|
+
|
|
338
|
+
// Output summary
|
|
339
|
+
const output = {
|
|
340
|
+
asset_path: assetPath,
|
|
341
|
+
state_path: state.state_path,
|
|
342
|
+
post_path: postPath,
|
|
343
|
+
highlight_words: highlightWords,
|
|
344
|
+
phase: state.phase.current,
|
|
345
|
+
content_version: state.artifacts.content_version,
|
|
346
|
+
};
|
|
347
|
+
printResult(output, renderPrepareFinalize);
|
|
348
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prepare — Execute deterministic Prepare phase steps:
|
|
3
|
+
* 1. channel-route (1st pass): route.primary, extras, account, content_profile, visual_params
|
|
4
|
+
* 2. authoring decision: rewrite_allowed, style_mode
|
|
5
|
+
* 3. format: apply text formatting rules to body
|
|
6
|
+
* 4. asset-meta: title, slug, date, description, tags
|
|
7
|
+
*
|
|
8
|
+
* Style step is NOT included — it requires LLM and stays as a skill.
|
|
9
|
+
* This command reads the run state, applies deterministic transforms,
|
|
10
|
+
* and writes back the updated state.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* zzhub-pipeline prepare \
|
|
14
|
+
* --state /path/to/state.json \
|
|
15
|
+
* [--body /path/to/body_raw.md] \
|
|
16
|
+
* [--title "Custom title"] \
|
|
17
|
+
* [--intent-text "发公众号文章"] \
|
|
18
|
+
* [--account default] \
|
|
19
|
+
* [--route wechat-article] \
|
|
20
|
+
* [--extras blog] \
|
|
21
|
+
* [--style-request]
|
|
22
|
+
*
|
|
23
|
+
* Output: Updated state JSON. body_formatted is written to a managed temp file
|
|
24
|
+
* unless --body-out is explicitly provided.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { copyFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
28
|
+
import { extname, join, resolve } from "path";
|
|
29
|
+
import { parseArgs, requireArg, optionalArg, flagArg } from "../args";
|
|
30
|
+
import { printResult, renderPrepare } from "../output";
|
|
31
|
+
import { resolveWorkspacePaths } from "../config";
|
|
32
|
+
import {
|
|
33
|
+
defaultBodyInputs,
|
|
34
|
+
readState,
|
|
35
|
+
writeState,
|
|
36
|
+
} from "../state";
|
|
37
|
+
import { resolveFullRoute } from "../routes";
|
|
38
|
+
import { resolveAuthoring, hasStyleRequest } from "../profiles";
|
|
39
|
+
import {
|
|
40
|
+
formatArticle,
|
|
41
|
+
generateSlug,
|
|
42
|
+
todayDate,
|
|
43
|
+
extractDescription,
|
|
44
|
+
findIllustrationMarkers,
|
|
45
|
+
stripFrontmatter,
|
|
46
|
+
} from "../text";
|
|
47
|
+
import type { RoutePrimary } from "../state";
|
|
48
|
+
|
|
49
|
+
async function stageManagedBodyFile(
|
|
50
|
+
workspaceRoot: string,
|
|
51
|
+
runId: string,
|
|
52
|
+
sourcePath: string,
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
const tempRoot = resolveWorkspacePaths(workspaceRoot).tempRoot;
|
|
55
|
+
const extension = extname(sourcePath) || ".md";
|
|
56
|
+
const managedDir = join(tempRoot, runId);
|
|
57
|
+
const managedPath = join(managedDir, `source-body${extension}`);
|
|
58
|
+
await mkdir(managedDir, { recursive: true });
|
|
59
|
+
if (resolve(sourcePath) !== resolve(managedPath)) {
|
|
60
|
+
await copyFile(sourcePath, managedPath);
|
|
61
|
+
}
|
|
62
|
+
return managedPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getManagedFormattedBodyPath(workspaceRoot: string, runId: string): string {
|
|
66
|
+
return join(resolveWorkspacePaths(workspaceRoot).tempRoot, runId, "formatted-body.md");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldPreserveExistingRoute(params: {
|
|
70
|
+
intentText: string;
|
|
71
|
+
routeOverride?: RoutePrimary;
|
|
72
|
+
extrasRaw?: string;
|
|
73
|
+
accountOverride?: string;
|
|
74
|
+
assetPath: string;
|
|
75
|
+
}): boolean {
|
|
76
|
+
return (
|
|
77
|
+
Boolean(params.assetPath) &&
|
|
78
|
+
!params.intentText.trim() &&
|
|
79
|
+
!params.routeOverride &&
|
|
80
|
+
!params.extrasRaw &&
|
|
81
|
+
!params.accountOverride
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function prepare(args: string[]): Promise<void> {
|
|
86
|
+
const parsed = parseArgs(args);
|
|
87
|
+
|
|
88
|
+
if (parsed.help) {
|
|
89
|
+
console.log(`
|
|
90
|
+
Usage: zzhub-pipeline prepare [options]
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
--state Path to state JSON (required)
|
|
94
|
+
--body Path to body text file (body_raw or body_styled) (optional if body already attached)
|
|
95
|
+
--title Override title (optional; defaults to first heading or first line)
|
|
96
|
+
--intent-text Original user intent text for route resolution (optional)
|
|
97
|
+
--account Override account (optional)
|
|
98
|
+
--route Override primary route (optional)
|
|
99
|
+
--extras Comma-separated extra routes (optional)
|
|
100
|
+
--style-request Flag: user requested style processing
|
|
101
|
+
--body-out Path to write formatted body (optional; defaults to managed temp path)
|
|
102
|
+
--suggested-title Suggested title from writer (optional)
|
|
103
|
+
--suggested-slug Suggested slug from writer (optional; English + hyphens, ≤50 chars; overrides auto-generated slug)
|
|
104
|
+
--highlight-words Comma-separated highlight words for cover (optional; overrides auto-extract in prepare-finalize)
|
|
105
|
+
`.trim());
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
110
|
+
const bodyPath = optionalArg(parsed, "body");
|
|
111
|
+
const titleOverride = optionalArg(parsed, "title");
|
|
112
|
+
const accountOverride = optionalArg(parsed, "account");
|
|
113
|
+
const routeOverride = optionalArg(parsed, "route") as RoutePrimary | undefined;
|
|
114
|
+
const extrasRaw = optionalArg(parsed, "extras");
|
|
115
|
+
const isStyleRequest = flagArg(parsed, "style-request");
|
|
116
|
+
const bodyOutPath = optionalArg(parsed, "body-out");
|
|
117
|
+
const suggestedTitle = optionalArg(parsed, "suggested-title");
|
|
118
|
+
const suggestedSlug = optionalArg(parsed, "suggested-slug");
|
|
119
|
+
const highlightWordsRaw = optionalArg(parsed, "highlight-words");
|
|
120
|
+
const highlightWordsOverride = highlightWordsRaw
|
|
121
|
+
? highlightWordsRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
122
|
+
: undefined;
|
|
123
|
+
|
|
124
|
+
const extras = extrasRaw
|
|
125
|
+
? (extrasRaw.split(",").map((s) => s.trim()) as RoutePrimary[])
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
128
|
+
// Read state and body
|
|
129
|
+
const state = await readState(statePath);
|
|
130
|
+
const intentText = optionalArg(parsed, "intent-text") ?? state.intent.intent_text ?? "";
|
|
131
|
+
const resolvedBodyPath = bodyPath
|
|
132
|
+
? await stageManagedBodyFile(state.workspace_root, state.run_id, bodyPath)
|
|
133
|
+
: state.source_body_path;
|
|
134
|
+
if (!resolvedBodyPath) {
|
|
135
|
+
throw new Error("No body is attached yet. Use attach-body first or pass --body.");
|
|
136
|
+
}
|
|
137
|
+
const bodyRaw = await readFile(resolvedBodyPath, "utf-8");
|
|
138
|
+
const cleanBody = stripFrontmatter(bodyRaw);
|
|
139
|
+
state.source_body_path = resolvedBodyPath;
|
|
140
|
+
state.intent.intent_text = intentText || state.intent.intent_text;
|
|
141
|
+
|
|
142
|
+
// ── Step 1: Channel route (1st pass) ──
|
|
143
|
+
const route = shouldPreserveExistingRoute({
|
|
144
|
+
intentText,
|
|
145
|
+
routeOverride,
|
|
146
|
+
extrasRaw,
|
|
147
|
+
accountOverride,
|
|
148
|
+
assetPath: state.asset_path,
|
|
149
|
+
})
|
|
150
|
+
? state.route
|
|
151
|
+
: resolveFullRoute(intentText, {
|
|
152
|
+
primary: routeOverride,
|
|
153
|
+
extras,
|
|
154
|
+
account: accountOverride,
|
|
155
|
+
contentForm: state.intent.content_form,
|
|
156
|
+
targets: state.intent.targets,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
state.route = route;
|
|
160
|
+
|
|
161
|
+
// If caller provided explicit highlight words, store them now so prepare-finalize
|
|
162
|
+
// can skip the auto-extract algorithm.
|
|
163
|
+
if (highlightWordsOverride && highlightWordsOverride.length > 0) {
|
|
164
|
+
state.route.highlight_words = highlightWordsOverride;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Re-derive requires.render and requires.publish based on resolved route.
|
|
168
|
+
// requires.research is intentional (set at init from user request), not touched here.
|
|
169
|
+
// Same for render/publish: if the orchestrator set them at init, keep that value.
|
|
170
|
+
// wechat-article always needs render regardless.
|
|
171
|
+
state.intent.requires.render =
|
|
172
|
+
state.intent.requires.render ||
|
|
173
|
+
route.primary === "wechat-article";
|
|
174
|
+
state.intent.requires.publish =
|
|
175
|
+
state.intent.requires.publish ||
|
|
176
|
+
state.intent.task_kind === "publish";
|
|
177
|
+
|
|
178
|
+
// ── Step 2: Author select ──
|
|
179
|
+
const styleRequest =
|
|
180
|
+
isStyleRequest || hasStyleRequest(intentText);
|
|
181
|
+
|
|
182
|
+
const authoring = resolveAuthoring({
|
|
183
|
+
contentOrigin: state.intent.content_origin,
|
|
184
|
+
styleHint: state.intent.style_hint,
|
|
185
|
+
hasStyleRequest: styleRequest,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
state.authoring = authoring;
|
|
189
|
+
|
|
190
|
+
// Update requires.style based on authoring result
|
|
191
|
+
state.intent.requires.style =
|
|
192
|
+
authoring.rewrite_allowed && authoring.style_mode !== "none";
|
|
193
|
+
|
|
194
|
+
// ── Step 3: Format ──
|
|
195
|
+
// In the current workflow, Writer/Style already own the LLM rewrite.
|
|
196
|
+
// Prepare records the authoring snapshot, then always formats the final body.
|
|
197
|
+
const bodyFormatted = formatArticle(cleanBody);
|
|
198
|
+
|
|
199
|
+
// ── Step 4: Asset meta ──
|
|
200
|
+
// Determine title
|
|
201
|
+
let title = titleOverride ?? suggestedTitle ?? state.metadata.title.trim();
|
|
202
|
+
if (!title) {
|
|
203
|
+
// Extract from body: first heading or first line
|
|
204
|
+
const lines = cleanBody.split("\n");
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const trimmed = line.trim();
|
|
207
|
+
if (!trimmed) continue;
|
|
208
|
+
const headingMatch = trimmed.match(/^#{1,6}\s+(.+)/);
|
|
209
|
+
if (headingMatch) {
|
|
210
|
+
title = headingMatch[1].trim();
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
// Use first non-empty line
|
|
214
|
+
title = trimmed;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Prefer editor-supplied slug; sanitize it defensively, then fall back to auto-generate
|
|
220
|
+
const slug = suggestedSlug
|
|
221
|
+
? suggestedSlug
|
|
222
|
+
.toLowerCase()
|
|
223
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
224
|
+
.replace(/^-+|-+$/g, "")
|
|
225
|
+
.slice(0, 80) || generateSlug(title)
|
|
226
|
+
: generateSlug(title);
|
|
227
|
+
const date = todayDate();
|
|
228
|
+
|
|
229
|
+
// Description: required for wechat-article, optional otherwise
|
|
230
|
+
let description: string | null = null;
|
|
231
|
+
if (route.primary === "wechat-article") {
|
|
232
|
+
description = extractDescription(bodyFormatted);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Tags: required for blog (can be empty list)
|
|
236
|
+
const tags: string[] = [];
|
|
237
|
+
|
|
238
|
+
state.metadata = {
|
|
239
|
+
title,
|
|
240
|
+
slug,
|
|
241
|
+
date,
|
|
242
|
+
description,
|
|
243
|
+
tags,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// ── Scan for illustration markers (for article body_inputs) ──
|
|
247
|
+
const needsArticleInputs =
|
|
248
|
+
route.primary === "wechat-article" ||
|
|
249
|
+
route.extras.includes("wechat-article");
|
|
250
|
+
const keepsNewspicInputs =
|
|
251
|
+
route.primary === "wechat-newspic" ||
|
|
252
|
+
route.extras.includes("wechat-newspic");
|
|
253
|
+
|
|
254
|
+
if (needsArticleInputs) {
|
|
255
|
+
const markers = findIllustrationMarkers(cleanBody);
|
|
256
|
+
if (markers.length > 0) {
|
|
257
|
+
const previousInputs =
|
|
258
|
+
state.images.body_inputs.scope === "article"
|
|
259
|
+
? state.images.body_inputs
|
|
260
|
+
: null;
|
|
261
|
+
const markerSet = new Set(markers);
|
|
262
|
+
const received = previousInputs
|
|
263
|
+
? previousInputs.received.filter((item) => markerSet.has(item.marker))
|
|
264
|
+
: [];
|
|
265
|
+
state.images.body_inputs = {
|
|
266
|
+
scope: "article",
|
|
267
|
+
expected: markers.length,
|
|
268
|
+
received,
|
|
269
|
+
status:
|
|
270
|
+
received.length >= markers.length
|
|
271
|
+
? "ready"
|
|
272
|
+
: "pending",
|
|
273
|
+
layout: previousInputs?.layout ?? "staggered",
|
|
274
|
+
};
|
|
275
|
+
} else {
|
|
276
|
+
state.images.body_inputs = defaultBodyInputs();
|
|
277
|
+
}
|
|
278
|
+
} else if (!keepsNewspicInputs || state.images.body_inputs.scope === "article") {
|
|
279
|
+
state.images.body_inputs = defaultBodyInputs();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Write state ──
|
|
283
|
+
state.formatted_body_path =
|
|
284
|
+
bodyOutPath ?? getManagedFormattedBodyPath(state.workspace_root, state.run_id);
|
|
285
|
+
await writeState(statePath, state);
|
|
286
|
+
|
|
287
|
+
// ── Output formatted body ──
|
|
288
|
+
const formattedBodyPath = state.formatted_body_path;
|
|
289
|
+
await mkdir(join(resolveWorkspacePaths(state.workspace_root).tempRoot, state.run_id), { recursive: true });
|
|
290
|
+
await writeFile(formattedBodyPath, bodyFormatted, "utf-8");
|
|
291
|
+
|
|
292
|
+
// Output summary for orchestrator
|
|
293
|
+
const illustrationMarkers = findIllustrationMarkers(cleanBody);
|
|
294
|
+
const output = {
|
|
295
|
+
state_path: statePath,
|
|
296
|
+
route: {
|
|
297
|
+
primary: state.route.primary,
|
|
298
|
+
account: state.route.account,
|
|
299
|
+
},
|
|
300
|
+
metadata: {
|
|
301
|
+
title: state.metadata.title,
|
|
302
|
+
slug: state.metadata.slug,
|
|
303
|
+
date: state.metadata.date,
|
|
304
|
+
},
|
|
305
|
+
requires_style: state.intent.requires.style,
|
|
306
|
+
body_formatted_path: formattedBodyPath,
|
|
307
|
+
...(illustrationMarkers.length > 0 ? { illustration_markers: illustrationMarkers } : {}),
|
|
308
|
+
};
|
|
309
|
+
printResult(output, renderPrepare);
|
|
310
|
+
}
|