@zzclub/pipeline 0.13.2 → 0.16.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/ARTICLE-THEMES.md +86 -0
- package/LICENSE +21 -0
- package/README.md +182 -788
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/package.json +6 -2
- package/src/adapter-types.ts +3 -0
- package/src/adapters/builtin-markdown-renderer.ts +1 -0
- package/src/article-theme.test.ts +87 -0
- package/src/article-theme.ts +115 -0
- package/src/commands/abandon.test.ts +1 -1
- package/src/commands/article-theme.ts +16 -0
- package/src/commands/checkpoint.test.ts +2 -2
- package/src/commands/ingest-handoff.ts +6 -0
- package/src/commands/init.ts +10 -0
- package/src/commands/prepare.ts +5 -1
- package/src/commands/publish.ts +4 -0
- package/src/commands/reset.ts +2 -0
- package/src/commands/wechat-export.ts +4 -0
- package/src/commands/wechat-preview.ts +35 -0
- package/src/plugins.ts +2 -0
- package/src/providers/index.ts +17 -2
- package/src/providers/publish-core.ts +32 -1
- package/src/providers/wechat.test.ts +12 -0
- package/src/providers/wechat.ts +60 -39
- package/src/render-branding.ts +1 -1
- package/src/schema/config.ts +12 -0
- package/src/schema/state.ts +5 -0
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +21 -1
- package/src/wechat-preview/assets/browser-dist/assets/style-C9-YtTIm.css +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/wechat-renderer-D1jHoaM2.js +624 -0
- package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -512
- package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +293 -0
- package/src/wechat-preview/assets/browser-dist/studio.js +332 -0
- package/src/wechat-preview/browser/editor-export.ts +17 -2
- package/src/wechat-preview/browser/highlight.ts +2 -0
- package/src/wechat-preview/browser/studio/color-palettes.test.ts +31 -0
- package/src/wechat-preview/browser/studio/color-palettes.ts +115 -0
- package/src/wechat-preview/browser/studio/studio.css +638 -0
- package/src/wechat-preview/browser/studio/studio.html +292 -0
- package/src/wechat-preview/browser/studio/studio.ts +721 -0
- package/src/wechat-preview/browser/studio/syntax-presets.test.ts +74 -0
- package/src/wechat-preview/browser/studio/syntax-presets.ts +590 -0
- package/src/wechat-preview/fixtures/all-nodes.md +38 -0
- package/src/wechat-preview/index.ts +36 -5
- package/src/wechat-preview/plugins/builtin-badge.ts +152 -0
- package/src/wechat-preview/plugins/builtin-callout.ts +261 -0
- package/src/wechat-preview/plugins/builtin-highlight.ts +46 -0
- package/src/wechat-preview/plugins/builtin-kbd.ts +133 -0
- package/src/wechat-preview/plugins/index.test.ts +313 -0
- package/src/wechat-preview/plugins/index.ts +6 -0
- package/src/wechat-preview/plugins/registry.ts +62 -0
- package/src/wechat-preview/plugins/types.ts +31 -0
- package/src/wechat-preview/server/http.ts +160 -3
- package/src/wechat-preview/server/paths.ts +2 -2
- package/src/wechat-preview/server/singleton.ts +14 -12
- package/src/wechat-preview/server/studio.test.ts +92 -0
- package/src/wechat-preview/server/ui.ts +1 -0
- package/src/wechat-preview/wechat-export.integration.test.ts +9 -1
- package/src/wechat-preview/wechat-renderer.ts +72 -7
- package/src/workflow.test.ts +1 -0
|
@@ -8,6 +8,9 @@ import type { PipelineConfig, ResolvedWorkspacePaths } from "../config";
|
|
|
8
8
|
import type { PublishResult, PublishTarget, WorkflowState } from "../state";
|
|
9
9
|
import { getPublishProvider, type PublishRouteContext } from "./index";
|
|
10
10
|
import { reportProgress } from "../monitor/recorder";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { resolveConfigRelativePath } from "../config";
|
|
13
|
+
import { readArticleTheme, snapshotArticleTheme, type ArticleThemePackage } from "../article-theme";
|
|
11
14
|
|
|
12
15
|
export interface PublishTargetError {
|
|
13
16
|
route: string;
|
|
@@ -54,6 +57,7 @@ export function filterIdempotent(
|
|
|
54
57
|
existingResults: PublishResult[],
|
|
55
58
|
contentVersion: number,
|
|
56
59
|
renderVersion: number,
|
|
60
|
+
themeHashes?: Map<string, string | null>,
|
|
57
61
|
): PublishTarget[] {
|
|
58
62
|
return targets.filter((target) => {
|
|
59
63
|
const existing = existingResults.find(
|
|
@@ -64,7 +68,8 @@ export function filterIdempotent(
|
|
|
64
68
|
r.content_version === contentVersion &&
|
|
65
69
|
r.render_version === renderVersion,
|
|
66
70
|
);
|
|
67
|
-
|
|
71
|
+
// 模板变化必须重新导出;旧结果无模板时继续兼容。
|
|
72
|
+
return !existing || (themeHashes && (existing.article_theme_hash ?? null) !== (themeHashes.get(`${target.route}@${target.account}`) ?? null));
|
|
68
73
|
});
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -92,11 +97,32 @@ export async function executePublishTargets(
|
|
|
92
97
|
const { state, targets, dryRun, config, workspacePaths, onResult } = params;
|
|
93
98
|
|
|
94
99
|
const deduped = dedupeTargets(targets);
|
|
100
|
+
const themes = new Map<string, ArticleThemePackage>();
|
|
101
|
+
const themeHashes = new Map<string, string | null>();
|
|
102
|
+
const themeErrors = new Map<string, unknown>();
|
|
103
|
+
for (const target of deduped) {
|
|
104
|
+
if (target.route !== "wechat-article") continue;
|
|
105
|
+
const account = config.wx.accounts[target.account] ?? config.wx.accounts[config.wx.defaultAccount];
|
|
106
|
+
const selected = state.intent.article_theme || resolveConfigRelativePath(account?.articleTheme);
|
|
107
|
+
if (!selected) continue;
|
|
108
|
+
const key = `${target.route}@${target.account}`;
|
|
109
|
+
try {
|
|
110
|
+
const source = await readArticleTheme(selected);
|
|
111
|
+
const theme = dryRun ? source : await snapshotArticleTheme(source, join(state.workspace_root, ".zzhub-media", "article-themes"));
|
|
112
|
+
themes.set(key, theme);
|
|
113
|
+
themeHashes.set(key, theme.hash);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// 一个账号的模板损坏不阻断其他发布目标,结果仍按原契约逐个记录。
|
|
116
|
+
themeHashes.set(key, "invalid");
|
|
117
|
+
themeErrors.set(key, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
95
120
|
const filtered = filterIdempotent(
|
|
96
121
|
deduped,
|
|
97
122
|
state.publish.results,
|
|
98
123
|
state.artifacts.content_version,
|
|
99
124
|
state.artifacts.render_version,
|
|
125
|
+
themeHashes,
|
|
100
126
|
);
|
|
101
127
|
|
|
102
128
|
const results: PublishResult[] = [];
|
|
@@ -106,6 +132,9 @@ export async function executePublishTargets(
|
|
|
106
132
|
reportProgress({ stage: "publish.targets", message: "正在发布", current: results.length, total: filtered.length, unit: "targets", route: target.route, account: target.account });
|
|
107
133
|
let result: PublishResult;
|
|
108
134
|
try {
|
|
135
|
+
const previous = state.publish.results.find(item => item.route === target.route && item.account === target.account);
|
|
136
|
+
if (previous?.detail?.includes("WX_RESULT_UNKNOWN")) throw new Error("WX_RESULT_UNKNOWN: 上次发送结果未知,请检查草稿箱后再通过 reset --mode publish 明确重试");
|
|
137
|
+
if (themeErrors.has(`${target.route}@${target.account}`)) throw themeErrors.get(`${target.route}@${target.account}`);
|
|
109
138
|
const provider = getPublishProvider(target.route);
|
|
110
139
|
const ctx: PublishRouteContext = {
|
|
111
140
|
state,
|
|
@@ -113,6 +142,7 @@ export async function executePublishTargets(
|
|
|
113
142
|
config,
|
|
114
143
|
workspacePaths,
|
|
115
144
|
accountOverride: target.account,
|
|
145
|
+
articleTheme: themes.get(`${target.route}@${target.account}`),
|
|
116
146
|
};
|
|
117
147
|
const providerResult = await provider(ctx);
|
|
118
148
|
result = {
|
|
@@ -121,6 +151,7 @@ export async function executePublishTargets(
|
|
|
121
151
|
account: target.account,
|
|
122
152
|
content_version: state.artifacts.content_version,
|
|
123
153
|
render_version: state.artifacts.render_version,
|
|
154
|
+
article_theme_hash: themeHashes.get(`${target.route}@${target.account}`) ?? null,
|
|
124
155
|
};
|
|
125
156
|
} catch (err) {
|
|
126
157
|
const error: PublishTargetError = {
|
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
resolveFilenameFromUrl,
|
|
11
11
|
resolveDraftMediaId,
|
|
12
12
|
readResponseBodyWithLimit,
|
|
13
|
+
createWechatNewspic,
|
|
13
14
|
} from "./wechat";
|
|
14
15
|
|
|
16
|
+
test("贴图配文按 UTF-8 长度校验,上传前拒绝超限内容", async () => {
|
|
17
|
+
await expect(createWechatNewspic({ content: "😀".repeat(512) + "a" } as any)).rejects.toThrow("贴图配文过长");
|
|
18
|
+
});
|
|
19
|
+
|
|
15
20
|
describe("resolveDraftMediaId", () => {
|
|
16
21
|
test("extracts media_id from a successful draft response", () => {
|
|
17
22
|
expect(resolveDraftMediaId({ code: 0, data: { media_id: "media-1" } }))
|
|
@@ -193,6 +198,13 @@ describe("extractImageUrls", () => {
|
|
|
193
198
|
});
|
|
194
199
|
|
|
195
200
|
describe("replaceImageUrls", () => {
|
|
201
|
+
test("replaces large Base64 sources and preserves literal replacement URLs", () => {
|
|
202
|
+
const src = `data:image/png;base64,${"A".repeat(3_400_000)}`;
|
|
203
|
+
const url = "https://example.com/image.png?value=$&";
|
|
204
|
+
expect(replaceImageUrls(`<img src="${src}" data-src="${src}">`, { [src]: url }))
|
|
205
|
+
.toBe(`<img src="${url}" data-src="${url}">`);
|
|
206
|
+
});
|
|
207
|
+
|
|
196
208
|
test("replaces URLs using the mapping", () => {
|
|
197
209
|
const html = '<img src="https://old.com/img.png">';
|
|
198
210
|
const result = replaceImageUrls(html, { "https://old.com/img.png": "https://new.com/img.png" });
|
package/src/providers/wechat.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function resolveDraftMediaId(
|
|
|
94
94
|
? record.media_id
|
|
95
95
|
: null;
|
|
96
96
|
if (!mediaId) {
|
|
97
|
-
throw new Error("WeChat draft API response did not include media_id");
|
|
97
|
+
throw new Error("WX_RESULT_UNKNOWN: WeChat draft API response did not include media_id");
|
|
98
98
|
}
|
|
99
99
|
return mediaId;
|
|
100
100
|
}
|
|
@@ -130,6 +130,8 @@ interface BaseWechatPublishInput {
|
|
|
130
130
|
timeout?: number;
|
|
131
131
|
config: PipelineConfig;
|
|
132
132
|
noteId?: string | null;
|
|
133
|
+
/** 同一运行的同一发布版本复用幂等标识。 */
|
|
134
|
+
idempotencyKey?: string;
|
|
133
135
|
nezusBaseUrl?: string | null;
|
|
134
136
|
nezusPat?: string | null;
|
|
135
137
|
}
|
|
@@ -140,6 +142,8 @@ export interface WechatDraftInput extends BaseWechatPublishInput {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
export interface WechatNewspicInput extends BaseWechatPublishInput {
|
|
145
|
+
/** 明确选择配图时,不从正文中的示例语法补充图片。 */
|
|
146
|
+
explicitPhotos?: boolean;
|
|
143
147
|
content: string;
|
|
144
148
|
existingDraftMediaId?: string | null;
|
|
145
149
|
}
|
|
@@ -170,6 +174,7 @@ export function fillWxAccountConfig(source?: WxAccountConfig): WxAccountConfig {
|
|
|
170
174
|
appId: source?.appId ?? "",
|
|
171
175
|
appSecret: source?.appSecret ?? "",
|
|
172
176
|
customCss: source?.customCss ?? null,
|
|
177
|
+
...(source?.articleTheme ? { articleTheme: source.articleTheme } : {}),
|
|
173
178
|
theme: source?.theme ?? { editorVars: {}, exportTheme: {} },
|
|
174
179
|
} as WxAccountConfig;
|
|
175
180
|
}
|
|
@@ -186,7 +191,9 @@ function getWxRuntimeConfig(
|
|
|
186
191
|
accountOverride || config.wx.defaultAccount,
|
|
187
192
|
);
|
|
188
193
|
const configuredAccount = config.wx.accounts[accountName];
|
|
189
|
-
|
|
194
|
+
// 云端 worker 以每个任务的环境变量注入凭据,不要求把用户账号写进基础 config。
|
|
195
|
+
const hasInjectedCredentials = Boolean(process.env.WX_APPID && process.env.WX_APPSECRET && process.env.ZZCLUB_PAT);
|
|
196
|
+
if (!configuredAccount && !hasInjectedCredentials) {
|
|
190
197
|
throw new Error(`Unknown wx account: ${accountName}`);
|
|
191
198
|
}
|
|
192
199
|
const accountConfig = fillWxAccountConfig(configuredAccount);
|
|
@@ -276,8 +283,8 @@ export function replaceImageUrls(html: string, imageUrlMap: Record<string, strin
|
|
|
276
283
|
if (!orig || !wx) {
|
|
277
284
|
continue;
|
|
278
285
|
}
|
|
279
|
-
|
|
280
|
-
replaced = replaced.
|
|
286
|
+
// Base64 图片地址可能数 MB,按字面替换,避免超大正则及替换串转义。
|
|
287
|
+
replaced = replaced.replaceAll(orig, () => wx);
|
|
281
288
|
}
|
|
282
289
|
return replaced;
|
|
283
290
|
}
|
|
@@ -320,28 +327,51 @@ export async function readResponseBodyWithLimit(
|
|
|
320
327
|
return body.buffer;
|
|
321
328
|
}
|
|
322
329
|
|
|
330
|
+
/** 服务端错误只保留分类和脱敏提示,正文及凭据不进入 Pipeline 状态。 */
|
|
331
|
+
function serviceError(response: Response, payload: unknown, mutating: boolean): Error {
|
|
332
|
+
const detail = asRecord(payload);
|
|
333
|
+
const code = String(asRecord(detail.data).code || "WX_SERVICE_ERROR").replace(/[^A-Z0-9_]/g, "").slice(0, 80);
|
|
334
|
+
if (mutating && response.status >= 500 && !code.startsWith("WX_")) return unknownResult();
|
|
335
|
+
if (mutating && response.status >= 500 && code === "WX_SERVICE_ERROR") return unknownResult();
|
|
336
|
+
const message = String(detail.statusMessage || detail.message || "发布服务请求失败")
|
|
337
|
+
.replace(/Bearer\s+[^\s"',}]+/gi, "Bearer [redacted]")
|
|
338
|
+
.replace(/(["']?(?:access_token|token|pat|appSecret|secret)["']?\s*[:=]\s*)["']?[^\s"'&,}]+["']?/gi, "$1[redacted]");
|
|
339
|
+
const requestId = response.headers.get("x-request-id") || String(detail.requestId || "");
|
|
340
|
+
const trace = /^[a-zA-Z0-9_-]{1,100}$/.test(requestId) ? ` [request ${requestId}]` : "";
|
|
341
|
+
return new Error(`API request failed with status ${response.status}: ${code}: ${message.slice(0, 300)}${trace}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function unknownResult(): Error {
|
|
345
|
+
return new Error("WX_RESULT_UNKNOWN: 请求结果未确认,请检查公众号草稿箱后再决定是否重试");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 网络、响应读取及 JSON 解析中断都可能发生在上游已经写入之后。 */
|
|
349
|
+
async function requestWechat(url: string, options: RequestInit, timeout: number): Promise<unknown> {
|
|
350
|
+
const mutating = /\/draft\/(?:add|update|delete)$/.test(url);
|
|
351
|
+
let response: Response;
|
|
352
|
+
let payload: unknown;
|
|
353
|
+
try {
|
|
354
|
+
response = await fetch(url, { ...options, signal: AbortSignal.timeout(timeout) });
|
|
355
|
+
const responseText = await response.text();
|
|
356
|
+
try { payload = responseText ? JSON.parse(responseText) : {}; }
|
|
357
|
+
catch {
|
|
358
|
+
if (response.ok || mutating) throw new Error("invalid response");
|
|
359
|
+
payload = {};
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (mutating) throw unknownResult();
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
if (!response.ok) throw serviceError(response, payload, mutating);
|
|
366
|
+
return payload;
|
|
367
|
+
}
|
|
368
|
+
|
|
323
369
|
async function requestJson(
|
|
324
370
|
url: string,
|
|
325
371
|
options: { method: string; headers?: Record<string, string>; body?: unknown },
|
|
326
372
|
timeout: number,
|
|
327
373
|
): Promise<unknown> {
|
|
328
|
-
|
|
329
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
330
|
-
try {
|
|
331
|
-
const response = await fetch(url, {
|
|
332
|
-
method: options.method,
|
|
333
|
-
headers: options.headers,
|
|
334
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
335
|
-
signal: controller.signal,
|
|
336
|
-
});
|
|
337
|
-
const responseText = await response.text();
|
|
338
|
-
if (!response.ok) {
|
|
339
|
-
throw new Error(`API request failed with status ${response.status}: ${responseText}`);
|
|
340
|
-
}
|
|
341
|
-
return responseText ? JSON.parse(responseText) : {};
|
|
342
|
-
} finally {
|
|
343
|
-
clearTimeout(timeoutId);
|
|
344
|
-
}
|
|
374
|
+
return requestWechat(url, { ...options, body: options.body ? JSON.stringify(options.body) : undefined }, timeout);
|
|
345
375
|
}
|
|
346
376
|
|
|
347
377
|
async function requestFormData(
|
|
@@ -349,23 +379,7 @@ async function requestFormData(
|
|
|
349
379
|
options: { method: string; headers?: Record<string, string>; body: FormData },
|
|
350
380
|
timeout: number,
|
|
351
381
|
): Promise<unknown> {
|
|
352
|
-
|
|
353
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
354
|
-
try {
|
|
355
|
-
const response = await fetch(url, {
|
|
356
|
-
method: options.method,
|
|
357
|
-
headers: options.headers,
|
|
358
|
-
body: options.body,
|
|
359
|
-
signal: controller.signal,
|
|
360
|
-
});
|
|
361
|
-
const responseText = await response.text();
|
|
362
|
-
if (!response.ok) {
|
|
363
|
-
throw new Error(`API request failed with status ${response.status}: ${responseText}`);
|
|
364
|
-
}
|
|
365
|
-
return responseText ? JSON.parse(responseText) : {};
|
|
366
|
-
} finally {
|
|
367
|
-
clearTimeout(timeoutId);
|
|
368
|
-
}
|
|
382
|
+
return requestWechat(url, options, timeout);
|
|
369
383
|
}
|
|
370
384
|
|
|
371
385
|
async function sleep(ms: number): Promise<void> {
|
|
@@ -639,6 +653,7 @@ export async function createWechatDraft(input: WechatDraftInput): Promise<Record
|
|
|
639
653
|
headers: {
|
|
640
654
|
"Content-Type": "application/json",
|
|
641
655
|
Authorization: `Bearer ${runtime.pat}`,
|
|
656
|
+
...(input.idempotencyKey ? { "Idempotency-Key": input.idempotencyKey } : {}),
|
|
642
657
|
},
|
|
643
658
|
body: draftBody,
|
|
644
659
|
},
|
|
@@ -672,13 +687,18 @@ export async function createWechatDraft(input: WechatDraftInput): Promise<Record
|
|
|
672
687
|
}
|
|
673
688
|
|
|
674
689
|
export async function createWechatNewspic(input: WechatNewspicInput): Promise<Record<string, unknown>> {
|
|
690
|
+
// 配文超限时在上传图片前拒绝,避免微信返回不明确的 45166。
|
|
691
|
+
if (Buffer.byteLength(input.content, "utf8") > 2048)
|
|
692
|
+
throw new Error("贴图配文过长,请精简至 2KB 以内;完整文章可保留在内容图中");
|
|
675
693
|
const runtime = getWxRuntimeConfig(input.config, input.account);
|
|
676
|
-
const fallbackPhotos = extractImageUrls(input.content);
|
|
694
|
+
const fallbackPhotos = input.explicitPhotos ? [] : extractImageUrls(input.content);
|
|
677
695
|
const photos = parsePhotos(input.photos);
|
|
678
696
|
const finalPhotos = mergePhotoLists(photos, fallbackPhotos);
|
|
679
697
|
if (finalPhotos.length === 0) {
|
|
680
698
|
throw new Error("No photos available for wx newspic upload");
|
|
681
699
|
}
|
|
700
|
+
if (finalPhotos.length > 20)
|
|
701
|
+
throw new Error("微信贴图最多支持 20 张配图");
|
|
682
702
|
|
|
683
703
|
const tokenTimeout = resolveTimeout(TOKEN_TIMEOUT, runtime.timeout, input.timeout);
|
|
684
704
|
const uploadTimeout = resolveTimeout(UPLOAD_TIMEOUT, runtime.timeout, input.timeout);
|
|
@@ -726,6 +746,7 @@ export async function createWechatNewspic(input: WechatNewspicInput): Promise<Re
|
|
|
726
746
|
headers: {
|
|
727
747
|
"Content-Type": "application/json",
|
|
728
748
|
Authorization: `Bearer ${runtime.pat}`,
|
|
749
|
+
...(input.idempotencyKey ? { "Idempotency-Key": input.idempotencyKey } : {}),
|
|
729
750
|
},
|
|
730
751
|
body: draftBody,
|
|
731
752
|
},
|
package/src/render-branding.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function resolveRenderBranding(config: PipelineConfig, account: string):
|
|
|
16
16
|
const branding = config.render.branding;
|
|
17
17
|
const selected = branding.accounts[account];
|
|
18
18
|
const visual = getVisualParams(account);
|
|
19
|
-
const logo = selected?.logo ?? branding.logo ?? config.imgx.icon;
|
|
19
|
+
const logo = process.env.ZZHUB_PIPELINE_BRANDING_LOGO?.trim() || (selected?.logo ?? branding.logo ?? config.imgx.icon);
|
|
20
20
|
return {
|
|
21
21
|
logo: logo === null ? resolveInputPath(visual?.fallback_icon ?? join(ICONS_DIR, "logo.png")) : resolveBrandLogo(logo),
|
|
22
22
|
footerText: selected?.footerText ?? branding.footerText ?? visual?.footer ?? config.wx.accounts[account]?.name ?? "",
|
package/src/schema/config.ts
CHANGED
|
@@ -53,6 +53,16 @@ const WechatThemeOverridesSchema = withObjectDefault(
|
|
|
53
53
|
z.object({
|
|
54
54
|
editorVars: z.record(z.string(), z.string()).default({}),
|
|
55
55
|
exportTheme: withObjectDefault(WechatExportThemeOverridesSchema),
|
|
56
|
+
syntaxPresets: z.record(z.string(), z.string()).optional(),
|
|
57
|
+
structure: z.object({
|
|
58
|
+
numberedHeadings: z.boolean().optional(),
|
|
59
|
+
headingLabel: z.string().optional(),
|
|
60
|
+
quoteLabel: z.string().optional(),
|
|
61
|
+
calloutPresets: z.record(z.string(), z.object({
|
|
62
|
+
title: z.string().optional(),
|
|
63
|
+
icon: z.string().optional(),
|
|
64
|
+
})).optional(),
|
|
65
|
+
}).optional(),
|
|
56
66
|
}),
|
|
57
67
|
);
|
|
58
68
|
|
|
@@ -67,6 +77,8 @@ const WxAccountConfigSchema = z.object({
|
|
|
67
77
|
appId: z.string().default(""),
|
|
68
78
|
appSecret: z.string().default(""),
|
|
69
79
|
customCss: z.string().nullable().default(null),
|
|
80
|
+
/** 账号默认的本地正文模板,未配置时保持原样式。 */
|
|
81
|
+
articleTheme: z.string().nullable().optional(),
|
|
70
82
|
theme: WechatThemeOverridesSchema,
|
|
71
83
|
});
|
|
72
84
|
|
package/src/schema/state.ts
CHANGED
|
@@ -260,6 +260,7 @@ const ImagesSchema = withObjectDefault(
|
|
|
260
260
|
);
|
|
261
261
|
|
|
262
262
|
const PublishResultSchema = z.object({
|
|
263
|
+
article_theme_hash: z.string().nullable().optional(),
|
|
263
264
|
route: RoutePrimarySchema,
|
|
264
265
|
account: trimmedString("default"),
|
|
265
266
|
status: PublishResultStatusSchema,
|
|
@@ -273,6 +274,7 @@ const PublishResultSchema = z.object({
|
|
|
273
274
|
const PublishResultsSchema = withObjectDefault(
|
|
274
275
|
z.object({
|
|
275
276
|
results: z.array(PublishResultSchema).default([]),
|
|
277
|
+
attempt: z.number().int().nonnegative().optional(),
|
|
276
278
|
}),
|
|
277
279
|
);
|
|
278
280
|
|
|
@@ -310,7 +312,10 @@ const IntentSchema = withObjectDefault(
|
|
|
310
312
|
.transform((arr) => arr.filter((s) => s.trim().length > 0)),
|
|
311
313
|
style_hint: z.string().nullable().default(null),
|
|
312
314
|
cover_theme: z.string().trim().min(1).nullable().default(null),
|
|
315
|
+
article_theme: z.string().nullable().optional(),
|
|
313
316
|
newspic_render: NewspicRenderSpecSchema.nullable().default(null),
|
|
317
|
+
// 用户确认的贴图副本文件,正文不进入状态文件。
|
|
318
|
+
newspic_file: z.string().optional(),
|
|
314
319
|
requires: IntentRequiresSchema,
|
|
315
320
|
existing_draft_media_id: z.string().nullable().default(null),
|
|
316
321
|
note_id: z.string().nullable().default(null),
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
+
"_wechat-renderer-D1jHoaM2.js": {
|
|
3
|
+
"file": "assets/wechat-renderer-D1jHoaM2.js",
|
|
4
|
+
"name": "wechat-renderer"
|
|
5
|
+
},
|
|
2
6
|
"src/wechat-preview/browser/editor-export.ts": {
|
|
3
7
|
"file": "editor-export.js",
|
|
4
8
|
"name": "editor-export",
|
|
5
9
|
"src": "src/wechat-preview/browser/editor-export.ts",
|
|
6
|
-
"isEntry": true
|
|
10
|
+
"isEntry": true,
|
|
11
|
+
"imports": [
|
|
12
|
+
"_wechat-renderer-D1jHoaM2.js"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"src/wechat-preview/browser/studio/studio.html": {
|
|
16
|
+
"file": "studio.js",
|
|
17
|
+
"name": "studio",
|
|
18
|
+
"src": "src/wechat-preview/browser/studio/studio.html",
|
|
19
|
+
"isEntry": true,
|
|
20
|
+
"imports": [
|
|
21
|
+
"_wechat-renderer-D1jHoaM2.js"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"style.css": {
|
|
25
|
+
"file": "assets/style-C9-YtTIm.css",
|
|
26
|
+
"src": "style.css"
|
|
7
27
|
}
|
|
8
28
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-main: #f8fafc;--bg-panel: #ffffff;--border: #e2e8f0;--border-focus: #ca6093;--text-main: #1e293b;--text-muted: #64748b;--brand: #ca6093;--brand-hover: #b35082;--brand-light: rgba(202, 96, 147, .08);--success: #10b981;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;--font-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font-sans);color:var(--text-main);background:var(--bg-main);height:100vh;display:flex;flex-direction:column;overflow:hidden}.studio-header{height:52px;background:var(--bg-panel);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 16px;z-index:10}.brand-section{display:flex;align-items:center;gap:10px}.brand-logo{font-size:18px}.brand-title{font-size:15px;font-weight:700;color:var(--text-main);letter-spacing:-.01em}.brand-badge{font-size:11px;font-weight:600;background:var(--brand-light);color:var(--brand);padding:2px 8px;border-radius:999px}.header-controls{display:flex;align-items:center;gap:12px}.account-select-wrap{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-muted)}.account-select{padding:5px 10px;font-size:13px;border-radius:6px;border:1px solid var(--border);background:#fff;color:var(--text-main);outline:none;cursor:pointer}.account-select:focus{border-color:var(--border-focus)}.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:#fff;color:var(--text-main);transition:all .15s ease;text-decoration:none}.btn:hover{background:#f1f5f9}.btn-primary{background:var(--brand);color:#fff;border-color:var(--brand)}.btn-primary:hover{background:var(--brand-hover);border-color:var(--brand-hover)}.btn-success{background:#059669;color:#fff;border-color:#059669}.btn-success:hover{background:#047857;border-color:#047857}.studio-body{flex:1;display:flex;min-height:0;overflow:hidden}.panel{display:flex;flex-direction:column;background:var(--bg-panel);min-height:0}.panel-header{padding:10px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:#fafbfc}.panel-title{font-size:13px;font-weight:650;color:var(--text-main);display:flex;align-items:center;gap:6px}.left-panel{width:360px;border-right:1px solid var(--border);flex-shrink:0}.tab-nav{display:flex;border-bottom:1px solid var(--border);background:#f8fafc}.tab-btn{flex:1;padding:10px 12px;font-size:13px;font-weight:600;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}.tab-btn:hover{color:var(--text-main)}.tab-btn.active{color:var(--brand);background:#fff;border-bottom-color:var(--brand)}.tab-content{flex:1;overflow-y:auto;padding:16px}.config-section{margin-bottom:22px}.section-label{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:12px;display:flex;align-items:center;gap:6px}.form-group{margin-bottom:14px}.form-label{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--text-main);margin-bottom:6px}.form-val{font-family:var(--font-mono);font-size:11px;color:var(--text-muted)}.slider-input{width:100%;accent-color:var(--brand);cursor:pointer}.color-input-row{display:flex;align-items:center;gap:8px}.color-picker{-webkit-appearance:none;appearance:none;border:1px solid var(--border);border-radius:6px;width:32px;height:32px;cursor:pointer;padding:2px;background:#fff}.color-picker::-webkit-color-swatch-wrapper{padding:0}.color-picker::-webkit-color-swatch{border:none;border-radius:4px}.color-hex{flex:1;padding:6px 10px;font-family:var(--font-mono);font-size:12px;border:1px solid var(--border);border-radius:6px;color:var(--text-main)}.switch-label{display:flex;align-items:center;justify-content:space-between;cursor:pointer;font-size:12px}.switch-toggle{position:relative;display:inline-block;width:38px;height:20px}.switch-toggle input{opacity:0;width:0;height:0}.slider-round{position:absolute;cursor:pointer;inset:0;background-color:#cbd5e1;transition:.2s;border-radius:20px}.slider-round:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background-color:#fff;transition:.2s;border-radius:50%}input:checked+.slider-round{background-color:var(--brand)}input:checked+.slider-round:before{transform:translate(18px)}.css-editor{width:100%;height:140px;font-family:var(--font-mono);font-size:12px;line-height:1.5;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:#fafbfc;color:#0f172a;resize:vertical;outline:none}.css-editor:focus{border-color:var(--border-focus);background:#fff}.syntax-card{border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:12px;background:#fff;transition:border-color .15s}.syntax-card:hover{border-color:#cbd5e1}.syntax-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}.syntax-card-title{font-size:13px;font-weight:650;color:var(--text-main)}.syntax-card-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;background:#f1f5f9;color:var(--text-muted)}.syntax-card-desc{font-size:12px;color:var(--text-muted);margin-bottom:8px;line-height:1.4}.syntax-code-snippet{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px 10px;font-family:var(--font-mono);font-size:11px;color:#334155;white-space:pre-wrap;margin-bottom:8px}.syntax-btn-insert{width:100%;padding:4px 8px;font-size:11px;font-weight:600;border:1px dashed var(--border);border-radius:4px;background:#fafbfc;color:var(--brand);cursor:pointer}.syntax-btn-insert:hover{background:var(--brand-light);border-color:var(--brand)}.editor-panel{flex:1;border-right:1px solid var(--border);min-width:320px}.editor-toolbar{display:flex;gap:8px;align-items:center}.editor-textarea{flex:1;width:100%;border:none;outline:none;resize:none;padding:16px 20px;font-family:var(--font-mono);font-size:13px;line-height:1.7;color:#1e293b;background:#fff}.editor-footer{padding:6px 16px;border-top:1px solid var(--border);background:#fafbfc;font-size:11px;color:var(--text-muted);display:flex;justify-content:space-between}.preview-panel{width:480px;flex-shrink:0;background:#f1f5f9}.preview-container{flex:1;overflow-y:auto;display:flex;flex-direction:column;align-items:center;padding:24px 16px 48px}.device-frame{width:430px;min-height:680px;background:#fff;border-radius:18px;box-shadow:0 10px 30px #00000014,0 1px 3px #0000000d;overflow:hidden;display:flex;flex-direction:column;border:1px solid #e2e8f0}.device-notch-bar{height:28px;background:#fafbfc;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-size:11px;font-weight:600;color:#64748b}.device-screen{flex:1;padding:20px 16px 36px;box-sizing:border-box;overflow-y:auto}.toast-container{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}.toast{background:#1e293b;color:#fff;padding:10px 16px;border-radius:8px;font-size:13px;font-weight:500;box-shadow:0 6px 18px #00000026;opacity:0;transform:translateY(10px);transition:all .25s ease;pointer-events:auto}.toast.show{opacity:1;transform:translateY(0)}.toast.toast-success{background:#065f46;border:1px solid #10b981}.preset-group{margin-bottom:10px;padding:10px 12px;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius-sm);transition:border-color .15s}.preset-group:hover{border-color:#cbd5e1}.preset-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.preset-title{font-size:12px;font-weight:650;color:var(--text-main);display:flex;align-items:center;gap:5px}.preset-select{width:100%;padding:5px 8px;font-size:12px;font-weight:500;border:1px solid var(--border);border-radius:4px;background:#fff;color:var(--text-main);outline:none;cursor:pointer;transition:border-color .15s}.preset-select:focus{border-color:var(--brand)}.preset-desc{font-size:11px;color:var(--text-muted);margin-top:5px;line-height:1.4;padding-left:2px}.btn-extract-css{padding:2px 7px;font-size:11px;font-weight:550;color:var(--brand);background:#fff;border:1px solid var(--border);border-radius:4px;cursor:pointer;transition:all .15s ease;white-space:nowrap}.btn-extract-css:hover{background:#f1f5f9;border-color:var(--brand)}#studio-editor-host{display:none}
|