autocrew 0.1.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/HAMLETDEER.md +562 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/README_CN.md +190 -0
- package/adapters/openclaw/index.ts +68 -0
- package/bin/autocrew.mjs +23 -0
- package/bin/autocrew.ts +13 -0
- package/openclaw.plugin.json +36 -0
- package/package.json +74 -0
- package/skills/_writing-style/SKILL.md +68 -0
- package/skills/audience-profiler/SKILL.md +241 -0
- package/skills/content-attribution/SKILL.md +128 -0
- package/skills/content-review/SKILL.md +257 -0
- package/skills/cover-generator/SKILL.md +93 -0
- package/skills/humanizer-zh/SKILL.md +75 -0
- package/skills/intel-digest/SKILL.md +57 -0
- package/skills/intel-pull/SKILL.md +74 -0
- package/skills/manage-pipeline/SKILL.md +63 -0
- package/skills/memory-distill/SKILL.md +89 -0
- package/skills/onboarding/SKILL.md +117 -0
- package/skills/pipeline-status/SKILL.md +51 -0
- package/skills/platform-rewrite/SKILL.md +125 -0
- package/skills/pre-publish/SKILL.md +142 -0
- package/skills/publish-content/SKILL.md +500 -0
- package/skills/remix-content/SKILL.md +77 -0
- package/skills/research/SKILL.md +127 -0
- package/skills/setup/SKILL.md +353 -0
- package/skills/spawn-batch-writer/SKILL.md +66 -0
- package/skills/spawn-planner/SKILL.md +72 -0
- package/skills/spawn-writer/SKILL.md +60 -0
- package/skills/teardown/SKILL.md +144 -0
- package/skills/title-craft/SKILL.md +234 -0
- package/skills/topic-ideas/SKILL.md +105 -0
- package/skills/video-timeline/SKILL.md +117 -0
- package/skills/write-script/SKILL.md +232 -0
- package/skills/xhs-cover-review/SKILL.md +48 -0
- package/src/adapters/browser/browser-cdp.ts +260 -0
- package/src/adapters/browser/browser-relay.ts +236 -0
- package/src/adapters/browser/gateway-client.ts +148 -0
- package/src/adapters/browser/types.ts +36 -0
- package/src/adapters/image/gemini.ts +219 -0
- package/src/adapters/research/tikhub.ts +19 -0
- package/src/cli/banner.ts +18 -0
- package/src/cli/bootstrap.ts +33 -0
- package/src/cli/commands/adapt.ts +28 -0
- package/src/cli/commands/advance.ts +28 -0
- package/src/cli/commands/assets.ts +24 -0
- package/src/cli/commands/audit.ts +18 -0
- package/src/cli/commands/contents.ts +18 -0
- package/src/cli/commands/cover.ts +58 -0
- package/src/cli/commands/events.ts +17 -0
- package/src/cli/commands/humanize.ts +27 -0
- package/src/cli/commands/index.ts +80 -0
- package/src/cli/commands/init.ts +28 -0
- package/src/cli/commands/intel.ts +55 -0
- package/src/cli/commands/learn.ts +34 -0
- package/src/cli/commands/memory.ts +18 -0
- package/src/cli/commands/migrate.ts +24 -0
- package/src/cli/commands/open.ts +21 -0
- package/src/cli/commands/pipelines.ts +18 -0
- package/src/cli/commands/pre-publish.ts +27 -0
- package/src/cli/commands/profile.ts +31 -0
- package/src/cli/commands/research.ts +36 -0
- package/src/cli/commands/restore.ts +28 -0
- package/src/cli/commands/review.ts +61 -0
- package/src/cli/commands/start.ts +28 -0
- package/src/cli/commands/status.ts +14 -0
- package/src/cli/commands/templates.ts +15 -0
- package/src/cli/commands/topics.ts +18 -0
- package/src/cli/commands/trash.ts +28 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/commands/versions.ts +24 -0
- package/src/cli/index.ts +40 -0
- package/src/data/sensitive-words-builtin.json +114 -0
- package/src/data/source-presets.yaml +54 -0
- package/src/e2e.test.ts +596 -0
- package/src/modules/auth/cookie-manager.ts +113 -0
- package/src/modules/cards/template-engine.ts +74 -0
- package/src/modules/cards/templates/comparison-table.ts +71 -0
- package/src/modules/cards/templates/data-chart.ts +76 -0
- package/src/modules/cards/templates/flow-chart.ts +49 -0
- package/src/modules/cards/templates/key-points.ts +59 -0
- package/src/modules/cover/prompt-builder.test.ts +157 -0
- package/src/modules/cover/prompt-builder.ts +212 -0
- package/src/modules/cover/ratio-adapter.test.ts +122 -0
- package/src/modules/cover/ratio-adapter.ts +104 -0
- package/src/modules/filter/sensitive-words.test.ts +72 -0
- package/src/modules/filter/sensitive-words.ts +212 -0
- package/src/modules/humanizer/zh.test.ts +75 -0
- package/src/modules/humanizer/zh.ts +175 -0
- package/src/modules/intel/collector.ts +19 -0
- package/src/modules/intel/collectors/competitor.test.ts +71 -0
- package/src/modules/intel/collectors/competitor.ts +65 -0
- package/src/modules/intel/collectors/rss.test.ts +56 -0
- package/src/modules/intel/collectors/rss.ts +70 -0
- package/src/modules/intel/collectors/trends.test.ts +80 -0
- package/src/modules/intel/collectors/trends.ts +107 -0
- package/src/modules/intel/collectors/web-search.test.ts +85 -0
- package/src/modules/intel/collectors/web-search.ts +81 -0
- package/src/modules/intel/integration.test.ts +203 -0
- package/src/modules/intel/intel-engine.test.ts +103 -0
- package/src/modules/intel/intel-engine.ts +96 -0
- package/src/modules/intel/source-config.test.ts +113 -0
- package/src/modules/intel/source-config.ts +131 -0
- package/src/modules/learnings/diff-tracker.test.ts +144 -0
- package/src/modules/learnings/diff-tracker.ts +189 -0
- package/src/modules/learnings/rule-distiller.ts +141 -0
- package/src/modules/memory/distill.ts +208 -0
- package/src/modules/migrate/legacy-migrate.test.ts +169 -0
- package/src/modules/migrate/legacy-migrate.ts +229 -0
- package/src/modules/pro/api-client.ts +192 -0
- package/src/modules/pro/gate.test.ts +110 -0
- package/src/modules/pro/gate.ts +104 -0
- package/src/modules/profile/creator-profile.test.ts +178 -0
- package/src/modules/profile/creator-profile.ts +248 -0
- package/src/modules/publish/douyin-api.ts +34 -0
- package/src/modules/publish/wechat-mp.ts +320 -0
- package/src/modules/publish/xiaohongshu-api.ts +127 -0
- package/src/modules/research/free-engine.ts +360 -0
- package/src/modules/timeline/markup-generator.ts +63 -0
- package/src/modules/timeline/parser.ts +275 -0
- package/src/modules/workflow/templates.ts +124 -0
- package/src/modules/writing/platform-rewrite.ts +190 -0
- package/src/modules/writing/title-hashtag.ts +385 -0
- package/src/runtime/context.test.ts +97 -0
- package/src/runtime/context.ts +129 -0
- package/src/runtime/events.test.ts +83 -0
- package/src/runtime/events.ts +104 -0
- package/src/runtime/hooks.ts +174 -0
- package/src/runtime/tool-runner.test.ts +204 -0
- package/src/runtime/tool-runner.ts +282 -0
- package/src/runtime/workflow-engine.test.ts +455 -0
- package/src/runtime/workflow-engine.ts +391 -0
- package/src/server/index.ts +409 -0
- package/src/server/start.ts +39 -0
- package/src/storage/local-store.test.ts +304 -0
- package/src/storage/local-store.ts +704 -0
- package/src/storage/pipeline-store.test.ts +363 -0
- package/src/storage/pipeline-store.ts +698 -0
- package/src/tools/asset.ts +96 -0
- package/src/tools/content-save.ts +276 -0
- package/src/tools/cover-review.ts +221 -0
- package/src/tools/humanize.ts +54 -0
- package/src/tools/init.ts +133 -0
- package/src/tools/intel.ts +92 -0
- package/src/tools/memory.ts +76 -0
- package/src/tools/pipeline-ops.ts +109 -0
- package/src/tools/pipeline.ts +168 -0
- package/src/tools/pre-publish.ts +232 -0
- package/src/tools/publish.ts +183 -0
- package/src/tools/registry.ts +198 -0
- package/src/tools/research.ts +304 -0
- package/src/tools/review.ts +305 -0
- package/src/tools/rewrite.ts +165 -0
- package/src/tools/status.ts +30 -0
- package/src/tools/timeline.ts +234 -0
- package/src/tools/topic-create.ts +50 -0
- package/src/types/providers.ts +69 -0
- package/src/types/timeline.test.ts +147 -0
- package/src/types/timeline.ts +83 -0
- package/src/utils/retry.test.ts +97 -0
- package/src/utils/retry.ts +85 -0
- package/templates/AGENTS.md +99 -0
- package/templates/SOUL.md +31 -0
- package/templates/TOOLS.md +76 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_IMAGE_GENERATOR_SCRIPT = path.join(
|
|
7
|
+
os.homedir(),
|
|
8
|
+
".openclaw",
|
|
9
|
+
"workspace-muse",
|
|
10
|
+
"skills",
|
|
11
|
+
"seedream",
|
|
12
|
+
"scripts",
|
|
13
|
+
"generate_image.py",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const DEFAULT_WECHAT_PUBLISH_SCRIPT = path.join(
|
|
17
|
+
os.homedir(),
|
|
18
|
+
".openclaw",
|
|
19
|
+
"xiaohu-wechat-format",
|
|
20
|
+
"scripts",
|
|
21
|
+
"publish.py",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export interface WechatMpDraftOptions {
|
|
25
|
+
articlePath: string;
|
|
26
|
+
theme?: string;
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
skipImages?: boolean;
|
|
29
|
+
author?: string;
|
|
30
|
+
imageSize?: string;
|
|
31
|
+
imageGeneratorScript?: string;
|
|
32
|
+
imageApiKey?: string;
|
|
33
|
+
wechatPublishScript?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WechatMpDraftResult {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
articlePath: string;
|
|
39
|
+
publishInput: string;
|
|
40
|
+
coverPath: string;
|
|
41
|
+
imageCount: number;
|
|
42
|
+
generatedImages: string[];
|
|
43
|
+
command?: string;
|
|
44
|
+
stdout?: string;
|
|
45
|
+
stderr?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveImageGeneratorScript(customPath?: string): string {
|
|
50
|
+
return customPath || process.env.AUTOCREW_IMAGE_GENERATOR_SCRIPT || DEFAULT_IMAGE_GENERATOR_SCRIPT;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveWechatPublishScript(customPath?: string): string {
|
|
54
|
+
return customPath || process.env.AUTOCREW_WECHAT_PUBLISH_SCRIPT || DEFAULT_WECHAT_PUBLISH_SCRIPT;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveImageApiKey(customKey?: string): string | undefined {
|
|
58
|
+
return customKey || process.env.AUTOCREW_IMAGE_API_KEY || process.env.ARK_API_KEY || undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function escapeRegExp(value: string): string {
|
|
62
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractTitle(markdown: string): string {
|
|
66
|
+
let content = markdown;
|
|
67
|
+
if (content.startsWith("---")) {
|
|
68
|
+
const closing = content.indexOf("\n---", 3);
|
|
69
|
+
if (closing >= 0) {
|
|
70
|
+
content = content.slice(closing + 4);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const h1 = content.match(/^#\s+(.+)$/m);
|
|
75
|
+
if (h1?.[1]) return h1[1].trim();
|
|
76
|
+
|
|
77
|
+
const lines = content.split("\n");
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
if (!trimmed || trimmed === "---") continue;
|
|
81
|
+
if (/^[a-zA-Z0-9_-]+:\s*/.test(trimmed)) continue;
|
|
82
|
+
return trimmed.slice(0, 80);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return "tech article";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function fileExists(targetPath: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(targetPath);
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runCommand(
|
|
98
|
+
command: string,
|
|
99
|
+
args: string[],
|
|
100
|
+
cwd?: string,
|
|
101
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const child = spawn(command, args, {
|
|
104
|
+
cwd,
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
env: process.env,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let stdout = "";
|
|
110
|
+
let stderr = "";
|
|
111
|
+
|
|
112
|
+
child.stdout.on("data", (chunk) => {
|
|
113
|
+
stdout += String(chunk);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
child.stderr.on("data", (chunk) => {
|
|
117
|
+
stderr += String(chunk);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
child.on("error", reject);
|
|
121
|
+
child.on("close", (code) => {
|
|
122
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function generateImage(
|
|
128
|
+
prompt: string,
|
|
129
|
+
outputPath: string,
|
|
130
|
+
size: string,
|
|
131
|
+
imageGeneratorScript: string,
|
|
132
|
+
imageApiKey?: string,
|
|
133
|
+
): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
134
|
+
const cwd = path.dirname(outputPath);
|
|
135
|
+
await fs.mkdir(cwd, { recursive: true });
|
|
136
|
+
|
|
137
|
+
const args = [
|
|
138
|
+
"run",
|
|
139
|
+
imageGeneratorScript,
|
|
140
|
+
"--prompt",
|
|
141
|
+
prompt,
|
|
142
|
+
"--filename",
|
|
143
|
+
path.basename(outputPath),
|
|
144
|
+
"--size",
|
|
145
|
+
size,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
if (imageApiKey) {
|
|
149
|
+
args.push("--api-key", imageApiKey);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = await runCommand("uv", args, cwd);
|
|
153
|
+
return {
|
|
154
|
+
ok: result.code === 0,
|
|
155
|
+
stdout: result.stdout,
|
|
156
|
+
stderr: result.stderr,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function publishWechatMpDraft(
|
|
161
|
+
options: WechatMpDraftOptions,
|
|
162
|
+
): Promise<WechatMpDraftResult> {
|
|
163
|
+
const articlePath = path.resolve(options.articlePath);
|
|
164
|
+
if (!(await fileExists(articlePath))) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
articlePath,
|
|
168
|
+
publishInput: articlePath,
|
|
169
|
+
coverPath: "",
|
|
170
|
+
imageCount: 0,
|
|
171
|
+
generatedImages: [],
|
|
172
|
+
error: `Article not found: ${articlePath}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const imageGeneratorScript = resolveImageGeneratorScript(options.imageGeneratorScript);
|
|
177
|
+
const wechatPublishScript = resolveWechatPublishScript(options.wechatPublishScript);
|
|
178
|
+
const imageApiKey = resolveImageApiKey(options.imageApiKey);
|
|
179
|
+
|
|
180
|
+
const articleDir = path.dirname(articlePath);
|
|
181
|
+
const imagesDir = path.join(articleDir, "images");
|
|
182
|
+
await fs.mkdir(imagesDir, { recursive: true });
|
|
183
|
+
|
|
184
|
+
const originalContent = await fs.readFile(articlePath, "utf-8");
|
|
185
|
+
const imageMatches = [...originalContent.matchAll(/\[IMAGE:\s*(.+?)\]/g)];
|
|
186
|
+
|
|
187
|
+
let newContent = originalContent;
|
|
188
|
+
let coverPath = "";
|
|
189
|
+
const generatedImages: string[] = [];
|
|
190
|
+
|
|
191
|
+
for (let index = 0; index < imageMatches.length; index += 1) {
|
|
192
|
+
const match = imageMatches[index];
|
|
193
|
+
const prompt = match[1]?.trim();
|
|
194
|
+
if (!prompt) continue;
|
|
195
|
+
|
|
196
|
+
const filename = `img-${String(index + 1).padStart(2, "0")}.png`;
|
|
197
|
+
const imagePath = path.join(imagesDir, filename);
|
|
198
|
+
const relativePath = `images/${filename}`;
|
|
199
|
+
|
|
200
|
+
const exists = await fileExists(imagePath);
|
|
201
|
+
if (!options.skipImages || !exists) {
|
|
202
|
+
const imageResult = await generateImage(prompt, imagePath, {
|
|
203
|
+
size: options.imageSize || "16:9",
|
|
204
|
+
imageGeneratorScript,
|
|
205
|
+
imageApiKey,
|
|
206
|
+
});
|
|
207
|
+
if (!imageResult.ok) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
articlePath,
|
|
211
|
+
publishInput: articlePath,
|
|
212
|
+
coverPath: coverPath || imagePath,
|
|
213
|
+
imageCount: generatedImages.length,
|
|
214
|
+
generatedImages,
|
|
215
|
+
stderr: imageResult.stderr,
|
|
216
|
+
error: `Failed to generate image ${filename}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
generatedImages.push(imagePath);
|
|
222
|
+
if (!coverPath) {
|
|
223
|
+
coverPath = imagePath;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const escaped = escapeRegExp(match[0]);
|
|
227
|
+
newContent = newContent.replace(new RegExp(escaped), ``);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!coverPath) {
|
|
231
|
+
const title = extractTitle(originalContent);
|
|
232
|
+
const fallbackPrompt = `Dark cinematic tech atmosphere, abstract concept art for article: ${title.slice(0, 60)}, no text, moody lighting`;
|
|
233
|
+
const fallbackCoverPath = path.join(articleDir, "cover.png");
|
|
234
|
+
const fallbackResult = await generateImage(fallbackPrompt, fallbackCoverPath, {
|
|
235
|
+
size: options.imageSize || "16:9",
|
|
236
|
+
imageGeneratorScript,
|
|
237
|
+
imageApiKey,
|
|
238
|
+
});
|
|
239
|
+
if (!fallbackResult.ok) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
articlePath,
|
|
243
|
+
publishInput: articlePath,
|
|
244
|
+
coverPath: fallbackCoverPath,
|
|
245
|
+
imageCount: generatedImages.length,
|
|
246
|
+
generatedImages,
|
|
247
|
+
stderr: fallbackResult.stderr,
|
|
248
|
+
error: "Failed to generate fallback cover",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
coverPath = fallbackCoverPath;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let publishInput = articlePath;
|
|
255
|
+
const processedPath = path.join(articleDir, "_processed_article.md");
|
|
256
|
+
if (imageMatches.length > 0) {
|
|
257
|
+
await fs.writeFile(processedPath, newContent, "utf-8");
|
|
258
|
+
publishInput = processedPath;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (options.dryRun) {
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
articlePath,
|
|
265
|
+
publishInput,
|
|
266
|
+
coverPath,
|
|
267
|
+
imageCount: imageMatches.length,
|
|
268
|
+
generatedImages,
|
|
269
|
+
command: `python3 ${wechatPublishScript} --input ${publishInput} --cover ${coverPath} --theme ${options.theme || "newspaper"} --author ${options.author || "Lawrence"}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!(await fileExists(wechatPublishScript))) {
|
|
274
|
+
if (publishInput === processedPath) {
|
|
275
|
+
await fs.rm(processedPath, { force: true });
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
articlePath,
|
|
280
|
+
publishInput,
|
|
281
|
+
coverPath,
|
|
282
|
+
imageCount: imageMatches.length,
|
|
283
|
+
generatedImages,
|
|
284
|
+
error: `WeChat publish script not found: ${wechatPublishScript}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const publishArgs = [
|
|
289
|
+
wechatPublishScript,
|
|
290
|
+
"--input",
|
|
291
|
+
publishInput,
|
|
292
|
+
"--cover",
|
|
293
|
+
coverPath,
|
|
294
|
+
"--theme",
|
|
295
|
+
options.theme || "newspaper",
|
|
296
|
+
"--author",
|
|
297
|
+
options.author || "Lawrence",
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const publishCwd = path.dirname(path.dirname(wechatPublishScript));
|
|
301
|
+
const publishResult = await runCommand("python3", publishArgs, publishCwd);
|
|
302
|
+
|
|
303
|
+
if (publishInput === processedPath) {
|
|
304
|
+
await fs.writeFile(articlePath, newContent, "utf-8");
|
|
305
|
+
await fs.rm(processedPath, { force: true });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
ok: publishResult.code === 0,
|
|
310
|
+
articlePath,
|
|
311
|
+
publishInput,
|
|
312
|
+
coverPath,
|
|
313
|
+
imageCount: imageMatches.length,
|
|
314
|
+
generatedImages,
|
|
315
|
+
stdout: publishResult.stdout,
|
|
316
|
+
stderr: publishResult.stderr,
|
|
317
|
+
command: `python3 ${publishArgs.join(" ")}`,
|
|
318
|
+
error: publishResult.code === 0 ? undefined : "WeChat draft publish failed",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XiaoHongShu API publisher — calls scripts/publish_xhs.py via child_process.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `xhs` Python library with cookie-based auth and local signing.
|
|
5
|
+
* Defaults to private publishing for safety.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const PUBLISH_SCRIPT = path.resolve(
|
|
11
|
+
import.meta.dirname ?? path.join(process.cwd(), "src", "modules", "publish"),
|
|
12
|
+
"..",
|
|
13
|
+
"..",
|
|
14
|
+
"..",
|
|
15
|
+
"scripts",
|
|
16
|
+
"publish_xhs.py",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export interface XhsPublishOptions {
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
imagePaths: string[];
|
|
23
|
+
cookie?: string;
|
|
24
|
+
isPrivate?: boolean;
|
|
25
|
+
postTime?: string;
|
|
26
|
+
dryRun?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface XhsPublishResult {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
noteId?: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
isPrivate?: boolean;
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runPython(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const child = spawn("python3", args, {
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
env: process.env,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let stderr = "";
|
|
47
|
+
|
|
48
|
+
child.stdout.on("data", (chunk) => { stdout += String(chunk); });
|
|
49
|
+
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
|
50
|
+
child.on("error", (err) => {
|
|
51
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
52
|
+
resolve({ code: 1, stdout: "", stderr: "python3 not found. Ensure Python 3 is installed." });
|
|
53
|
+
} else {
|
|
54
|
+
reject(err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
child.on("close", (code) => { resolve({ code: code ?? 1, stdout, stderr }); });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function publishToXiaohongshu(options: XhsPublishOptions): Promise<XhsPublishResult> {
|
|
62
|
+
const {
|
|
63
|
+
title,
|
|
64
|
+
description,
|
|
65
|
+
imagePaths,
|
|
66
|
+
cookie,
|
|
67
|
+
isPrivate = true,
|
|
68
|
+
postTime,
|
|
69
|
+
dryRun = false,
|
|
70
|
+
} = options;
|
|
71
|
+
|
|
72
|
+
if (!imagePaths.length) {
|
|
73
|
+
return { ok: false, error: "At least one image is required." };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const args: string[] = [
|
|
77
|
+
PUBLISH_SCRIPT,
|
|
78
|
+
"--title", title,
|
|
79
|
+
"--desc", description,
|
|
80
|
+
"--images", ...imagePaths,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
if (cookie) {
|
|
84
|
+
args.push("--cookie", cookie);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isPrivate) {
|
|
88
|
+
args.push("--private");
|
|
89
|
+
} else {
|
|
90
|
+
args.push("--public");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (postTime) {
|
|
94
|
+
args.push("--post-time", postTime);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
args.push("--dry-run");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = await runPython(args);
|
|
102
|
+
|
|
103
|
+
if (result.stderr.includes("xhs library not installed") || result.stderr.includes("No module named")) {
|
|
104
|
+
return { ok: false, error: "xhs Python library not installed. Run: pip install xhs" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (result.stderr.includes("python3 not found")) {
|
|
108
|
+
return { ok: false, error: result.stderr };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(result.stdout);
|
|
113
|
+
return {
|
|
114
|
+
ok: parsed.ok ?? false,
|
|
115
|
+
noteId: parsed.note_id,
|
|
116
|
+
url: parsed.url,
|
|
117
|
+
isPrivate: parsed.is_private,
|
|
118
|
+
dryRun: parsed.dry_run,
|
|
119
|
+
error: parsed.error,
|
|
120
|
+
};
|
|
121
|
+
} catch {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: result.stderr || result.stdout || `publish_xhs.py exited with code ${result.code}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|