@zzclub/pipeline 0.4.0 → 0.9.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 +102 -3
- package/package.json +1 -1
- package/src/adapter-types.ts +6 -0
- package/src/adapters/builtin-image-renderer.ts +7 -1
- package/src/adapters/builtin-markdown-renderer.ts +1 -0
- package/src/commands/analytics.test.ts +155 -0
- package/src/commands/analytics.ts +223 -0
- package/src/commands/init.ts +42 -3
- package/src/commands/publish.ts +44 -67
- package/src/commands/republish.ts +146 -0
- package/src/commands/sync-blog.ts +19 -3
- package/src/commands/topic.test.ts +146 -0
- package/src/commands/topic.ts +717 -0
- package/src/commands/wechat-export.ts +2 -0
- package/src/commands/wx-draft-delete.ts +36 -0
- package/src/commands/wx-drafts.ts +47 -0
- package/src/config.ts +0 -0
- package/src/db.test.ts +47 -0
- package/src/db.ts +95 -0
- package/src/imgx/assets/icons/ancientone-logo.png +0 -0
- package/src/imgx/assets/templates/wechat-cover-split.html +1 -1
- package/src/imgx/render-card.ts +1 -5
- package/src/plugins.ts +11 -1
- package/src/providers/blog.ts +9 -3
- package/src/providers/index.test.ts +1 -1
- package/src/providers/index.ts +29 -5
- package/src/providers/publish-core.ts +124 -0
- package/src/providers/wechat.ts +260 -28
- package/src/routes.ts +1 -1
- package/src/schema/analytics.test.ts +54 -0
- package/src/schema/analytics.ts +19 -0
- package/src/schema/config.ts +25 -1
- package/src/schema/state.ts +10 -0
- package/src/schema/topic.test.ts +69 -0
- package/src/schema/topic.ts +55 -0
- package/src/state.ts +1 -0
- package/src/text.test.ts +11 -6
- package/src/text.ts +27 -9
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +2 -7
- package/src/wechat-preview/assets/browser-dist/assets/{style-BYNXnUvc.css → style-vhHYSP8T.css} +1 -1
- package/src/wechat-preview/assets/browser-dist/editor-export.js +27 -27
- package/src/wechat-preview/browser/editor-export.css +248 -159
- package/src/wechat-preview/index.ts +28 -11
- package/src/wechat-preview/themes.ts +70 -24
- package/src/wechat-preview/wechat-formatter.test.ts +0 -1
- package/src/wechat-preview/wechat-formatter.ts +48 -23
- package/src/workflow.test.ts +294 -1
- package/src/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
|
@@ -73,6 +73,16 @@ const THEMES: Record<string, WechatPreviewTheme> = {
|
|
|
73
73
|
"--muted": "210 14% 96%",
|
|
74
74
|
"--muted-foreground": "215 8% 42%",
|
|
75
75
|
"--border": "220 10% 86%",
|
|
76
|
+
"--brand": "#ca6093",
|
|
77
|
+
"--brand-soft": "#d4789e",
|
|
78
|
+
"--brand-bg": "rgba(202, 96, 147, 0.08)",
|
|
79
|
+
"--brand-bg-deep": "rgba(202, 96, 147, 0.14)",
|
|
80
|
+
"--text": "#202124",
|
|
81
|
+
"--text-soft": "#5f6368",
|
|
82
|
+
"--text-mid": "#3c4043",
|
|
83
|
+
"--divider": "#dadce0",
|
|
84
|
+
"--bg-page": "#ffffff",
|
|
85
|
+
"--bg-warm": "#f8f9fa",
|
|
76
86
|
},
|
|
77
87
|
exportTheme: {
|
|
78
88
|
containerStyle: buildContainerStyle({
|
|
@@ -89,9 +99,9 @@ const THEMES: Record<string, WechatPreviewTheme> = {
|
|
|
89
99
|
"'SweiCurveLeg', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
90
100
|
bodyColor: "#202124",
|
|
91
101
|
mutedColor: "#5f6368",
|
|
92
|
-
primaryColor: "#
|
|
102
|
+
primaryColor: "#ca6093",
|
|
93
103
|
dividerColor: "#dadce0",
|
|
94
|
-
blockquoteBorderColor: "#
|
|
104
|
+
blockquoteBorderColor: "#ca6093",
|
|
95
105
|
bodyLineHeight: "1.92",
|
|
96
106
|
bodyLetterSpacing: "0.03em",
|
|
97
107
|
},
|
|
@@ -101,39 +111,75 @@ const THEMES: Record<string, WechatPreviewTheme> = {
|
|
|
101
111
|
account: "ancientone",
|
|
102
112
|
editorVars: {
|
|
103
113
|
...BASE_EDITOR_VARS,
|
|
104
|
-
"--primary": "
|
|
105
|
-
"--ring": "
|
|
106
|
-
"--accent": "
|
|
107
|
-
"--accent-foreground": "
|
|
108
|
-
"--muted": "
|
|
109
|
-
"--border": "
|
|
114
|
+
"--primary": "337 38% 58%",
|
|
115
|
+
"--ring": "337 38% 58%",
|
|
116
|
+
"--accent": "337 30% 96%",
|
|
117
|
+
"--accent-foreground": "337 28% 22%",
|
|
118
|
+
"--muted": "338 25% 97%",
|
|
119
|
+
"--border": "337 20% 88%",
|
|
120
|
+
"--brand": "#ca6093",
|
|
121
|
+
"--brand-soft": "#d4789e",
|
|
122
|
+
"--brand-bg": "rgba(202, 96, 147, 0.08)",
|
|
123
|
+
"--brand-bg-deep": "rgba(202, 96, 147, 0.14)",
|
|
124
|
+
"--text": "#2c2c2c",
|
|
125
|
+
"--text-soft": "#8c8c8c",
|
|
126
|
+
"--text-mid": "#5c5c5c",
|
|
127
|
+
"--divider": "#d8d0d3",
|
|
128
|
+
"--bg-page": "#fefefe",
|
|
129
|
+
"--bg-warm": "#faf8f9",
|
|
110
130
|
},
|
|
111
131
|
exportTheme: {
|
|
112
132
|
containerStyle: buildContainerStyle({
|
|
113
|
-
background: "#
|
|
114
|
-
color: "#
|
|
133
|
+
background: "#fefefe",
|
|
134
|
+
color: "#2c2c2c",
|
|
115
135
|
fontFamily:
|
|
116
|
-
"'
|
|
117
|
-
lineHeight: "
|
|
118
|
-
letterSpacing: "0.
|
|
136
|
+
"'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, 'SF Pro Text', 'Noto Sans SC', sans-serif",
|
|
137
|
+
lineHeight: "2.0",
|
|
138
|
+
letterSpacing: "0.015em",
|
|
119
139
|
}),
|
|
120
140
|
footerText: "公众号 · 古一软件",
|
|
121
|
-
footerStyle: buildFooterStyle("#
|
|
141
|
+
footerStyle: buildFooterStyle("#8c8c8c"),
|
|
122
142
|
fontFamily:
|
|
123
|
-
"'
|
|
124
|
-
bodyColor: "#
|
|
125
|
-
mutedColor: "#
|
|
126
|
-
primaryColor: "#
|
|
127
|
-
dividerColor: "#
|
|
128
|
-
blockquoteBorderColor: "#
|
|
129
|
-
bodyLineHeight: "
|
|
130
|
-
bodyLetterSpacing: "0.
|
|
143
|
+
"'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, 'SF Pro Text', 'Noto Sans SC', sans-serif",
|
|
144
|
+
bodyColor: "#2c2c2c",
|
|
145
|
+
mutedColor: "#8c8c8c",
|
|
146
|
+
primaryColor: "#ca6093",
|
|
147
|
+
dividerColor: "#d8d0d3",
|
|
148
|
+
blockquoteBorderColor: "#ca6093",
|
|
149
|
+
bodyLineHeight: "2.0",
|
|
150
|
+
bodyLetterSpacing: "0.015em",
|
|
131
151
|
},
|
|
132
152
|
},
|
|
133
153
|
};
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
function definedValues<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
156
|
+
const result: Record<string, unknown> = {};
|
|
157
|
+
for (const key of Object.keys(obj)) {
|
|
158
|
+
if (obj[key] !== undefined) {
|
|
159
|
+
result[key] = obj[key];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result as Partial<T>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getWechatPreviewTheme(
|
|
166
|
+
account: string,
|
|
167
|
+
overrides?: {
|
|
168
|
+
editorVars?: Record<string, string>;
|
|
169
|
+
exportTheme?: Partial<WechatExportTheme>;
|
|
170
|
+
},
|
|
171
|
+
): WechatPreviewTheme {
|
|
172
|
+
const base = THEMES[account] ?? THEMES.default;
|
|
173
|
+
if (!overrides) return base;
|
|
174
|
+
|
|
175
|
+
const hasEditorVars = overrides.editorVars && Object.keys(overrides.editorVars).length > 0;
|
|
176
|
+
const hasExportTheme = overrides.exportTheme && Object.keys(definedValues(overrides.exportTheme)).length > 0;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
editorVars: hasEditorVars ? { ...base.editorVars, ...overrides.editorVars } : base.editorVars,
|
|
181
|
+
exportTheme: hasExportTheme ? { ...base.exportTheme, ...definedValues(overrides.exportTheme!) } : base.exportTheme,
|
|
182
|
+
};
|
|
137
183
|
}
|
|
138
184
|
|
|
139
185
|
export function getWechatPreviewStyleName(account: string): string {
|
|
@@ -137,7 +137,6 @@ describe("normalizeHrStyles", () => {
|
|
|
137
137
|
const input = '<hr style="border: 1px solid red">';
|
|
138
138
|
const result = normalizeHrStyles(input);
|
|
139
139
|
expect(result).toContain("border: none");
|
|
140
|
-
expect(result).toContain("border-top: 1px solid #dadce0");
|
|
141
140
|
expect(result).toContain("height: 0");
|
|
142
141
|
});
|
|
143
142
|
});
|
|
@@ -276,7 +276,6 @@ export function normalizeHrStyles(html: string): string {
|
|
|
276
276
|
'background-color': 'transparent',
|
|
277
277
|
'background-image': 'none',
|
|
278
278
|
'border': 'none',
|
|
279
|
-
'border-top': '1px solid #dadce0',
|
|
280
279
|
'overflow-x': 'visible',
|
|
281
280
|
'font-size': '0',
|
|
282
281
|
'line-height': '0',
|
|
@@ -486,32 +485,40 @@ function normalizeTypography(
|
|
|
486
485
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(originalTagName)) {
|
|
487
486
|
clearTextFrame()
|
|
488
487
|
setStyle(styles, 'font-family', fontFamily)
|
|
489
|
-
setStyle(styles, 'font-weight', originalTagName === 'h3' ? '600' : '700')
|
|
490
|
-
setStyle(styles, 'color', originalTagName === 'h1' ? bodyColor : primaryColor)
|
|
491
488
|
|
|
492
489
|
if (originalTagName === 'h1') {
|
|
493
|
-
setStyle(styles, 'font-
|
|
494
|
-
setStyle(styles, '
|
|
495
|
-
setStyle(styles, '
|
|
496
|
-
setStyle(styles, '
|
|
490
|
+
setStyle(styles, 'font-weight', '700')
|
|
491
|
+
setStyle(styles, 'color', bodyColor)
|
|
492
|
+
setStyle(styles, 'font-size', '1.625rem')
|
|
493
|
+
setStyle(styles, 'line-height', '1.3')
|
|
494
|
+
setStyle(styles, 'letter-spacing', '-0.01em')
|
|
495
|
+
setStyle(styles, 'margin', '3em 0 1em')
|
|
497
496
|
}
|
|
498
497
|
else if (originalTagName === 'h2') {
|
|
499
|
-
setStyle(styles, 'font-
|
|
500
|
-
setStyle(styles, '
|
|
501
|
-
setStyle(styles, '
|
|
502
|
-
setStyle(styles, '
|
|
498
|
+
setStyle(styles, 'font-weight', '700')
|
|
499
|
+
setStyle(styles, 'color', primaryColor)
|
|
500
|
+
setStyle(styles, 'font-size', '1.25rem')
|
|
501
|
+
setStyle(styles, 'line-height', '1.35')
|
|
502
|
+
setStyle(styles, 'letter-spacing', '0.02em')
|
|
503
|
+
setStyle(styles, 'margin', '2.4em 0 0.9em')
|
|
503
504
|
}
|
|
504
505
|
else if (originalTagName === 'h3') {
|
|
505
|
-
setStyle(styles, 'font-
|
|
506
|
-
setStyle(styles, '
|
|
507
|
-
setStyle(styles, '
|
|
508
|
-
setStyle(styles, '
|
|
506
|
+
setStyle(styles, 'font-weight', '600')
|
|
507
|
+
setStyle(styles, 'color', bodyColor)
|
|
508
|
+
setStyle(styles, 'font-size', '1.125rem')
|
|
509
|
+
setStyle(styles, 'line-height', '1.45')
|
|
510
|
+
setStyle(styles, 'letter-spacing', '0.015em')
|
|
511
|
+
setStyle(styles, 'margin', '2em 0 0.65em')
|
|
512
|
+
setStyle(styles, 'padding-bottom', '0.5em')
|
|
513
|
+
setStyle(styles, 'border-bottom', `1px solid ${dividerColor}`)
|
|
509
514
|
}
|
|
510
515
|
else {
|
|
511
|
-
setStyle(styles, 'font-
|
|
512
|
-
setStyle(styles, '
|
|
516
|
+
setStyle(styles, 'font-weight', '600')
|
|
517
|
+
setStyle(styles, 'color', bodyColor)
|
|
518
|
+
setStyle(styles, 'font-size', '1rem')
|
|
519
|
+
setStyle(styles, 'line-height', '1.5')
|
|
513
520
|
setStyle(styles, 'letter-spacing', '0.02em')
|
|
514
|
-
setStyle(styles, 'margin', '1.
|
|
521
|
+
setStyle(styles, 'margin', '1.25em 0 0.5em')
|
|
515
522
|
}
|
|
516
523
|
return
|
|
517
524
|
}
|
|
@@ -557,6 +564,17 @@ function normalizeTypography(
|
|
|
557
564
|
return
|
|
558
565
|
}
|
|
559
566
|
|
|
567
|
+
if (outTagName === 'figcaption') {
|
|
568
|
+
clearTextFrame()
|
|
569
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
570
|
+
setStyle(styles, 'font-size', '12px')
|
|
571
|
+
setStyle(styles, 'line-height', '1.6')
|
|
572
|
+
setStyle(styles, 'color', mutedColor)
|
|
573
|
+
setStyle(styles, 'text-align', 'center')
|
|
574
|
+
setStyle(styles, 'margin', '8px 0 0 0')
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
|
|
560
578
|
if (outTagName === 'span' || outTagName === 'strong' || outTagName === 'em' || outTagName === 'b' || outTagName === 'i' || outTagName === 'u' || outTagName === 's' || outTagName === 'del') {
|
|
561
579
|
clearTextFrame()
|
|
562
580
|
setStyle(styles, 'font-family', fontFamily)
|
|
@@ -662,13 +680,20 @@ function getOneDomCssStyle(
|
|
|
662
680
|
const textParent = node.parentElement
|
|
663
681
|
const shouldPreserveWhitespace = isInCodeBlock || hasBlockCodeAncestor(textParent)
|
|
664
682
|
// 在代码块内,保留所有空格和换行
|
|
665
|
-
//
|
|
683
|
+
// 先转义 HTML 特殊字符,防止代码块内的 <img>,  等被下游正则误匹配
|
|
666
684
|
if (shouldPreserveWhitespace && text) {
|
|
667
685
|
return text
|
|
668
|
-
.replace(
|
|
669
|
-
.replace(
|
|
686
|
+
.replace(/&/g, '&')
|
|
687
|
+
.replace(/</g, '<')
|
|
688
|
+
.replace(/>/g, '>')
|
|
689
|
+
.replace(/ /g, '\u00A0')
|
|
690
|
+
.replace(/\t/g, '\u00A0\u00A0\u00A0\u00A0')
|
|
670
691
|
}
|
|
671
692
|
return text
|
|
693
|
+
.replace(/&/g, '&')
|
|
694
|
+
.replace(/</g, '<')
|
|
695
|
+
.replace(/>/g, '>')
|
|
696
|
+
|
|
672
697
|
}
|
|
673
698
|
|
|
674
699
|
// 注释节点
|
|
@@ -874,8 +899,8 @@ function getOneDomCssStyle(
|
|
|
874
899
|
const val = (el as HTMLInputElement).value
|
|
875
900
|
if (!val)
|
|
876
901
|
return ''
|
|
877
|
-
// 返回居中的 figcaption
|
|
878
|
-
return `<figcaption style="margin-top:
|
|
902
|
+
// 返回居中的 figcaption(小号文字,无装饰符号)
|
|
903
|
+
return `<figcaption style="margin-top: 8px; text-align: center; color: #8c8c8c; font-size: 12px; line-height: 1.6;">${val}</figcaption>`
|
|
879
904
|
}
|
|
880
905
|
return ''
|
|
881
906
|
}
|
package/src/workflow.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { prepare } from "./commands/prepare";
|
|
|
14
14
|
import { prepareFinalize } from "./commands/prepare-finalize";
|
|
15
15
|
import { render } from "./commands/render";
|
|
16
16
|
import { publish } from "./commands/publish";
|
|
17
|
+
import { republish } from "./commands/republish";
|
|
17
18
|
import { reconcile } from "./commands/reconcile";
|
|
18
19
|
import { review } from "./commands/review";
|
|
19
20
|
import { status } from "./commands/status";
|
|
@@ -22,8 +23,11 @@ import { tasks } from "./commands/tasks";
|
|
|
22
23
|
import { renderPostsRelativePath, resolveWorkspacePaths, resolveWorkspaceRoot } from "./config";
|
|
23
24
|
import { resolveFullRoute } from "./routes";
|
|
24
25
|
import { resolveAuthoring } from "./profiles";
|
|
26
|
+
import { WorkflowStateSchema } from "./schema/state";
|
|
25
27
|
import { defaultState, readState, validateForPhase, writeState } from "./state";
|
|
26
28
|
import { getTaskByStatePath } from "./task-manager";
|
|
29
|
+
import { dedupeTargets, filterIdempotent } from "./providers/publish-core";
|
|
30
|
+
import { getPublishProvider, listPublishProviders } from "./providers";
|
|
27
31
|
import { stripLeadingH1, stripLeadingTitleHeading } from "./text";
|
|
28
32
|
|
|
29
33
|
const TEST_CONFIG_PATH = join(
|
|
@@ -1211,6 +1215,7 @@ describe("config", () => {
|
|
|
1211
1215
|
appId: "",
|
|
1212
1216
|
appSecret: "",
|
|
1213
1217
|
customCss: null,
|
|
1218
|
+
theme: { editorVars: {}, exportTheme: {} },
|
|
1214
1219
|
},
|
|
1215
1220
|
},
|
|
1216
1221
|
},
|
|
@@ -1336,7 +1341,7 @@ describe("publish validation", () => {
|
|
|
1336
1341
|
expect(updated.phase.current).toBe("publish");
|
|
1337
1342
|
expect(updated.phase.publish.status).toBe("pending");
|
|
1338
1343
|
expect(updated.publish.results[0]?.status).toBe("failed");
|
|
1339
|
-
expect(updated.publish.results[0]?.detail).toContain("
|
|
1344
|
+
expect(updated.publish.results[0]?.detail).toContain("blog publish command failed");
|
|
1340
1345
|
|
|
1341
1346
|
const task = await getTaskByStatePath(statePath);
|
|
1342
1347
|
expect(task.blockers).toEqual([]);
|
|
@@ -2085,3 +2090,291 @@ describe("newspic_render default behavior", () => {
|
|
|
2085
2090
|
expect(finalState.intent.newspic_render?.pagination_mode ?? "single").toBe("single");
|
|
2086
2091
|
});
|
|
2087
2092
|
});
|
|
2093
|
+
|
|
2094
|
+
describe("publish_targets and multi-account", () => {
|
|
2095
|
+
test("PublishResult schema accepts account field with default", () => {
|
|
2096
|
+
const state = WorkflowStateSchema.parse({});
|
|
2097
|
+
// publish.results should accept items with account field
|
|
2098
|
+
const result = {
|
|
2099
|
+
route: "wechat-article" as const,
|
|
2100
|
+
account: "ancientone",
|
|
2101
|
+
status: "success" as const,
|
|
2102
|
+
detail: null,
|
|
2103
|
+
published_at: "2026-06-14T10:00:00Z",
|
|
2104
|
+
content_version: 1,
|
|
2105
|
+
render_version: 1,
|
|
2106
|
+
};
|
|
2107
|
+
state.publish.results.push(result);
|
|
2108
|
+
const parsed = WorkflowStateSchema.parse(state);
|
|
2109
|
+
expect(parsed.publish.results[0].account).toBe("ancientone");
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
test("PublishResult defaults account to 'default' when missing", () => {
|
|
2113
|
+
const result = {
|
|
2114
|
+
route: "wechat-article",
|
|
2115
|
+
status: "success",
|
|
2116
|
+
detail: null,
|
|
2117
|
+
published_at: null,
|
|
2118
|
+
content_version: 0,
|
|
2119
|
+
render_version: 0,
|
|
2120
|
+
};
|
|
2121
|
+
const state = WorkflowStateSchema.parse({
|
|
2122
|
+
publish: { results: [result] },
|
|
2123
|
+
});
|
|
2124
|
+
expect(state.publish.results[0].account).toBe("default");
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
test("publish_targets field defaults to empty array", () => {
|
|
2128
|
+
const state = WorkflowStateSchema.parse({});
|
|
2129
|
+
expect(state.publish_targets).toEqual([]);
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
test("publish_targets accepts array of {route, account}", () => {
|
|
2133
|
+
const state = WorkflowStateSchema.parse({
|
|
2134
|
+
publish_targets: [
|
|
2135
|
+
{ route: "wechat-article", account: "default" },
|
|
2136
|
+
{ route: "wechat-article", account: "ancientone" },
|
|
2137
|
+
{ route: "blog", account: "default" },
|
|
2138
|
+
],
|
|
2139
|
+
});
|
|
2140
|
+
expect(state.publish_targets).toHaveLength(3);
|
|
2141
|
+
expect(state.publish_targets[1].account).toBe("ancientone");
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
test("publish_targets account defaults to 'default' when omitted", () => {
|
|
2145
|
+
const state = WorkflowStateSchema.parse({
|
|
2146
|
+
publish_targets: [{ route: "blog" }],
|
|
2147
|
+
});
|
|
2148
|
+
expect(state.publish_targets[0].account).toBe("default");
|
|
2149
|
+
});
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
describe("provider accountOverride", () => {
|
|
2153
|
+
test("PublishRouteContext accepts accountOverride", () => {
|
|
2154
|
+
// Type-level test: this should compile
|
|
2155
|
+
const ctx: import("./providers").PublishRouteContext = {
|
|
2156
|
+
state: {} as any,
|
|
2157
|
+
dryRun: true,
|
|
2158
|
+
config: {} as any,
|
|
2159
|
+
workspacePaths: {} as any,
|
|
2160
|
+
accountOverride: "ancientone",
|
|
2161
|
+
};
|
|
2162
|
+
expect(ctx.accountOverride).toBe("ancientone");
|
|
2163
|
+
});
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
describe("PUBLISH_PROVIDERS", () => {
|
|
2167
|
+
test("blog is registered as publish provider", () => {
|
|
2168
|
+
const provider = getPublishProvider("blog");
|
|
2169
|
+
expect(provider).toBeDefined();
|
|
2170
|
+
expect(typeof provider).toBe("function");
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
test("listPublishProviders includes blog", () => {
|
|
2174
|
+
const providers = listPublishProviders();
|
|
2175
|
+
expect(providers).toContain("blog");
|
|
2176
|
+
});
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
describe("executePublishTargets helpers", () => {
|
|
2180
|
+
test("dedupeTargets deduplicates by route+account", () => {
|
|
2181
|
+
const targets = [
|
|
2182
|
+
{ route: "wechat-article" as const, account: "default" },
|
|
2183
|
+
{ route: "wechat-article" as const, account: "default" }, // duplicate
|
|
2184
|
+
{ route: "blog" as const, account: "default" },
|
|
2185
|
+
];
|
|
2186
|
+
const deduped = dedupeTargets(targets);
|
|
2187
|
+
expect(deduped).toHaveLength(2);
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
test("filterIdempotent filters out already-published targets", () => {
|
|
2191
|
+
const targets = [
|
|
2192
|
+
{ route: "wechat-article" as const, account: "default" },
|
|
2193
|
+
{ route: "blog" as const, account: "default" },
|
|
2194
|
+
];
|
|
2195
|
+
const existingResults = [
|
|
2196
|
+
{
|
|
2197
|
+
route: "wechat-article" as const,
|
|
2198
|
+
account: "default",
|
|
2199
|
+
status: "success" as const,
|
|
2200
|
+
detail: null,
|
|
2201
|
+
published_at: "2026-06-14T10:00:00Z",
|
|
2202
|
+
content_version: 1,
|
|
2203
|
+
render_version: 1,
|
|
2204
|
+
},
|
|
2205
|
+
];
|
|
2206
|
+
const filtered = filterIdempotent(targets, existingResults, 1, 1);
|
|
2207
|
+
expect(filtered).toHaveLength(1);
|
|
2208
|
+
expect(filtered[0].route).toBe("blog");
|
|
2209
|
+
});
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
describe("publish command with publish_targets", () => {
|
|
2213
|
+
test("publish iterates publish_targets when present", async () => {
|
|
2214
|
+
const tmpDir = await makeTempDir("zzhub-test-publish-targets-");
|
|
2215
|
+
const statePath = join(tmpDir, "state.json");
|
|
2216
|
+
const state = defaultState();
|
|
2217
|
+
state.run_id = "test-run";
|
|
2218
|
+
state.workspace_root = tmpDir;
|
|
2219
|
+
state.state_path = statePath;
|
|
2220
|
+
state.asset_path = tmpDir;
|
|
2221
|
+
state.route.primary = "wechat-article";
|
|
2222
|
+
state.route.account = "default";
|
|
2223
|
+
state.publish_targets = [
|
|
2224
|
+
{ route: "wechat-article", account: "default" },
|
|
2225
|
+
{ route: "wechat-newspic", account: "ancientone" },
|
|
2226
|
+
];
|
|
2227
|
+
state.metadata.title = "Test";
|
|
2228
|
+
state.metadata.slug = "test";
|
|
2229
|
+
state.metadata.date = "2026-06-14";
|
|
2230
|
+
state.content_review.status = "passed";
|
|
2231
|
+
// post.md needed by wechat-newspic provider (read before dry-run check)
|
|
2232
|
+
await writeFile(join(tmpDir, "post.md"), "正文", "utf-8");
|
|
2233
|
+
await writeState(statePath, state);
|
|
2234
|
+
|
|
2235
|
+
await publish(["--state", statePath, "--dry-run"]);
|
|
2236
|
+
|
|
2237
|
+
const finalState = await readState(statePath);
|
|
2238
|
+
expect(finalState.publish.results).toHaveLength(2);
|
|
2239
|
+
expect(finalState.publish.results[0].account).toBe("default");
|
|
2240
|
+
expect(finalState.publish.results[0].status).toBe("skipped");
|
|
2241
|
+
expect(finalState.publish.results[1].account).toBe("ancientone");
|
|
2242
|
+
expect(finalState.publish.results[1].status).toBe("skipped");
|
|
2243
|
+
});
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
describe("init with multi-target --targets", () => {
|
|
2247
|
+
test("single target leaves publish_targets empty", async () => {
|
|
2248
|
+
const tmpDir = await makeTempDir("zzhub-test-init-single-");
|
|
2249
|
+
const output = await captureJsonOutput<any>(() =>
|
|
2250
|
+
init([
|
|
2251
|
+
"--workspace", tmpDir,
|
|
2252
|
+
"--task-kind", "publish",
|
|
2253
|
+
"--content-form", "article",
|
|
2254
|
+
"--targets", "wechat-article",
|
|
2255
|
+
"--content-origin", "user",
|
|
2256
|
+
"--account", "default",
|
|
2257
|
+
]),
|
|
2258
|
+
);
|
|
2259
|
+
const state = await readState(output.state_path);
|
|
2260
|
+
expect(state.publish_targets).toEqual([]);
|
|
2261
|
+
expect(state.route.primary).toBe("wechat-article");
|
|
2262
|
+
expect(state.route.account).toBe("default");
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
test("multi-target populates publish_targets", async () => {
|
|
2266
|
+
const tmpDir = await makeTempDir("zzhub-test-init-multi-");
|
|
2267
|
+
const output = await captureJsonOutput<any>(() =>
|
|
2268
|
+
init([
|
|
2269
|
+
"--workspace", tmpDir,
|
|
2270
|
+
"--task-kind", "publish",
|
|
2271
|
+
"--content-form", "article",
|
|
2272
|
+
"--targets", "wechat-article@default,wechat-article@ancientone,blog@default",
|
|
2273
|
+
"--content-origin", "user",
|
|
2274
|
+
]),
|
|
2275
|
+
);
|
|
2276
|
+
const state = await readState(output.state_path);
|
|
2277
|
+
expect(state.publish_targets).toHaveLength(3);
|
|
2278
|
+
expect(state.publish_targets[0]).toEqual({ route: "wechat-article", account: "default" });
|
|
2279
|
+
expect(state.publish_targets[1]).toEqual({ route: "wechat-article", account: "ancientone" });
|
|
2280
|
+
expect(state.publish_targets[2]).toEqual({ route: "blog", account: "default" });
|
|
2281
|
+
expect(state.route.primary).toBe("wechat-article");
|
|
2282
|
+
expect(state.route.account).toBe("default");
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
test("--targets without @ uses --account value", async () => {
|
|
2286
|
+
const tmpDir = await makeTempDir("zzhub-test-init-no-at-");
|
|
2287
|
+
const output = await captureJsonOutput<any>(() =>
|
|
2288
|
+
init([
|
|
2289
|
+
"--workspace", tmpDir,
|
|
2290
|
+
"--task-kind", "publish",
|
|
2291
|
+
"--content-form", "article",
|
|
2292
|
+
"--targets", "wechat-article,blog",
|
|
2293
|
+
"--content-origin", "user",
|
|
2294
|
+
"--account", "ancientone",
|
|
2295
|
+
]),
|
|
2296
|
+
);
|
|
2297
|
+
const state = await readState(output.state_path);
|
|
2298
|
+
expect(state.publish_targets).toHaveLength(2);
|
|
2299
|
+
expect(state.publish_targets[0].account).toBe("ancientone");
|
|
2300
|
+
expect(state.publish_targets[1].account).toBe("ancientone");
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
describe("republish command", () => {
|
|
2305
|
+
test("republish adds targets and executes", async () => {
|
|
2306
|
+
const tmpDir = await makeTempDir("zzhub-test-republish-");
|
|
2307
|
+
const statePath = join(tmpDir, "state.json");
|
|
2308
|
+
const state = defaultState();
|
|
2309
|
+
state.run_id = "test-run";
|
|
2310
|
+
state.workspace_root = tmpDir;
|
|
2311
|
+
state.asset_path = tmpDir;
|
|
2312
|
+
state.route.primary = "wechat-article";
|
|
2313
|
+
state.route.account = "default";
|
|
2314
|
+
state.metadata.title = "Test";
|
|
2315
|
+
state.metadata.slug = "test";
|
|
2316
|
+
state.metadata.date = "2026-06-14";
|
|
2317
|
+
state.content_review.status = "passed";
|
|
2318
|
+
state.mode = "done";
|
|
2319
|
+
state.publish.results = [
|
|
2320
|
+
{
|
|
2321
|
+
route: "wechat-article",
|
|
2322
|
+
account: "default",
|
|
2323
|
+
status: "success",
|
|
2324
|
+
detail: null,
|
|
2325
|
+
published_at: "2026-06-14T10:00:00Z",
|
|
2326
|
+
content_version: 1,
|
|
2327
|
+
render_version: 1,
|
|
2328
|
+
},
|
|
2329
|
+
];
|
|
2330
|
+
await writeState(statePath, state);
|
|
2331
|
+
|
|
2332
|
+
// Republish to another account with dry-run
|
|
2333
|
+
await republish(["--state", statePath, "--account", "ancientone", "--dry-run"]);
|
|
2334
|
+
|
|
2335
|
+
const finalState = await readState(statePath);
|
|
2336
|
+
expect(finalState.publish_targets).toContainEqual({
|
|
2337
|
+
route: "wechat-article",
|
|
2338
|
+
account: "ancientone",
|
|
2339
|
+
});
|
|
2340
|
+
expect(finalState.publish.results).toHaveLength(2);
|
|
2341
|
+
expect(finalState.mode).toBe("done");
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
test("republish skips idempotent targets", async () => {
|
|
2345
|
+
const tmpDir = await makeTempDir("zzhub-test-republish-idem-");
|
|
2346
|
+
const statePath = join(tmpDir, "state.json");
|
|
2347
|
+
const state = defaultState();
|
|
2348
|
+
state.run_id = "test-run";
|
|
2349
|
+
state.workspace_root = tmpDir;
|
|
2350
|
+
state.asset_path = tmpDir;
|
|
2351
|
+
state.route.primary = "wechat-article";
|
|
2352
|
+
state.route.account = "default";
|
|
2353
|
+
state.metadata.title = "Test";
|
|
2354
|
+
state.metadata.slug = "test";
|
|
2355
|
+
state.metadata.date = "2026-06-14";
|
|
2356
|
+
state.content_review.status = "passed";
|
|
2357
|
+
state.mode = "done";
|
|
2358
|
+
state.artifacts.content_version = 1;
|
|
2359
|
+
state.artifacts.render_version = 1;
|
|
2360
|
+
state.publish.results = [
|
|
2361
|
+
{
|
|
2362
|
+
route: "wechat-article",
|
|
2363
|
+
account: "default",
|
|
2364
|
+
status: "success",
|
|
2365
|
+
detail: null,
|
|
2366
|
+
published_at: "2026-06-14T10:00:00Z",
|
|
2367
|
+
content_version: 1,
|
|
2368
|
+
render_version: 1,
|
|
2369
|
+
},
|
|
2370
|
+
];
|
|
2371
|
+
await writeState(statePath, state);
|
|
2372
|
+
|
|
2373
|
+
// Try to republish same target - should skip
|
|
2374
|
+
await republish(["--state", statePath, "--account", "default", "--dry-run"]);
|
|
2375
|
+
|
|
2376
|
+
const finalState = await readState(statePath);
|
|
2377
|
+
// Should still have only 1 result (skipped duplicate)
|
|
2378
|
+
expect(finalState.publish.results).toHaveLength(1);
|
|
2379
|
+
});
|
|
2380
|
+
});
|