@zzclub/pipeline 0.10.1 → 0.10.2
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 +43 -1
- package/package.json +2 -1
- package/src/adapter-types.ts +21 -0
- package/src/adapters/builtin-markdown-renderer.ts +7 -0
- package/src/cli.ts +5 -1
- package/src/commands/ingest-handoff.ts +1 -21
- package/src/commands/prepare-finalize.ts +2 -1
- package/src/commands/prepare.ts +22 -14
- package/src/commands/publish.ts +2 -3
- package/src/commands/reconcile.ts +3 -5
- package/src/commands/render.ts +2 -1
- package/src/commands/republish.ts +2 -3
- package/src/commands/reset.ts +9 -1
- package/src/commands/review.ts +3 -2
- package/src/commands/sync-blog.ts +2 -1
- package/src/commands/wechat-export.ts +134 -12
- package/src/commands/wechat-preview.ts +160 -0
- package/src/imgx/runtime.ts +31 -3
- package/src/logger.test.ts +56 -0
- package/src/logger.ts +178 -0
- package/src/plugins.ts +2 -0
- package/src/providers/blog.ts +1 -2
- package/src/providers/index.ts +29 -1
- package/src/schema/config.ts +2 -0
- package/src/state.ts +24 -0
- package/src/task-manager.test.ts +57 -0
- package/src/task-manager.ts +23 -5
- package/src/wechat-preview/assets/browser-dist/editor-export.js +129 -73
- package/src/wechat-preview/browser/editor-export.css +5 -366
- package/src/wechat-preview/browser/editor-export.ts +7 -3
- package/src/wechat-preview/bundle-stale.test.ts +52 -0
- package/src/wechat-preview/index.ts +390 -83
- package/src/wechat-preview/server/client.ts +164 -0
- package/src/wechat-preview/server/http.ts +229 -0
- package/src/wechat-preview/server/index.ts +33 -0
- package/src/wechat-preview/server/local-file.test.ts +52 -0
- package/src/wechat-preview/server/local-file.ts +116 -0
- package/src/wechat-preview/server/paths.ts +80 -0
- package/src/wechat-preview/server/registry.test.ts +60 -0
- package/src/wechat-preview/server/registry.ts +118 -0
- package/src/wechat-preview/server/singleton.ts +118 -0
- package/src/wechat-preview/server/types.ts +61 -0
- package/src/wechat-preview/server/ui.ts +324 -0
- package/src/wechat-preview/themes.ts +8 -0
- package/src/wechat-preview/wechat-renderer.links.test.ts +44 -0
- package/src/wechat-preview/wechat-renderer.test.ts +2 -0
- package/src/wechat-preview/wechat-renderer.ts +52 -6
- package/src/wechat-preview.test.ts +14 -7
- package/src/workflow-materials.test.ts +52 -1
- package/src/workflow-materials.ts +12 -0
- package/src/workflow.test.ts +61 -0
package/README.md
CHANGED
|
@@ -418,6 +418,7 @@ src/
|
|
|
418
418
|
| `republish` | Publish a completed task to an additional account or platform |
|
|
419
419
|
| `imgx` | Run bundled imgx renderer subcommands |
|
|
420
420
|
| `wechat-export` | Render markdown to WeChat HTML with bundled preview styles |
|
|
421
|
+
| `wechat-preview` | Local singleton server to preview exported WeChat HTML |
|
|
421
422
|
| `cos-upload` | Upload a local image to configured COS CDN |
|
|
422
423
|
| `config` | Read or update pipeline config |
|
|
423
424
|
| `doctor` | Inspect resolved paths and provider health |
|
|
@@ -630,6 +631,47 @@ bun run src/cli.ts wechat-export \
|
|
|
630
631
|
`--preview-shell-out` 生成最终发布 HTML 的精确预览:预览页直接嵌入 `--out`
|
|
631
632
|
中的内联样式片段,不再加载 Milkdown 编辑器或复制浏览器 bundle。
|
|
632
633
|
|
|
634
|
+
默认会将导出结果登记到本地 **单例预览服务**(`http://127.0.0.1:18765`),可在浏览器
|
|
635
|
+
侧栏切换多篇产物,无需发到微信草稿箱。服务未启动时会自动拉起;失败导出也会登记并展示
|
|
636
|
+
`error_kind` / timeout / Chrome stderr 等调试信息。
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
# 显式启动预览服务(全局只允许一个实例;重复启动会复用)
|
|
640
|
+
bun run src/cli.ts wechat-preview serve --open
|
|
641
|
+
|
|
642
|
+
# 导出并打开该条目
|
|
643
|
+
bun run src/cli.ts wechat-export \
|
|
644
|
+
--markdown /abs/path/body.md \
|
|
645
|
+
--out /tmp/article.html \
|
|
646
|
+
--account default \
|
|
647
|
+
--open
|
|
648
|
+
|
|
649
|
+
# 调试中间产物
|
|
650
|
+
bun run src/cli.ts wechat-export \
|
|
651
|
+
--markdown /abs/path/body.md \
|
|
652
|
+
--out /tmp/article.html \
|
|
653
|
+
--debug-dir /tmp/wx-debug \
|
|
654
|
+
--timeout-ms 20000
|
|
655
|
+
|
|
656
|
+
# 关闭预览登记
|
|
657
|
+
bun run src/cli.ts wechat-export ... --no-preview
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
| 子命令 | 说明 |
|
|
661
|
+
| --- | --- |
|
|
662
|
+
| `wechat-preview serve` | 启动/复用单例服务 |
|
|
663
|
+
| `wechat-preview status` | 是否在运行 |
|
|
664
|
+
| `wechat-preview open` | 打开 dashboard |
|
|
665
|
+
| `wechat-preview list` | 列出条目 |
|
|
666
|
+
| `wechat-preview clear` | 清空条目 |
|
|
667
|
+
|
|
668
|
+
环境变量:`ZZHUB_WECHAT_PREVIEW_PORT`(默认 18765)、`ZZHUB_WECHAT_PREVIEW_HOST`、
|
|
669
|
+
`ZZHUB_WECHAT_PREVIEW_DIR`(锁与条目落盘目录)。发布草稿时默认也会登记一条
|
|
670
|
+
`[publish] ...` 预览;设 `ZZHUB_WECHAT_PREVIEW_ON_PUBLISH=0` 可关闭。
|
|
671
|
+
|
|
672
|
+
源码(CSS/browser 入口)新于 dist 时会自动尝试 rebuild;失败则回退已有 bundle 并标记
|
|
673
|
+
`bundle_stale`。手动重建:`bun run build:wechat-preview`。
|
|
674
|
+
|
|
633
675
|
#### 自定义 CSS
|
|
634
676
|
|
|
635
677
|
可以通过账号配置或 CLI 覆盖内置样式:
|
|
@@ -854,7 +896,7 @@ bun run src/cli.ts init ... --note-id NOTE_ID
|
|
|
854
896
|
- 构建命令:`bun run build:wechat-preview`
|
|
855
897
|
- `wechat-export` 和微信草稿发布使用同一份最终内联 HTML
|
|
856
898
|
|
|
857
|
-
|
|
899
|
+
`@zzclub/milkdown-article-style`(`article.css`)会通过 Vite 以 raw CSS 打入浏览器
|
|
858
900
|
bundle。修改该 CSS 或浏览器 renderer 后必须执行:
|
|
859
901
|
|
|
860
902
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zzclub/pipeline",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zzp": "./src/cli.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@chenglou/pretext": "^0.0.5",
|
|
28
|
+
"@zzclub/milkdown-article-style": "0.1.1",
|
|
28
29
|
"image-size": "^2.0.2",
|
|
29
30
|
"pngjs": "^7.0.0",
|
|
30
31
|
"zod": "^4.4.2"
|
package/src/adapter-types.ts
CHANGED
|
@@ -143,6 +143,12 @@ export interface MarkdownRenderInput {
|
|
|
143
143
|
editorVars?: Record<string, string>;
|
|
144
144
|
exportTheme?: Record<string, string>;
|
|
145
145
|
};
|
|
146
|
+
|
|
147
|
+
/** Chrome virtual-time-budget in ms */
|
|
148
|
+
timeoutMs?: number;
|
|
149
|
+
|
|
150
|
+
/** Write intermediate artifacts for debugging */
|
|
151
|
+
debugDir?: string;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
/**
|
|
@@ -164,6 +170,21 @@ export interface MarkdownRenderOutput {
|
|
|
164
170
|
|
|
165
171
|
/** Path to preview shell HTML (if generated) */
|
|
166
172
|
previewShellPath?: string;
|
|
173
|
+
|
|
174
|
+
/** Wall-clock render duration */
|
|
175
|
+
durationMs?: number;
|
|
176
|
+
|
|
177
|
+
/** Whether the browser bundle was rebuilt during this render */
|
|
178
|
+
bundleRebuilt?: boolean;
|
|
179
|
+
|
|
180
|
+
/** Whether the browser bundle was stale before render */
|
|
181
|
+
bundleStale?: boolean;
|
|
182
|
+
|
|
183
|
+
/** Debug directory if requested */
|
|
184
|
+
debugDir?: string;
|
|
185
|
+
|
|
186
|
+
/** Semantic HTML from Milkdown before WeChat finalize (when available) */
|
|
187
|
+
semanticHtml?: string;
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
export interface MarkdownRenderPlugin {
|
|
@@ -51,6 +51,8 @@ export const builtinMarkdownRenderer: MarkdownRenderPlugin = {
|
|
|
51
51
|
previewShellOutPath: input.previewShellOutPath,
|
|
52
52
|
customCss: input.customCss,
|
|
53
53
|
themeOverrides: input.themeOverrides,
|
|
54
|
+
timeoutMs: input.timeoutMs,
|
|
55
|
+
debugDir: input.debugDir ?? undefined,
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
return {
|
|
@@ -59,6 +61,11 @@ export const builtinMarkdownRenderer: MarkdownRenderPlugin = {
|
|
|
59
61
|
account: result.account,
|
|
60
62
|
previewStyle: result.previewStyle,
|
|
61
63
|
previewShellPath: result.previewShellPath,
|
|
64
|
+
durationMs: result.durationMs,
|
|
65
|
+
bundleRebuilt: result.bundleRebuilt,
|
|
66
|
+
bundleStale: result.bundleStale,
|
|
67
|
+
debugDir: result.debugDir,
|
|
68
|
+
semanticHtml: result.semanticHtml,
|
|
62
69
|
};
|
|
63
70
|
},
|
|
64
71
|
};
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
+
import { getDailyLogPath, runWithCommandLog } from "./logger";
|
|
6
7
|
import { formatUsage, getCommandRegistry } from "./plugins";
|
|
7
8
|
|
|
8
9
|
const COMMANDS = getCommandRegistry();
|
|
@@ -36,10 +37,13 @@ async function main() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
|
-
await
|
|
40
|
+
await runWithCommandLog(cmd, args.slice(1), async () => {
|
|
41
|
+
await command.handler(args.slice(1));
|
|
42
|
+
});
|
|
40
43
|
} catch (err) {
|
|
41
44
|
const msg = err instanceof Error ? err.message : String(err);
|
|
42
45
|
console.error(`[zzhub-pipeline ${cmd}] Error: ${msg}`);
|
|
46
|
+
console.error(`[zzhub-pipeline ${cmd}] Full log: ${getDailyLogPath()}`);
|
|
43
47
|
process.exit(1);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -6,16 +6,12 @@ import { loadConfig, resolveWorkspacePaths, resolveWorkspaceRoot } from "../conf
|
|
|
6
6
|
import { printResult } from "../output";
|
|
7
7
|
import { resolveFullRoute } from "../routes";
|
|
8
8
|
import {
|
|
9
|
-
defaultArtifacts,
|
|
10
9
|
defaultContentReview,
|
|
11
|
-
defaultImages,
|
|
12
|
-
defaultMetadata,
|
|
13
|
-
defaultPhase,
|
|
14
|
-
defaultPublish,
|
|
15
10
|
defaultState,
|
|
16
11
|
generateRunId,
|
|
17
12
|
getRunStatePath,
|
|
18
13
|
readResolvedState,
|
|
14
|
+
resetDerivedState,
|
|
19
15
|
writeState,
|
|
20
16
|
type ContentForm,
|
|
21
17
|
type HandoffAuthoringPolicy,
|
|
@@ -302,22 +298,6 @@ function routesEqual(a: WorkflowState["route"], b: WorkflowState["route"]): bool
|
|
|
302
298
|
);
|
|
303
299
|
}
|
|
304
300
|
|
|
305
|
-
function resetDerivedState(state: WorkflowState): void {
|
|
306
|
-
state.mode = "active";
|
|
307
|
-
state.phase = defaultPhase();
|
|
308
|
-
state.asset_path = "";
|
|
309
|
-
state.formatted_body_path = null;
|
|
310
|
-
state.metadata = {
|
|
311
|
-
...defaultMetadata(),
|
|
312
|
-
title: state.metadata.title,
|
|
313
|
-
};
|
|
314
|
-
state.artifacts = defaultArtifacts();
|
|
315
|
-
state.images = defaultImages();
|
|
316
|
-
state.publish = defaultPublish();
|
|
317
|
-
state.content_review = defaultContentReview();
|
|
318
|
-
state.redo_hint = null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
301
|
async function resolveExistingStatePath(
|
|
322
302
|
workspaceRoot: string,
|
|
323
303
|
handoff: ResolvedWorkflowHandoff,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
writeState,
|
|
32
32
|
getCanonicalStatePath,
|
|
33
33
|
} from "../state";
|
|
34
|
+
import { loadTaskState } from "../task-manager";
|
|
34
35
|
import {
|
|
35
36
|
extractHighlightWords,
|
|
36
37
|
buildFrontmatter,
|
|
@@ -195,7 +196,7 @@ Options:
|
|
|
195
196
|
const initialResolved = await readResolvedState(requestedStatePath);
|
|
196
197
|
const releaseOperationLock = await acquireStateOperationLock(initialResolved.path);
|
|
197
198
|
try {
|
|
198
|
-
const resolved = await
|
|
199
|
+
const resolved = await loadTaskState(initialResolved.path);
|
|
199
200
|
const statePath = resolved.path;
|
|
200
201
|
const state = resolved.state;
|
|
201
202
|
if (state.content_review.status !== "passed") {
|
package/src/commands/prepare.ts
CHANGED
|
@@ -31,10 +31,10 @@ import { printResult, renderPrepare } from "../output";
|
|
|
31
31
|
import { resolveWorkspacePaths } from "../config";
|
|
32
32
|
import {
|
|
33
33
|
defaultBodyInputs,
|
|
34
|
-
readResolvedState,
|
|
35
34
|
reenterPrepare,
|
|
36
35
|
writeState,
|
|
37
36
|
} from "../state";
|
|
37
|
+
import { loadTaskState } from "../task-manager";
|
|
38
38
|
import { resolveFullRoute } from "../routes";
|
|
39
39
|
import { resolveAuthoring, hasStyleRequest } from "../profiles";
|
|
40
40
|
import { parseAccountName, parseRoutePrimary } from "../publish-targets";
|
|
@@ -131,7 +131,7 @@ Options:
|
|
|
131
131
|
: undefined;
|
|
132
132
|
|
|
133
133
|
// Read state and body
|
|
134
|
-
const resolved = await
|
|
134
|
+
const resolved = await loadTaskState(requestedStatePath);
|
|
135
135
|
const statePath = resolved.path;
|
|
136
136
|
const state = resolved.state;
|
|
137
137
|
const intentTextOverride = optionalArg(parsed, "intent-text");
|
|
@@ -144,9 +144,18 @@ Options:
|
|
|
144
144
|
}
|
|
145
145
|
const bodyRaw = await readFile(resolvedBodyPath, "utf-8");
|
|
146
146
|
const cleanBody = stripFrontmatter(bodyRaw);
|
|
147
|
+
const incomingBody = Boolean(bodyPath);
|
|
148
|
+
// A newly supplied body satisfies a pending writer/style revision. Clearing
|
|
149
|
+
// these hints prevents status from looping back to revise-content after the
|
|
150
|
+
// body has already been handed in via prepare --body. (format/asset-meta/
|
|
151
|
+
// channel-route hints are cleared below after prepare runs their steps.)
|
|
152
|
+
const effectiveRedoHint = incomingBody &&
|
|
153
|
+
(state.redo_hint === "writer" || state.redo_hint === "style")
|
|
154
|
+
? null
|
|
155
|
+
: state.redo_hint;
|
|
147
156
|
reenterPrepare(state, {
|
|
148
|
-
redoHint:
|
|
149
|
-
resetReview:
|
|
157
|
+
redoHint: effectiveRedoHint,
|
|
158
|
+
resetReview: incomingBody,
|
|
150
159
|
});
|
|
151
160
|
if (bodyPath && state.handoff.review_policy === "trust_user") {
|
|
152
161
|
state.content_review = { status: "passed", feedback: null };
|
|
@@ -180,16 +189,15 @@ Options:
|
|
|
180
189
|
state.route.highlight_words = highlightWordsOverride;
|
|
181
190
|
}
|
|
182
191
|
|
|
183
|
-
// Re-derive requires.render and requires.publish
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
state.intent.requires.
|
|
191
|
-
|
|
192
|
-
state.intent.task_kind === "publish";
|
|
192
|
+
// Re-derive requires.render and requires.publish deterministically from the
|
|
193
|
+
// resolved route/task kind rather than OR-accumulating: a flag set true for a
|
|
194
|
+
// previous route must not linger after the route changes (e.g. switching a
|
|
195
|
+
// task to blog-only would otherwise keep requires.render=true forever).
|
|
196
|
+
// requires.research is intentional (set at init from the user request) and is
|
|
197
|
+
// intentionally not recomputed here.
|
|
198
|
+
const allRoutes = [route.primary, ...route.extras];
|
|
199
|
+
state.intent.requires.render = allRoutes.some((r) => r !== "blog");
|
|
200
|
+
state.intent.requires.publish = state.intent.task_kind === "publish";
|
|
193
201
|
|
|
194
202
|
// ── Step 2: Author select ──
|
|
195
203
|
const styleRequest =
|
package/src/commands/publish.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
executePublishTargets,
|
|
34
34
|
upsertPublishResult,
|
|
35
35
|
} from "../providers/publish-core";
|
|
36
|
-
import {
|
|
36
|
+
import { loadTaskState } from "../task-manager";
|
|
37
37
|
|
|
38
38
|
export async function publish(args: string[]): Promise<void> {
|
|
39
39
|
const parsed = parseArgs(args);
|
|
@@ -60,10 +60,9 @@ Options:
|
|
|
60
60
|
const initialResolved = await readResolvedState(requestedStatePath);
|
|
61
61
|
const releaseOperationLock = await acquireStateOperationLock(initialResolved.path);
|
|
62
62
|
try {
|
|
63
|
-
const resolved = await
|
|
63
|
+
const resolved = await loadTaskState(initialResolved.path);
|
|
64
64
|
const statePath = resolved.path;
|
|
65
65
|
const state = resolved.state;
|
|
66
|
-
await reconcileStateArtifacts(state);
|
|
67
66
|
const config = loadConfig();
|
|
68
67
|
const workspacePaths = resolveWorkspacePaths(state.workspace_root, config);
|
|
69
68
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { parseArgs, requireArg } from "../args";
|
|
2
2
|
import { printResult, renderTaskShape } from "../output";
|
|
3
|
-
import {
|
|
4
|
-
import { getTaskByStatePath } from "../task-manager";
|
|
5
|
-
import { reconcileStateArtifacts } from "../workflow-materials";
|
|
3
|
+
import { writeState } from "../state";
|
|
4
|
+
import { getTaskByStatePath, loadTaskState } from "../task-manager";
|
|
6
5
|
|
|
7
6
|
export async function reconcile(args: string[]): Promise<void> {
|
|
8
7
|
const parsed = parseArgs(args);
|
|
@@ -18,10 +17,9 @@ Options:
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
const requestedStatePath = requireArg(parsed, "state", "state JSON path");
|
|
21
|
-
const resolved = await
|
|
20
|
+
const resolved = await loadTaskState(requestedStatePath);
|
|
22
21
|
const statePath = resolved.path;
|
|
23
22
|
const state = resolved.state;
|
|
24
|
-
await reconcileStateArtifacts(state);
|
|
25
23
|
await writeState(statePath, state);
|
|
26
24
|
|
|
27
25
|
const task = await getTaskByStatePath(statePath);
|
package/src/commands/render.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type NewspicRenderSpec,
|
|
32
32
|
type WorkflowState,
|
|
33
33
|
} from "../state";
|
|
34
|
+
import { loadTaskState } from "../task-manager";
|
|
34
35
|
import {
|
|
35
36
|
stripFrontmatter,
|
|
36
37
|
generateCoverTitle,
|
|
@@ -66,7 +67,7 @@ Options:
|
|
|
66
67
|
const initialResolved = await readResolvedState(requestedStatePath);
|
|
67
68
|
const releaseOperationLock = await acquireStateOperationLock(initialResolved.path);
|
|
68
69
|
try {
|
|
69
|
-
const resolved = await
|
|
70
|
+
const resolved = await loadTaskState(initialResolved.path);
|
|
70
71
|
const statePath = resolved.path;
|
|
71
72
|
const state = resolved.state;
|
|
72
73
|
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
executePublishTargets,
|
|
36
36
|
upsertPublishResult,
|
|
37
37
|
} from "../providers/publish-core";
|
|
38
|
-
import {
|
|
38
|
+
import { loadTaskState } from "../task-manager";
|
|
39
39
|
|
|
40
40
|
export async function republish(args: string[]): Promise<void> {
|
|
41
41
|
const parsed = parseArgs(args);
|
|
@@ -75,10 +75,9 @@ Examples:
|
|
|
75
75
|
const initialResolved = await readResolvedState(requestedStatePath);
|
|
76
76
|
const releaseOperationLock = await acquireStateOperationLock(initialResolved.path);
|
|
77
77
|
try {
|
|
78
|
-
const resolved = await
|
|
78
|
+
const resolved = await loadTaskState(initialResolved.path);
|
|
79
79
|
const statePath = resolved.path;
|
|
80
80
|
const state = resolved.state;
|
|
81
|
-
await reconcileStateArtifacts(state);
|
|
82
81
|
const config = loadConfig();
|
|
83
82
|
const workspacePaths = resolveWorkspacePaths(state.workspace_root, config);
|
|
84
83
|
|
package/src/commands/reset.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { parseArgs, requireArg } from "../args";
|
|
23
23
|
import { printResult, renderReset } from "../output";
|
|
24
24
|
import {
|
|
25
|
+
acquireStateOperationLock,
|
|
25
26
|
defaultContentReview,
|
|
26
27
|
readResolvedState,
|
|
27
28
|
reenterPrepare,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
reenterRender,
|
|
30
31
|
writeState,
|
|
31
32
|
} from "../state";
|
|
33
|
+
import { loadTaskState } from "../task-manager";
|
|
32
34
|
|
|
33
35
|
type ResetMode =
|
|
34
36
|
| "content"
|
|
@@ -84,7 +86,10 @@ Modes:
|
|
|
84
86
|
);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
const
|
|
89
|
+
const initialResolved = await readResolvedState(requestedStatePath);
|
|
90
|
+
const releaseOperationLock = await acquireStateOperationLock(initialResolved.path);
|
|
91
|
+
try {
|
|
92
|
+
const resolved = await loadTaskState(initialResolved.path);
|
|
88
93
|
const statePath = resolved.path;
|
|
89
94
|
const state = resolved.state;
|
|
90
95
|
|
|
@@ -149,4 +154,7 @@ Modes:
|
|
|
149
154
|
};
|
|
150
155
|
|
|
151
156
|
printResult(output, renderReset);
|
|
157
|
+
} finally {
|
|
158
|
+
await releaseOperationLock();
|
|
159
|
+
}
|
|
152
160
|
}
|
package/src/commands/review.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import { parseArgs, requireArg, optionalArg } from "../args";
|
|
19
19
|
import { printResult, renderReview } from "../output";
|
|
20
|
-
import {
|
|
20
|
+
import { writeState, type ContentReviewStatus } from "../state";
|
|
21
|
+
import { loadTaskState } from "../task-manager";
|
|
21
22
|
|
|
22
23
|
const VALID_STATUSES: ContentReviewStatus[] = [
|
|
23
24
|
"passed",
|
|
@@ -59,7 +60,7 @@ Statuses:
|
|
|
59
60
|
);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const resolved = await
|
|
63
|
+
const resolved = await loadTaskState(requestedStatePath);
|
|
63
64
|
const statePath = resolved.path;
|
|
64
65
|
const state = resolved.state;
|
|
65
66
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
readResolvedState,
|
|
7
7
|
updateState,
|
|
8
8
|
} from "../state";
|
|
9
|
+
import { loadTaskState } from "../task-manager";
|
|
9
10
|
import { publishBlogRoute } from "../providers/blog";
|
|
10
11
|
|
|
11
12
|
export async function syncBlog(args: string[]): Promise<void> {
|
|
@@ -31,7 +32,7 @@ Options:
|
|
|
31
32
|
const initialResolved = await readResolvedState(requestedStatePath);
|
|
32
33
|
const releaseOperationLock = await acquireStateOperationLock(initialResolved.path);
|
|
33
34
|
try {
|
|
34
|
-
const resolved = await
|
|
35
|
+
const resolved = await loadTaskState(initialResolved.path);
|
|
35
36
|
const statePath = resolved.path;
|
|
36
37
|
const state = resolved.state;
|
|
37
38
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
1
2
|
import { resolve } from "path";
|
|
2
|
-
import { optionalArg, parseArgs, requireArg } from "../args";
|
|
3
|
+
import { flagArg, optionalArg, parseArgs, requireArg } from "../args";
|
|
3
4
|
import { printResult, renderWechatExport } from "../output";
|
|
4
5
|
import { loadConfig, resolveConfigRelativePath } from "../config";
|
|
5
6
|
import { resolveMarkdownRenderer } from "../adapter-loader";
|
|
7
|
+
import { WechatExportError } from "../wechat-preview";
|
|
8
|
+
import { registerPreviewEntry } from "../wechat-preview/server";
|
|
6
9
|
|
|
7
10
|
const CSS_DEMO = `
|
|
8
11
|
Custom CSS is cascaded after the built-in theme and converted to conservative
|
|
@@ -36,6 +39,21 @@ export function resolveWechatExportCustomCss(
|
|
|
36
39
|
: resolveConfigRelativePath(accountCustomCss, configPath);
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
async function openUrl(url: string): Promise<void> {
|
|
43
|
+
const opener =
|
|
44
|
+
process.platform === "darwin"
|
|
45
|
+
? "open"
|
|
46
|
+
: process.platform === "win32"
|
|
47
|
+
? "cmd"
|
|
48
|
+
: "xdg-open";
|
|
49
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
50
|
+
try {
|
|
51
|
+
spawn(opener, args, { detached: true, stdio: "ignore" }).unref();
|
|
52
|
+
} catch {
|
|
53
|
+
// non-fatal
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export async function wechatExport(args: string[]): Promise<void> {
|
|
40
58
|
const parsed = parseArgs(args);
|
|
41
59
|
|
|
@@ -50,6 +68,12 @@ Options:
|
|
|
50
68
|
--title Page title for the render shell (optional)
|
|
51
69
|
--preview-shell-out Path to exact final-HTML preview (optional)
|
|
52
70
|
--custom-css CSS override path; replaces account customCss (optional)
|
|
71
|
+
--timeout-ms Chrome virtual-time-budget in ms (default: 15000)
|
|
72
|
+
--debug-dir Write intermediate artifacts for debugging (optional)
|
|
73
|
+
--preview Register result with local preview server (default: true)
|
|
74
|
+
--no-preview Do not register with preview server
|
|
75
|
+
--preview-auto-start Auto-start preview server if needed (default: true)
|
|
76
|
+
--open Open preview URL in browser after export
|
|
53
77
|
|
|
54
78
|
${CSS_DEMO}
|
|
55
79
|
`.trim());
|
|
@@ -62,6 +86,21 @@ ${CSS_DEMO}
|
|
|
62
86
|
const title = optionalArg(parsed, "title");
|
|
63
87
|
const previewShellOutPath = optionalArg(parsed, "preview-shell-out");
|
|
64
88
|
const cliCustomCss = optionalArg(parsed, "custom-css");
|
|
89
|
+
const timeoutRaw = optionalArg(parsed, "timeout-ms");
|
|
90
|
+
const timeoutMs = timeoutRaw ? Number(timeoutRaw) : undefined;
|
|
91
|
+
const debugDir = optionalArg(parsed, "debug-dir");
|
|
92
|
+
const noPreview = flagArg(parsed, "no-preview");
|
|
93
|
+
// Default true unless --no-preview or --preview=false
|
|
94
|
+
const previewExplicit = parsed.preview;
|
|
95
|
+
const preview =
|
|
96
|
+
!noPreview &&
|
|
97
|
+
previewExplicit !== false &&
|
|
98
|
+
previewExplicit !== "false";
|
|
99
|
+
const previewAutoStartExplicit = parsed["preview-auto-start"];
|
|
100
|
+
const previewAutoStart =
|
|
101
|
+
previewAutoStartExplicit !== false &&
|
|
102
|
+
previewAutoStartExplicit !== "false";
|
|
103
|
+
const open = flagArg(parsed, "open");
|
|
65
104
|
|
|
66
105
|
const config = loadConfig();
|
|
67
106
|
const wxAccount = config.wx.accounts[account] ?? config.wx.accounts[config.wx.defaultAccount];
|
|
@@ -70,15 +109,98 @@ ${CSS_DEMO}
|
|
|
70
109
|
wxAccount?.customCss,
|
|
71
110
|
);
|
|
72
111
|
const markdownRenderer = await resolveMarkdownRenderer(config);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
|
|
113
|
+
const displayTitle = title ?? "Wechat Preview Export";
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = await markdownRenderer.render({
|
|
117
|
+
markdownPath,
|
|
118
|
+
outPath,
|
|
119
|
+
account,
|
|
120
|
+
title,
|
|
121
|
+
previewShellOutPath,
|
|
122
|
+
customCss,
|
|
123
|
+
themeOverrides: wxAccount?.theme,
|
|
124
|
+
timeoutMs: timeoutMs !== undefined && Number.isFinite(timeoutMs) ? timeoutMs : undefined,
|
|
125
|
+
debugDir: debugDir ? resolve(process.cwd(), debugDir) : undefined,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let preview_url: string | undefined;
|
|
129
|
+
let preview_dashboard_url: string | undefined;
|
|
130
|
+
let preview_register_error: string | undefined;
|
|
131
|
+
|
|
132
|
+
if (preview) {
|
|
133
|
+
const reg = await registerPreviewEntry(
|
|
134
|
+
{
|
|
135
|
+
title: displayTitle,
|
|
136
|
+
account: result.account,
|
|
137
|
+
status: "success",
|
|
138
|
+
duration_ms: result.durationMs ?? 0,
|
|
139
|
+
markdown_path: resolve(process.cwd(), markdownPath),
|
|
140
|
+
html_path: result.htmlPath,
|
|
141
|
+
preview_style: result.previewStyle,
|
|
142
|
+
html: result.html,
|
|
143
|
+
debug: {
|
|
144
|
+
bundle_rebuilt: result.bundleRebuilt,
|
|
145
|
+
bundle_stale: result.bundleStale,
|
|
146
|
+
debug_dir: result.debugDir,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{ autoStart: previewAutoStart },
|
|
150
|
+
);
|
|
151
|
+
if (reg.ok) {
|
|
152
|
+
preview_url = reg.preview_url;
|
|
153
|
+
preview_dashboard_url = reg.dashboard_url;
|
|
154
|
+
if (open && preview_url) await openUrl(preview_url);
|
|
155
|
+
} else {
|
|
156
|
+
preview_register_error = reg.error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Keep semanticHtml out of default CLI output (large); it still lands in --debug-dir.
|
|
161
|
+
const { semanticHtml: _semanticHtml, ...resultRest } = result;
|
|
162
|
+
printResult(
|
|
163
|
+
{
|
|
164
|
+
...resultRest,
|
|
165
|
+
preview_url,
|
|
166
|
+
preview_dashboard_url,
|
|
167
|
+
preview_register_error,
|
|
168
|
+
},
|
|
169
|
+
renderWechatExport,
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (preview && error instanceof WechatExportError) {
|
|
173
|
+
const reg = await registerPreviewEntry(
|
|
174
|
+
{
|
|
175
|
+
title: error.title ?? displayTitle,
|
|
176
|
+
account: error.account,
|
|
177
|
+
status: "failed",
|
|
178
|
+
duration_ms: error.durationMs,
|
|
179
|
+
markdown_path: error.markdownPath,
|
|
180
|
+
error: error.message,
|
|
181
|
+
error_kind: error.kind,
|
|
182
|
+
debug: error.debug,
|
|
183
|
+
},
|
|
184
|
+
{ autoStart: previewAutoStart },
|
|
185
|
+
);
|
|
186
|
+
if (reg.ok) {
|
|
187
|
+
printResult(
|
|
188
|
+
{
|
|
189
|
+
ok: false,
|
|
190
|
+
error: error.message,
|
|
191
|
+
error_kind: error.kind,
|
|
192
|
+
duration_ms: error.durationMs,
|
|
193
|
+
debug: error.debug,
|
|
194
|
+
preview_url: reg.preview_url,
|
|
195
|
+
preview_dashboard_url: reg.dashboard_url,
|
|
196
|
+
},
|
|
197
|
+
renderWechatExport,
|
|
198
|
+
);
|
|
199
|
+
if (open && reg.preview_url) await openUrl(reg.preview_url);
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
84
206
|
}
|