@zzclub/pipeline 0.10.2 → 0.12.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 +8 -72
- package/package.json +1 -1
- package/src/adapter-types.ts +5 -0
- package/src/adapters/builtin-image-renderer.ts +7 -6
- package/src/adapters/builtin-markdown-renderer.ts +3 -0
- package/src/cli.ts +10 -4
- package/src/command-outcome.ts +50 -0
- package/src/commands/abandon.ts +13 -2
- package/src/commands/checkpoint.ts +3 -2
- package/src/commands/config.ts +47 -20
- package/src/commands/monitor.ts +21 -0
- package/src/commands/prepare-finalize.ts +2 -0
- package/src/commands/prepare.ts +3 -0
- package/src/commands/publish.ts +5 -2
- package/src/commands/render.ts +7 -3
- package/src/commands/republish.ts +4 -1
- package/src/commands/sync-blog.ts +4 -2
- package/src/commands/wechat-export.ts +6 -3
- package/src/config.ts +21 -2
- package/src/imgx/render-article.ts +8 -1
- package/src/imgx/render-card.ts +6 -1
- package/src/imgx/runtime.ts +1 -1
- package/src/logger.test.ts +28 -1
- package/src/logger.ts +39 -26
- package/src/monitor/client.ts +60 -0
- package/src/monitor/recorder.ts +142 -0
- package/src/monitor/runtime.ts +58 -0
- package/src/monitor/server.ts +170 -0
- package/src/monitor/store.ts +235 -0
- package/src/monitor/types.ts +77 -0
- package/src/monitor.test.ts +241 -0
- package/src/plugins.ts +4 -5
- package/src/providers/blog.ts +4 -1
- package/src/providers/cos.ts +3 -0
- package/src/providers/index.ts +7 -3
- package/src/providers/publish-core.ts +7 -1
- package/src/providers/wechat.ts +7 -2
- package/src/schema/config.account-name.test.ts +79 -0
- package/src/schema/config.ts +9 -3
- package/src/spawn.ts +4 -2
- package/src/state.ts +4 -0
- package/src/workflow.test.ts +3 -2
- package/src/commands/analytics.test.ts +0 -156
- package/src/commands/analytics.ts +0 -240
- package/src/commands/topic.test.ts +0 -146
- package/src/commands/topic.ts +0 -717
- package/src/db.test.ts +0 -47
- package/src/db.ts +0 -95
- package/src/schema/analytics.test.ts +0 -54
- package/src/schema/analytics.ts +0 -19
- package/src/schema/topic.test.ts +0 -69
- package/src/schema/topic.ts +0 -55
package/README.md
CHANGED
|
@@ -410,7 +410,7 @@ src/
|
|
|
410
410
|
| `review` | Update content review status |
|
|
411
411
|
| `abandon` | Mark one or more tasks as abandoned |
|
|
412
412
|
|
|
413
|
-
`ops` 组,
|
|
413
|
+
`ops` 组,11 个命令:
|
|
414
414
|
|
|
415
415
|
| 命令 | 说明 |
|
|
416
416
|
| --- | --- |
|
|
@@ -425,8 +425,6 @@ src/
|
|
|
425
425
|
| `hermes-metrics` | Show Hermes execution metrics per task |
|
|
426
426
|
| `wx-drafts` | List or get drafts from WeChat draft box |
|
|
427
427
|
| `wx-draft-delete` | Delete a draft from WeChat draft box |
|
|
428
|
-
| `topic` | 选题管理(添加、列表、更新、排期、复盘、放弃) |
|
|
429
|
-
| `analytics` | 发布数据录入和分析 |
|
|
430
428
|
|
|
431
429
|
## 常用命令
|
|
432
430
|
|
|
@@ -521,75 +519,6 @@ bun run src/cli.ts reset --state {state_path} --mode full # 完全放弃
|
|
|
521
519
|
|
|
522
520
|
每种模式均会设置 `redo_hint`,确保 Agent 恢复时知道从哪个子步骤开始。
|
|
523
521
|
|
|
524
|
-
### 选题管理
|
|
525
|
-
|
|
526
|
-
使用 `topic` 命令管理内容选题的完整生命周期:
|
|
527
|
-
|
|
528
|
-
```bash
|
|
529
|
-
# 添加选题
|
|
530
|
-
bun run src/cli.ts topic add \
|
|
531
|
-
--workspace {workspace} \
|
|
532
|
-
--title "AI 工具推荐" \
|
|
533
|
-
--priority high \
|
|
534
|
-
--tags "AI,工具"
|
|
535
|
-
|
|
536
|
-
# 列出选题
|
|
537
|
-
bun run src/cli.ts topic list --workspace {workspace}
|
|
538
|
-
bun run src/cli.ts topic list --workspace {workspace} --status backlog --priority high
|
|
539
|
-
|
|
540
|
-
# 更新选题(AI 评估)
|
|
541
|
-
bun run src/cli.ts topic update \
|
|
542
|
-
--workspace {workspace} \
|
|
543
|
-
--topic {topic_id} \
|
|
544
|
-
--ai-score 85 \
|
|
545
|
-
--ai-reason "基于热点趋势和受众匹配度"
|
|
546
|
-
|
|
547
|
-
# 排期
|
|
548
|
-
bun run src/cli.ts topic schedule \
|
|
549
|
-
--workspace {workspace} \
|
|
550
|
-
--topic {topic_id} \
|
|
551
|
-
--scheduled-date 2026-06-15 \
|
|
552
|
-
--target-account default
|
|
553
|
-
|
|
554
|
-
# 复盘(发布后)
|
|
555
|
-
bun run src/cli.ts topic retro \
|
|
556
|
-
--workspace {workspace} \
|
|
557
|
-
--topic {topic_id} \
|
|
558
|
-
--performance good \
|
|
559
|
-
--lessons "标题悬念效果好" \
|
|
560
|
-
--metrics-snapshot '{"reads": 1500, "likes": 45}'
|
|
561
|
-
|
|
562
|
-
# 放弃选题
|
|
563
|
-
bun run src/cli.ts topic abandon \
|
|
564
|
-
--workspace {workspace} \
|
|
565
|
-
--topic {topic_id} \
|
|
566
|
-
--reason "话题过时"
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
选题状态流转:`backlog` → `evaluating` → `scheduled` → `in_progress` → `published`(或 `abandoned`)
|
|
570
|
-
|
|
571
|
-
### 数据分析
|
|
572
|
-
|
|
573
|
-
使用 `analytics` 命令录入和分析发布后的数据:
|
|
574
|
-
|
|
575
|
-
```bash
|
|
576
|
-
# 录入发布数据
|
|
577
|
-
bun run src/cli.ts analytics record \
|
|
578
|
-
--state {workspace}/posts/{date-slug}/workflow-state.json \
|
|
579
|
-
--reads 1500 \
|
|
580
|
-
--likes 45 \
|
|
581
|
-
--favorites 23 \
|
|
582
|
-
--shares 12 \
|
|
583
|
-
--comments 8 \
|
|
584
|
-
--notes "标题效果好,转化率高"
|
|
585
|
-
|
|
586
|
-
# 列出历史数据
|
|
587
|
-
bun run src/cli.ts analytics list --workspace {workspace}
|
|
588
|
-
bun run src/cli.ts analytics list --workspace {workspace} --days 30 --sort reads
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
数据存储在 `{workspace}/zzhub.db`(SQLite 数据库),支持复杂查询和统计分析。
|
|
592
|
-
|
|
593
522
|
### Blog 同步
|
|
594
523
|
|
|
595
524
|
```bash
|
|
@@ -937,6 +866,13 @@ bun install --global .
|
|
|
937
866
|
|
|
938
867
|
Markdown → WeChat HTML 转换通过插件系统完成(默认使用内置 `builtin-wechat-preview` 适配器)。
|
|
939
868
|
|
|
869
|
+
## 本机多任务监控
|
|
870
|
+
|
|
871
|
+
`zzp monitor start` 按需启动共享 HTTP/SSE 服务,`monitor status` 查看状态,`monitor stop` 只停止监控。
|
|
872
|
+
CLI 独立记录执行和错误,GUI 可同时订阅多个任务。新版业务失败会退出 1,调用方仍应读取失败响应和最新任务状态。
|
|
873
|
+
|
|
874
|
+
详见 [监控接口与退出码迁移](docs/monitor.md)。
|
|
875
|
+
|
|
940
876
|
## 测试与验证
|
|
941
877
|
|
|
942
878
|
```bash
|
package/package.json
CHANGED
package/src/adapter-types.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { RenderAsset, RoutePrimary, WorkflowState } from "./state";
|
|
12
|
+
import type { MonitorProgress } from "./monitor/types";
|
|
12
13
|
|
|
13
14
|
// ── Doctor check ──────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -26,6 +27,8 @@ export interface PipelinePluginDoctorCheck {
|
|
|
26
27
|
* to runRenderArticleCli and runRenderCardCli via CLI argv.
|
|
27
28
|
*/
|
|
28
29
|
export interface ImageRenderInput {
|
|
30
|
+
/** 可选进度通知,旧插件可忽略。 */
|
|
31
|
+
onProgress?: (progress: MonitorProgress) => void;
|
|
29
32
|
/** Workflow state providing context (route, metadata, etc.) */
|
|
30
33
|
state: WorkflowState;
|
|
31
34
|
|
|
@@ -117,6 +120,8 @@ export interface ImageRenderPlugin {
|
|
|
117
120
|
* Maps to ExportMarkdownToWechatHtmlInput from wechat-preview.
|
|
118
121
|
*/
|
|
119
122
|
export interface MarkdownRenderInput {
|
|
123
|
+
/** 可选进度通知,旧插件可忽略。 */
|
|
124
|
+
onProgress?: (progress: MonitorProgress) => void;
|
|
120
125
|
/** Path to the markdown source file */
|
|
121
126
|
markdownPath: string;
|
|
122
127
|
|
|
@@ -138,6 +138,7 @@ async function renderCover(
|
|
|
138
138
|
outputDir: string,
|
|
139
139
|
visualParams: AccountVisualParams | null,
|
|
140
140
|
highlightWords: string[],
|
|
141
|
+
onProgress?: ImageRenderInput["onProgress"],
|
|
141
142
|
): Promise<RenderAsset> {
|
|
142
143
|
const coverOut = join(outputDir, "cover.png");
|
|
143
144
|
const route = template === "wechat-cover-split" ? "wechat-article" : "wechat-newspic";
|
|
@@ -152,12 +153,12 @@ async function renderCover(
|
|
|
152
153
|
}
|
|
153
154
|
cmdParts.push("--highlight-words", highlightWords.join(","));
|
|
154
155
|
cmdParts.push("--out", coverOut);
|
|
155
|
-
runRenderCardCli(cmdParts);
|
|
156
|
+
runRenderCardCli(cmdParts, onProgress);
|
|
156
157
|
} else {
|
|
157
158
|
const cmdParts = ["--template", "poster-3-4", "--text", title];
|
|
158
159
|
appendPosterVisualArgs(cmdParts, visualParams, highlightWords);
|
|
159
160
|
cmdParts.push("--out", coverOut);
|
|
160
|
-
runRenderCardCli(cmdParts);
|
|
161
|
+
runRenderCardCli(cmdParts, onProgress);
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
return { kind: "cover", route: route as RenderAsset["route"], path: coverOut };
|
|
@@ -231,7 +232,7 @@ async function renderLongformPages(
|
|
|
231
232
|
|
|
232
233
|
let result: RenderArticleResult;
|
|
233
234
|
try {
|
|
234
|
-
result = runRenderArticleCli(pageParts);
|
|
235
|
+
result = runRenderArticleCli(pageParts, input.onProgress);
|
|
235
236
|
} finally {
|
|
236
237
|
if (!keepTempFiles) {
|
|
237
238
|
await rm(tempDir, { recursive: true, force: true });
|
|
@@ -309,7 +310,7 @@ export const builtinImageRenderer: ImageRenderPlugin = {
|
|
|
309
310
|
|
|
310
311
|
if (route === "wechat-article") {
|
|
311
312
|
// Article: cover only (wechat-cover-split)
|
|
312
|
-
const cover = await renderCover("wechat-cover-split", title, outputDir, vp, highlightWords);
|
|
313
|
+
const cover = await renderCover("wechat-cover-split", title, outputDir, vp, highlightWords, input.onProgress);
|
|
313
314
|
return { assets: [cover], pageCount: 1, pages: [{ page: 1, imageCount: 0, imageSources: [] }] };
|
|
314
315
|
}
|
|
315
316
|
|
|
@@ -339,13 +340,13 @@ export const builtinImageRenderer: ImageRenderPlugin = {
|
|
|
339
340
|
if (!isLong) {
|
|
340
341
|
// Short: single poster cover
|
|
341
342
|
const coverTitle = generateCoverTitle(title);
|
|
342
|
-
const cover = await renderCover("poster-3-4", coverTitle, outputDir, vp, highlightWords);
|
|
343
|
+
const cover = await renderCover("poster-3-4", coverTitle, outputDir, vp, highlightWords, input.onProgress);
|
|
343
344
|
return { assets: [cover], pageCount: 1, pages: [{ page: 1, imageCount: 0, imageSources: [] }] };
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
// Long: cover + article pages
|
|
347
348
|
const coverTitle = generateCoverTitle(title);
|
|
348
|
-
const cover = await renderCover("poster-3-4", coverTitle, outputDir, vp, highlightWords);
|
|
349
|
+
const cover = await renderCover("poster-3-4", coverTitle, outputDir, vp, highlightWords, input.onProgress);
|
|
349
350
|
|
|
350
351
|
const bodyImages = input.bodyImages ?? [];
|
|
351
352
|
const pageResult = await renderLongformPages(input, outputDir, newspicRenderSpec, bodyImages, vp);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { exportMarkdownToWechatHtml } from "../wechat-preview";
|
|
9
9
|
import { findChrome } from "../imgx/runtime";
|
|
10
|
+
import { notifyProgress } from "../monitor/recorder";
|
|
10
11
|
import type {
|
|
11
12
|
MarkdownRenderPlugin,
|
|
12
13
|
MarkdownRenderInput,
|
|
@@ -43,6 +44,7 @@ export const builtinMarkdownRenderer: MarkdownRenderPlugin = {
|
|
|
43
44
|
},
|
|
44
45
|
|
|
45
46
|
async render(input: MarkdownRenderInput): Promise<MarkdownRenderOutput> {
|
|
47
|
+
notifyProgress(input.onProgress, { stage: "render.html", message: "正在导出微信 HTML" });
|
|
46
48
|
const result = await exportMarkdownToWechatHtml({
|
|
47
49
|
markdownPath: input.markdownPath,
|
|
48
50
|
outPath: input.outPath,
|
|
@@ -54,6 +56,7 @@ export const builtinMarkdownRenderer: MarkdownRenderPlugin = {
|
|
|
54
56
|
timeoutMs: input.timeoutMs,
|
|
55
57
|
debugDir: input.debugDir ?? undefined,
|
|
56
58
|
});
|
|
59
|
+
notifyProgress(input.onProgress, { stage: "render.html", message: "微信 HTML 导出完成", current: 1, total: 1, unit: "files" });
|
|
57
60
|
|
|
58
61
|
return {
|
|
59
62
|
html: result.html,
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from "path";
|
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { getDailyLogPath, runWithCommandLog } from "./logger";
|
|
7
7
|
import { formatUsage, getCommandRegistry } from "./plugins";
|
|
8
|
+
import { outcomeExitCode } from "./command-outcome";
|
|
8
9
|
|
|
9
10
|
const COMMANDS = getCommandRegistry();
|
|
10
11
|
|
|
@@ -37,14 +38,19 @@ async function main() {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// monitor 输出包含本机认证令牌,不能经过日志复制或监控自身。
|
|
42
|
+
const outcome = cmd === "monitor"
|
|
43
|
+
? await command.handler(args.slice(1))
|
|
44
|
+
: await runWithCommandLog(cmd, args.slice(1), () => command.handler(args.slice(1)));
|
|
45
|
+
if (outcome) {
|
|
46
|
+
process.exitCode = outcomeExitCode(outcome);
|
|
47
|
+
for (const error of outcome.errors ?? []) console.error(`[zzhub-pipeline ${cmd}] ${error.code}: ${error.message}`);
|
|
48
|
+
}
|
|
43
49
|
} catch (err) {
|
|
44
50
|
const msg = err instanceof Error ? err.message : String(err);
|
|
45
51
|
console.error(`[zzhub-pipeline ${cmd}] Error: ${msg}`);
|
|
46
52
|
console.error(`[zzhub-pipeline ${cmd}] Full log: ${getDailyLogPath()}`);
|
|
47
|
-
process.
|
|
53
|
+
process.exitCode = 1;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PublishResult } from "./state";
|
|
2
|
+
|
|
3
|
+
/** 同一次执行共用的业务结果,独立于工作流是否完成。 */
|
|
4
|
+
export interface CommandOutcome {
|
|
5
|
+
status: "success" | "skipped" | "waiting" | "partial_failure" | "failed";
|
|
6
|
+
errors?: CommandError[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** 可供终端和监控展示的错误,不推测可重试性。 */
|
|
10
|
+
export interface CommandError {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
stage?: string;
|
|
14
|
+
route?: string;
|
|
15
|
+
account?: string;
|
|
16
|
+
cause?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 保留已有错误码和堆栈,未知异常使用统一兜底。 */
|
|
20
|
+
export function commandError(error: unknown, code = "COMMAND_FAILED"): CommandError {
|
|
21
|
+
return {
|
|
22
|
+
code: error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : code,
|
|
23
|
+
message: error instanceof Error ? error.message : String(error),
|
|
24
|
+
...(error instanceof Error && error.stack ? { cause: error.stack } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 出口只使用 0/1;等待输入和幂等跳过都是正常完成命令。 */
|
|
29
|
+
export function outcomeExitCode(outcome: CommandOutcome): number {
|
|
30
|
+
return outcome.status === "failed" || outcome.status === "partial_failure" ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 仅汇总本次请求目标,避免历史失败污染新的追加发布。 */
|
|
34
|
+
export function publishOutcome(results: PublishResult[], skipped = false): CommandOutcome {
|
|
35
|
+
const failures = results.filter((result) => result.status === "failed");
|
|
36
|
+
if (failures.length) {
|
|
37
|
+
return {
|
|
38
|
+
status: results.some((result) => result.status === "success") ? "partial_failure" : "failed",
|
|
39
|
+
errors: failures.map((result) => ({
|
|
40
|
+
code: "PUBLISH_TARGET_FAILED",
|
|
41
|
+
message: result.detail || "发布失败,provider 未提供原因",
|
|
42
|
+
stage: "publish",
|
|
43
|
+
route: result.route,
|
|
44
|
+
account: result.account,
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (results.some((result) => result.status === "handoff")) return { status: "waiting" };
|
|
49
|
+
return { status: skipped || !results.length || results.every((result) => result.status === "skipped") ? "skipped" : "success" };
|
|
50
|
+
}
|
package/src/commands/abandon.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { parseArgs, optionalArg } from "../args";
|
|
|
17
17
|
import { printResult, renderAbandon } from "../output";
|
|
18
18
|
import { filterActiveTasks, getTaskByStatePath, listTasks, type ListedTask } from "../task-manager";
|
|
19
19
|
import { updateState } from "../state";
|
|
20
|
+
import type { CommandOutcome } from "../command-outcome";
|
|
20
21
|
|
|
21
22
|
// ── Result type ───────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -146,7 +147,7 @@ async function runInteractive(items: ListedTask[]): Promise<ListedTask[]> {
|
|
|
146
147
|
|
|
147
148
|
// ── Command entry point ───────────────────────────────────────────────────────
|
|
148
149
|
|
|
149
|
-
export async function abandon(args: string[]): Promise<void> {
|
|
150
|
+
export async function abandon(args: string[]): Promise<void | CommandOutcome> {
|
|
150
151
|
const parsed = parseArgs(args);
|
|
151
152
|
|
|
152
153
|
if (parsed.help) {
|
|
@@ -184,7 +185,7 @@ Options:
|
|
|
184
185
|
}
|
|
185
186
|
const result = await abandonTask(task);
|
|
186
187
|
printResult([result], renderAbandon);
|
|
187
|
-
return;
|
|
188
|
+
return abandonOutcome([result]);
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
// ── Interactive: TTY checkbox ─────────────────────────────────────────────
|
|
@@ -199,4 +200,14 @@ Options:
|
|
|
199
200
|
|
|
200
201
|
const results = await Promise.all(chosen.map(abandonTask));
|
|
201
202
|
printResult(results, renderAbandon);
|
|
203
|
+
return abandonOutcome(results);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 批量操作保留成功项,失败项决定本次执行结果。 */
|
|
207
|
+
function abandonOutcome(results: AbandonResult[]): CommandOutcome {
|
|
208
|
+
const failed = results.filter((result) => !result.ok);
|
|
209
|
+
return {
|
|
210
|
+
status: failed.length ? (failed.length < results.length ? "partial_failure" : "failed") : "success",
|
|
211
|
+
errors: failed.map((result) => ({ code: "ABANDON_FAILED", message: result.error || "放弃任务失败" })),
|
|
212
|
+
};
|
|
202
213
|
}
|
|
@@ -15,8 +15,9 @@ import { getTaskByStatePath } from "../task-manager";
|
|
|
15
15
|
import { parseTaskViewMode, renderTaskStatusMarkdown } from "../task-views";
|
|
16
16
|
import type { PhaseName } from "../state";
|
|
17
17
|
import { validateForPhase } from "../state";
|
|
18
|
+
import type { CommandOutcome } from "../command-outcome";
|
|
18
19
|
|
|
19
|
-
export async function checkpoint(args: string[]): Promise<void> {
|
|
20
|
+
export async function checkpoint(args: string[]): Promise<void | CommandOutcome> {
|
|
20
21
|
const parsed = parseArgs(args);
|
|
21
22
|
|
|
22
23
|
if (parsed.help) {
|
|
@@ -61,6 +62,6 @@ Options:
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
if (errors.length > 0) {
|
|
64
|
-
|
|
65
|
+
return { status: "failed", errors: errors.map((error) => ({ code: "CHECKPOINT_FAILED", message: `${error.field}: ${error.message}`, stage: phase })) };
|
|
65
66
|
}
|
|
66
67
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -5,13 +5,17 @@ import {
|
|
|
5
5
|
configSummary,
|
|
6
6
|
getConfigValue,
|
|
7
7
|
loadConfig,
|
|
8
|
-
|
|
8
|
+
normalizeConfig,
|
|
9
9
|
redactConfig,
|
|
10
10
|
redactConfigValue,
|
|
11
11
|
saveConfig,
|
|
12
12
|
setConfigValue,
|
|
13
13
|
} from "../config";
|
|
14
14
|
|
|
15
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
export async function configCommand(args: string[]): Promise<void> {
|
|
16
20
|
const parsed = parseArgs(args);
|
|
17
21
|
|
|
@@ -26,6 +30,11 @@ Options:
|
|
|
26
30
|
--export Print full config as JSON (secrets redacted; use --raw to show all)
|
|
27
31
|
--import Path to a JSON file to merge into current config
|
|
28
32
|
--raw Show secrets unredacted (use with --export)
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
zzhub-pipeline config --key wx.defaultAccount
|
|
36
|
+
zzhub-pipeline config --key wx.accounts.default.name --value "大号(早早集市)"
|
|
37
|
+
zzhub-pipeline config --export
|
|
29
38
|
`.trim());
|
|
30
39
|
return;
|
|
31
40
|
}
|
|
@@ -50,27 +59,45 @@ Options:
|
|
|
50
59
|
throw new Error(`Failed to read import file: ${err instanceof Error ? err.message : String(err)}`);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
// Merge:
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Merge: top-level shallow; wx.accounts deep-merge per key so partial imports
|
|
63
|
+
// keep existing fields (e.g. display `name`) unless overridden.
|
|
64
|
+
const importedObj = isPlainObject(imported) ? imported : {};
|
|
65
|
+
const importedWx = isPlainObject(importedObj.wx) ? importedObj.wx : {};
|
|
66
|
+
const importedAccounts = isPlainObject(importedWx.accounts) ? importedWx.accounts : {};
|
|
67
|
+
const mergedAccounts: Record<string, unknown> = { ...config.wx.accounts };
|
|
68
|
+
for (const [key, value] of Object.entries(importedAccounts)) {
|
|
69
|
+
const accountKey = key.trim();
|
|
70
|
+
if (!accountKey) continue;
|
|
71
|
+
const existing = isPlainObject(mergedAccounts[accountKey])
|
|
72
|
+
? mergedAccounts[accountKey]
|
|
73
|
+
: {};
|
|
74
|
+
const incoming = isPlainObject(value) ? value : {};
|
|
75
|
+
mergedAccounts[accountKey] = { ...existing, ...incoming };
|
|
76
|
+
}
|
|
63
77
|
const raw = {
|
|
64
|
-
paths: { ...config.paths, ...(importedObj.paths
|
|
65
|
-
services: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
paths: { ...config.paths, ...(isPlainObject(importedObj.paths) ? importedObj.paths : {}) },
|
|
79
|
+
services: {
|
|
80
|
+
...config.services,
|
|
81
|
+
...(isPlainObject(importedObj.services) ? importedObj.services : {}),
|
|
82
|
+
},
|
|
83
|
+
commands: {
|
|
84
|
+
...config.commands,
|
|
85
|
+
...(isPlainObject(importedObj.commands) ? importedObj.commands : {}),
|
|
86
|
+
},
|
|
87
|
+
wx: {
|
|
88
|
+
...config.wx,
|
|
89
|
+
...importedWx,
|
|
90
|
+
accounts: mergedAccounts,
|
|
91
|
+
},
|
|
92
|
+
cos: { ...config.cos, ...(isPlainObject(importedObj.cos) ? importedObj.cos : {}) },
|
|
93
|
+
plugins: {
|
|
94
|
+
...config.plugins,
|
|
95
|
+
...(isPlainObject(importedObj.plugins) ? importedObj.plugins : {}),
|
|
96
|
+
},
|
|
97
|
+
imgx: { ...config.imgx, ...(isPlainObject(importedObj.imgx) ? importedObj.imgx : {}) },
|
|
71
98
|
};
|
|
72
|
-
//
|
|
73
|
-
const merged =
|
|
99
|
+
// Soft-fill known account display names + Zod defaults / strip unknowns
|
|
100
|
+
const merged = normalizeConfig(raw);
|
|
74
101
|
|
|
75
102
|
saveConfig(merged);
|
|
76
103
|
printResult(configSummary(merged), renderConfig);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parseArgs } from "../args";
|
|
2
|
+
import { printHelp, printResult } from "../output";
|
|
3
|
+
import { monitorHealth, readMonitorDescriptor, startMonitor, stopMonitor } from "../monitor/client";
|
|
4
|
+
import { serveMonitor } from "../monitor/server";
|
|
5
|
+
|
|
6
|
+
/** 服务管理不进入业务日志,防止描述文件令牌被日志复制。 */
|
|
7
|
+
export async function monitor(args: string[]): Promise<void> {
|
|
8
|
+
const parsed = parseArgs(args);
|
|
9
|
+
const action = (JSON.parse(String(parsed._)) as string[])[0] || "status";
|
|
10
|
+
if (parsed.help) { printHelp("Usage: zzp monitor start | serve | status | stop\nLocal HTTP/SSE monitoring; business commands run independently."); return; }
|
|
11
|
+
if (action === "start") { printResult(await startMonitor()); return; }
|
|
12
|
+
if (action === "serve") { const server = await serveMonitor(); printResult(server?.descriptor || await startMonitor()); return; }
|
|
13
|
+
if (action === "stop") { printResult({ stopped: await stopMonitor() }); return; }
|
|
14
|
+
if (action === "status") {
|
|
15
|
+
const descriptor = readMonitorDescriptor();
|
|
16
|
+
const running = !!descriptor && await monitorHealth(descriptor);
|
|
17
|
+
printResult({ running, ...(running ? { url: descriptor.url, instance_id: descriptor.instance_id, version: 1 } : {}) });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unknown monitor action: ${action}`);
|
|
21
|
+
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getCanonicalStatePath,
|
|
33
33
|
} from "../state";
|
|
34
34
|
import { loadTaskState } from "../task-manager";
|
|
35
|
+
import { reportProgress } from "../monitor/recorder";
|
|
35
36
|
import {
|
|
36
37
|
extractHighlightWords,
|
|
37
38
|
buildFrontmatter,
|
|
@@ -330,6 +331,7 @@ Options:
|
|
|
330
331
|
});
|
|
331
332
|
|
|
332
333
|
// Write post.md
|
|
334
|
+
reportProgress({ stage: "prepare.save", message: "保存文章和工作流状态" });
|
|
333
335
|
const postContent = `${frontmatter}\n\n${removePageMarkers(body)}`;
|
|
334
336
|
const postPath = join(assetPath, "post.md");
|
|
335
337
|
const postTempPath = `${postPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
package/src/commands/prepare.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
writeState,
|
|
36
36
|
} from "../state";
|
|
37
37
|
import { loadTaskState } from "../task-manager";
|
|
38
|
+
import { reportProgress } from "../monitor/recorder";
|
|
38
39
|
import { resolveFullRoute } from "../routes";
|
|
39
40
|
import { resolveAuthoring, hasStyleRequest } from "../profiles";
|
|
40
41
|
import { parseAccountName, parseRoutePrimary } from "../publish-targets";
|
|
@@ -200,6 +201,7 @@ Options:
|
|
|
200
201
|
state.intent.requires.publish = state.intent.task_kind === "publish";
|
|
201
202
|
|
|
202
203
|
// ── Step 2: Author select ──
|
|
204
|
+
reportProgress({ stage: "prepare.author", message: "确定写作规则" });
|
|
203
205
|
const styleRequest =
|
|
204
206
|
isStyleRequest || hasStyleRequest(intentText);
|
|
205
207
|
|
|
@@ -219,6 +221,7 @@ Options:
|
|
|
219
221
|
// In the current workflow, Writer/Style already own the LLM rewrite.
|
|
220
222
|
// Prepare records the authoring snapshot, then always formats the final body.
|
|
221
223
|
const bodyFormatted = formatArticle(cleanBody);
|
|
224
|
+
reportProgress({ stage: "prepare.metadata", message: "整理文章元信息" });
|
|
222
225
|
|
|
223
226
|
// ── Step 4: Asset meta ──
|
|
224
227
|
// Determine title
|
package/src/commands/publish.ts
CHANGED
|
@@ -34,8 +34,9 @@ import {
|
|
|
34
34
|
upsertPublishResult,
|
|
35
35
|
} from "../providers/publish-core";
|
|
36
36
|
import { loadTaskState } from "../task-manager";
|
|
37
|
+
import { publishOutcome, type CommandOutcome } from "../command-outcome";
|
|
37
38
|
|
|
38
|
-
export async function publish(args: string[]): Promise<void> {
|
|
39
|
+
export async function publish(args: string[]): Promise<void | CommandOutcome> {
|
|
39
40
|
const parsed = parseArgs(args);
|
|
40
41
|
|
|
41
42
|
if (parsed.help) {
|
|
@@ -119,7 +120,7 @@ Options:
|
|
|
119
120
|
dry_run: true,
|
|
120
121
|
...(errors.length > 0 ? { errors } : {}),
|
|
121
122
|
}, renderPublish);
|
|
122
|
-
return;
|
|
123
|
+
return publishOutcome(results, true);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// Check if all targets are done
|
|
@@ -151,6 +152,8 @@ Options:
|
|
|
151
152
|
output.errors = errors;
|
|
152
153
|
}
|
|
153
154
|
printResult(output, renderPublish);
|
|
155
|
+
return publishOutcome(state.publish.results.filter((result) => targets.some((target) =>
|
|
156
|
+
target.route === result.route && target.account === result.account)), results.length === 0);
|
|
154
157
|
} finally {
|
|
155
158
|
await releaseOperationLock();
|
|
156
159
|
}
|
package/src/commands/render.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
} from "../text";
|
|
39
39
|
import { getLongformTheme } from "../routes";
|
|
40
40
|
import { collectNewspicRequiredMarkers } from "../workflow-materials";
|
|
41
|
+
import type { CommandOutcome } from "../command-outcome";
|
|
42
|
+
import { reportProgress } from "../monitor/recorder";
|
|
41
43
|
|
|
42
44
|
function getNewspicRenderSpec(state: WorkflowState): NewspicRenderSpec {
|
|
43
45
|
const spec = normalizeNewspicRenderSpec(state.intent.newspic_render);
|
|
@@ -47,7 +49,7 @@ function getNewspicRenderSpec(state: WorkflowState): NewspicRenderSpec {
|
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export async function render(args: string[]): Promise<void> {
|
|
52
|
+
export async function render(args: string[]): Promise<void | CommandOutcome> {
|
|
51
53
|
const parsed = parseArgs(args);
|
|
52
54
|
|
|
53
55
|
if (parsed.help) {
|
|
@@ -182,7 +184,7 @@ Options:
|
|
|
182
184
|
message: `Waiting for ${markers.length} body image(s)`,
|
|
183
185
|
phase: state.phase.current,
|
|
184
186
|
}, renderRender);
|
|
185
|
-
return;
|
|
187
|
+
return { status: "waiting" };
|
|
186
188
|
}
|
|
187
189
|
} else if (state.images.body_inputs.scope === "newspic-longform") {
|
|
188
190
|
state.images.body_inputs = defaultBodyInputs();
|
|
@@ -196,13 +198,14 @@ Options:
|
|
|
196
198
|
skip_render: true,
|
|
197
199
|
phase: state.phase.current,
|
|
198
200
|
}, renderRender);
|
|
199
|
-
return;
|
|
201
|
+
return { status: "skipped" };
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
// ── Invoke image renderer adapter ───────────────────────────────
|
|
203
205
|
|
|
204
206
|
const config = loadConfig();
|
|
205
207
|
const imageRenderer = await resolveImageRenderer(config);
|
|
208
|
+
reportProgress({ stage: "render.adapter", message: "开始生成图片" });
|
|
206
209
|
|
|
207
210
|
const bodyImages = (
|
|
208
211
|
state.images.body_inputs.scope === "newspic-longform" &&
|
|
@@ -212,6 +215,7 @@ Options:
|
|
|
212
215
|
: [];
|
|
213
216
|
|
|
214
217
|
const renderResult = await imageRenderer.render({
|
|
218
|
+
onProgress: reportProgress,
|
|
215
219
|
state,
|
|
216
220
|
bodyText: cleanBody,
|
|
217
221
|
outputDir,
|
|
@@ -36,8 +36,9 @@ import {
|
|
|
36
36
|
upsertPublishResult,
|
|
37
37
|
} from "../providers/publish-core";
|
|
38
38
|
import { loadTaskState } from "../task-manager";
|
|
39
|
+
import { publishOutcome, type CommandOutcome } from "../command-outcome";
|
|
39
40
|
|
|
40
|
-
export async function republish(args: string[]): Promise<void> {
|
|
41
|
+
export async function republish(args: string[]): Promise<void | CommandOutcome> {
|
|
41
42
|
const parsed = parseArgs(args);
|
|
42
43
|
|
|
43
44
|
if (parsed.help) {
|
|
@@ -182,6 +183,8 @@ Examples:
|
|
|
182
183
|
output.errors = errors;
|
|
183
184
|
}
|
|
184
185
|
printResult(output);
|
|
186
|
+
return publishOutcome(dryRun ? results : state.publish.results.filter((result) => newTargets.some((target) =>
|
|
187
|
+
target.route === result.route && target.account === result.account)), dryRun || results.length === 0);
|
|
185
188
|
} finally {
|
|
186
189
|
await releaseOperationLock();
|
|
187
190
|
}
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
} from "../state";
|
|
9
9
|
import { loadTaskState } from "../task-manager";
|
|
10
10
|
import { publishBlogRoute } from "../providers/blog";
|
|
11
|
+
import { publishOutcome, type CommandOutcome } from "../command-outcome";
|
|
11
12
|
|
|
12
|
-
export async function syncBlog(args: string[]): Promise<void> {
|
|
13
|
+
export async function syncBlog(args: string[]): Promise<void | CommandOutcome> {
|
|
13
14
|
const parsed = parseArgs(args);
|
|
14
15
|
|
|
15
16
|
if (parsed.help) {
|
|
@@ -50,7 +51,7 @@ Options:
|
|
|
50
51
|
|
|
51
52
|
if (dryRun) {
|
|
52
53
|
printResult({ ...result, mode: state.mode }, renderSyncBlog);
|
|
53
|
-
return;
|
|
54
|
+
return publishOutcome([result], true);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// Write result back to state
|
|
@@ -67,6 +68,7 @@ Options:
|
|
|
67
68
|
...result,
|
|
68
69
|
mode: finalState.mode,
|
|
69
70
|
}, renderSyncBlog);
|
|
71
|
+
return publishOutcome([result]);
|
|
70
72
|
} finally {
|
|
71
73
|
await releaseOperationLock();
|
|
72
74
|
}
|