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,229 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import {
|
|
5
|
+
initPipeline,
|
|
6
|
+
saveTopic,
|
|
7
|
+
stagePath,
|
|
8
|
+
slugify,
|
|
9
|
+
type TopicCandidate,
|
|
10
|
+
type ProjectMeta,
|
|
11
|
+
type PipelineStage,
|
|
12
|
+
} from "../../storage/pipeline-store.js";
|
|
13
|
+
|
|
14
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface MigrationResult {
|
|
17
|
+
topicsMigrated: number;
|
|
18
|
+
contentsMigrated: number;
|
|
19
|
+
errors: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Status → Stage Mapping ─────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const STATUS_TO_STAGE: Record<string, PipelineStage> = {
|
|
25
|
+
topic_saved: "topics",
|
|
26
|
+
drafting: "drafting",
|
|
27
|
+
draft_ready: "drafting",
|
|
28
|
+
reviewing: "drafting",
|
|
29
|
+
revision: "drafting",
|
|
30
|
+
approved: "production",
|
|
31
|
+
cover_pending: "production",
|
|
32
|
+
publish_ready: "production",
|
|
33
|
+
publishing: "production",
|
|
34
|
+
published: "published",
|
|
35
|
+
archived: "trash",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ─── Migration ──────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function dirExists(p: string): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
const stat = await fs.stat(p);
|
|
43
|
+
return stat.isDirectory();
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
50
|
+
try {
|
|
51
|
+
const stat = await fs.stat(p);
|
|
52
|
+
return stat.isFile();
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function migrateTopics(
|
|
59
|
+
dataDir: string,
|
|
60
|
+
errors: string[],
|
|
61
|
+
): Promise<number> {
|
|
62
|
+
const topicsDir = path.join(dataDir, "topics");
|
|
63
|
+
let migrated = 0;
|
|
64
|
+
|
|
65
|
+
let files: string[];
|
|
66
|
+
try {
|
|
67
|
+
files = await fs.readdir(topicsDir);
|
|
68
|
+
} catch {
|
|
69
|
+
return 0; // no legacy topics dir
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
if (!file.endsWith(".json")) continue;
|
|
74
|
+
try {
|
|
75
|
+
const raw = await fs.readFile(path.join(topicsDir, file), "utf-8");
|
|
76
|
+
const json = JSON.parse(raw);
|
|
77
|
+
|
|
78
|
+
const topic: TopicCandidate = {
|
|
79
|
+
title: json.title ?? "Untitled",
|
|
80
|
+
domain: json.domain ?? "general",
|
|
81
|
+
score: {
|
|
82
|
+
heat: json.score?.heat ?? json.heat ?? 50,
|
|
83
|
+
differentiation: json.score?.differentiation ?? json.differentiation ?? 50,
|
|
84
|
+
audienceFit: json.score?.audienceFit ?? json.audience_fit ?? 50,
|
|
85
|
+
overall: json.score?.overall ?? json.overall ?? 50,
|
|
86
|
+
},
|
|
87
|
+
formats: json.formats ?? [],
|
|
88
|
+
suggestedPlatforms: json.suggested_platforms ?? json.platforms ?? [],
|
|
89
|
+
createdAt: json.created_at ?? json.createdAt ?? new Date().toISOString(),
|
|
90
|
+
intelRefs: json.intel_refs ?? json.intelRefs ?? [],
|
|
91
|
+
angles: json.angles ?? [],
|
|
92
|
+
audienceResonance: json.audience_resonance ?? json.audienceResonance ?? "",
|
|
93
|
+
references: json.references ?? [],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Idempotent check: does the target file already exist?
|
|
97
|
+
const targetFile = `${slugify(topic.domain)}-${slugify(topic.title)}.md`;
|
|
98
|
+
const targetPath = path.join(stagePath("topics", dataDir), targetFile);
|
|
99
|
+
if (await fileExists(targetPath)) continue;
|
|
100
|
+
|
|
101
|
+
await saveTopic(topic, dataDir);
|
|
102
|
+
migrated++;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
errors.push(`topic ${file}: ${(err as Error).message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return migrated;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function migrateContents(
|
|
112
|
+
dataDir: string,
|
|
113
|
+
errors: string[],
|
|
114
|
+
): Promise<number> {
|
|
115
|
+
const contentsDir = path.join(dataDir, "contents");
|
|
116
|
+
let migrated = 0;
|
|
117
|
+
|
|
118
|
+
let entries: string[];
|
|
119
|
+
try {
|
|
120
|
+
entries = await fs.readdir(contentsDir);
|
|
121
|
+
} catch {
|
|
122
|
+
return 0; // no legacy contents dir
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!entry.startsWith("content-")) continue;
|
|
127
|
+
const srcDir = path.join(contentsDir, entry);
|
|
128
|
+
if (!(await dirExists(srcDir))) continue;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Read legacy meta.json
|
|
132
|
+
const metaPath = path.join(srcDir, "meta.json");
|
|
133
|
+
if (!(await fileExists(metaPath))) {
|
|
134
|
+
errors.push(`content ${entry}: missing meta.json`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const metaRaw = await fs.readFile(metaPath, "utf-8");
|
|
139
|
+
const legacyMeta = JSON.parse(metaRaw);
|
|
140
|
+
|
|
141
|
+
const status: string = legacyMeta.status ?? "drafting";
|
|
142
|
+
const stage = STATUS_TO_STAGE[status] ?? "drafting";
|
|
143
|
+
|
|
144
|
+
// For "topics" stage content, skip — topics are handled separately
|
|
145
|
+
if (stage === "topics") continue;
|
|
146
|
+
|
|
147
|
+
const projectName = slugify(legacyMeta.title ?? entry);
|
|
148
|
+
const targetDir = path.join(stagePath(stage, dataDir), projectName);
|
|
149
|
+
|
|
150
|
+
// Idempotent: skip if target already exists
|
|
151
|
+
if (await dirExists(targetDir)) continue;
|
|
152
|
+
|
|
153
|
+
// Create target directory
|
|
154
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
155
|
+
await fs.mkdir(path.join(targetDir, "references"), { recursive: true });
|
|
156
|
+
|
|
157
|
+
// Copy files from source (except meta.json, which we convert)
|
|
158
|
+
const srcFiles = await fs.readdir(srcDir);
|
|
159
|
+
for (const f of srcFiles) {
|
|
160
|
+
if (f === "meta.json") continue;
|
|
161
|
+
const srcFile = path.join(srcDir, f);
|
|
162
|
+
const stat = await fs.stat(srcFile);
|
|
163
|
+
if (stat.isFile()) {
|
|
164
|
+
await fs.copyFile(srcFile, path.join(targetDir, f));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Convert meta.json → meta.yaml
|
|
169
|
+
const now = new Date().toISOString();
|
|
170
|
+
const draftFile = srcFiles.includes("draft.md") ? "draft-v1.md" : undefined;
|
|
171
|
+
|
|
172
|
+
// If draft.md exists, create draft-v1.md from it
|
|
173
|
+
if (draftFile && srcFiles.includes("draft.md")) {
|
|
174
|
+
const draftContent = await fs.readFile(
|
|
175
|
+
path.join(targetDir, "draft.md"),
|
|
176
|
+
"utf-8",
|
|
177
|
+
);
|
|
178
|
+
await fs.writeFile(
|
|
179
|
+
path.join(targetDir, "draft-v1.md"),
|
|
180
|
+
draftContent,
|
|
181
|
+
"utf-8",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const projectMeta: ProjectMeta = {
|
|
186
|
+
title: legacyMeta.title ?? entry,
|
|
187
|
+
domain: legacyMeta.domain ?? "general",
|
|
188
|
+
format: legacyMeta.format ?? "article",
|
|
189
|
+
createdAt: legacyMeta.created_at ?? legacyMeta.createdAt ?? now,
|
|
190
|
+
sourceTopic: legacyMeta.source_topic ?? "",
|
|
191
|
+
intelRefs: legacyMeta.intel_refs ?? [],
|
|
192
|
+
versions: draftFile
|
|
193
|
+
? [{ file: "draft-v1.md", createdAt: legacyMeta.created_at ?? now, note: "migrated from legacy" }]
|
|
194
|
+
: [],
|
|
195
|
+
current: draftFile ?? "",
|
|
196
|
+
history: [{ stage, entered: now }],
|
|
197
|
+
platforms: legacyMeta.platforms?.map((p: string) => ({
|
|
198
|
+
format: p,
|
|
199
|
+
status: "pending",
|
|
200
|
+
})) ?? [],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
await fs.writeFile(
|
|
204
|
+
path.join(targetDir, "meta.yaml"),
|
|
205
|
+
yaml.dump(projectMeta, { lineWidth: -1 }),
|
|
206
|
+
"utf-8",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
migrated++;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
errors.push(`content ${entry}: ${(err as Error).message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return migrated;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function migrateLegacyData(
|
|
219
|
+
dataDir: string,
|
|
220
|
+
): Promise<MigrationResult> {
|
|
221
|
+
const errors: string[] = [];
|
|
222
|
+
|
|
223
|
+
await initPipeline(dataDir);
|
|
224
|
+
|
|
225
|
+
const topicsMigrated = await migrateTopics(dataDir, errors);
|
|
226
|
+
const contentsMigrated = await migrateContents(dataDir, errors);
|
|
227
|
+
|
|
228
|
+
return { topicsMigrated, contentsMigrated, errors };
|
|
229
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pro API Client — Communicates with the AutoCrew Pro cloud backend.
|
|
3
|
+
*
|
|
4
|
+
* All Pro features (ASR, crawling, competitor analysis, analytics, TTS, digital human)
|
|
5
|
+
* go through this client. The backend is a slimmed-down version of 墨灵 AI.
|
|
6
|
+
*/
|
|
7
|
+
import { readProKey } from "./gate.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BASE_URL = "https://api.autocrew.dev";
|
|
10
|
+
|
|
11
|
+
export interface ProApiOptions {
|
|
12
|
+
dataDir?: string;
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProApiResponse<T = unknown> {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
data?: T;
|
|
19
|
+
error?: string;
|
|
20
|
+
usage?: { used: number; remaining: number; unit: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getBaseUrl(options?: ProApiOptions): string {
|
|
24
|
+
return options?.baseUrl || process.env.AUTOCREW_PRO_API_URL || DEFAULT_BASE_URL;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getHeaders(dataDir?: string): Promise<Record<string, string>> {
|
|
28
|
+
const key = await readProKey(dataDir);
|
|
29
|
+
if (!key) throw new Error("Pro API key not found. Run autocrew upgrade first.");
|
|
30
|
+
return {
|
|
31
|
+
Authorization: `Bearer ${key}`,
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"User-Agent": "autocrew/0.1.0",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function request<T>(
|
|
38
|
+
method: string,
|
|
39
|
+
endpoint: string,
|
|
40
|
+
body: unknown | null,
|
|
41
|
+
options?: ProApiOptions,
|
|
42
|
+
): Promise<ProApiResponse<T>> {
|
|
43
|
+
const url = `${getBaseUrl(options)}${endpoint}`;
|
|
44
|
+
const headers = await getHeaders(options?.dataDir);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(url, {
|
|
48
|
+
method,
|
|
49
|
+
headers,
|
|
50
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const json = (await res.json()) as any;
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: json.error || json.detail || `HTTP ${res.status}`,
|
|
59
|
+
usage: json.usage,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ok: true, data: json.data ?? json, usage: json.usage };
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
return { ok: false, error: `Network error: ${err.message}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Auth ---
|
|
70
|
+
|
|
71
|
+
export async function verifyKey(options?: ProApiOptions): Promise<ProApiResponse<{
|
|
72
|
+
valid: boolean;
|
|
73
|
+
plan: string;
|
|
74
|
+
expiresAt: string | null;
|
|
75
|
+
usage: { used: number; remaining: number; unit: string };
|
|
76
|
+
}>> {
|
|
77
|
+
return request("GET", "/api/pro/verify", null, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getUsage(options?: ProApiOptions): Promise<ProApiResponse> {
|
|
81
|
+
return request("GET", "/api/pro/usage", null, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Research & Crawling ---
|
|
85
|
+
|
|
86
|
+
export async function researchCrawl(
|
|
87
|
+
params: { keyword: string; platform: string; count?: number },
|
|
88
|
+
options?: ProApiOptions,
|
|
89
|
+
): Promise<ProApiResponse> {
|
|
90
|
+
return request("POST", "/api/pro/research/crawl", params, options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function researchTrending(
|
|
94
|
+
params: { platform: string; count?: number },
|
|
95
|
+
options?: ProApiOptions,
|
|
96
|
+
): Promise<ProApiResponse> {
|
|
97
|
+
return request("POST", "/api/pro/research/trending", params, options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Competitor ---
|
|
101
|
+
|
|
102
|
+
export async function competitorProfile(
|
|
103
|
+
params: { profileUrl: string; platform: string },
|
|
104
|
+
options?: ProApiOptions,
|
|
105
|
+
): Promise<ProApiResponse> {
|
|
106
|
+
return request("POST", "/api/pro/competitor/profile", params, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function competitorNotes(
|
|
110
|
+
params: { profileUrl: string; platform: string; limit?: number },
|
|
111
|
+
options?: ProApiOptions,
|
|
112
|
+
): Promise<ProApiResponse> {
|
|
113
|
+
return request("POST", "/api/pro/competitor/notes", params, options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function competitorAnalyze(
|
|
117
|
+
params: { profileUrl: string; platform: string },
|
|
118
|
+
options?: ProApiOptions,
|
|
119
|
+
): Promise<ProApiResponse> {
|
|
120
|
+
return request("POST", "/api/pro/competitor/analyze", params, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Video ---
|
|
124
|
+
|
|
125
|
+
export async function transcribe(
|
|
126
|
+
params: { videoUrl: string },
|
|
127
|
+
options?: ProApiOptions,
|
|
128
|
+
): Promise<ProApiResponse<{ scriptText: string; title: string; author: string; wordCount: number; duration: number }>> {
|
|
129
|
+
return request("POST", "/api/pro/transcribe", params, options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function videoAnalyze(
|
|
133
|
+
params: { videoUrl: string },
|
|
134
|
+
options?: ProApiOptions,
|
|
135
|
+
): Promise<ProApiResponse> {
|
|
136
|
+
return request("POST", "/api/pro/video/analyze", params, options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Analytics ---
|
|
140
|
+
|
|
141
|
+
export async function analyticsAccount(
|
|
142
|
+
params: { platform: string; accountId?: string },
|
|
143
|
+
options?: ProApiOptions,
|
|
144
|
+
): Promise<ProApiResponse> {
|
|
145
|
+
return request("POST", "/api/pro/analytics/account", params, options);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function analyticsContent(
|
|
149
|
+
params: { platform: string; contentUrl: string },
|
|
150
|
+
options?: ProApiOptions,
|
|
151
|
+
): Promise<ProApiResponse> {
|
|
152
|
+
return request("POST", "/api/pro/analytics/content", params, options);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function analyticsReport(
|
|
156
|
+
params: { platform: string; period?: string },
|
|
157
|
+
options?: ProApiOptions,
|
|
158
|
+
): Promise<ProApiResponse> {
|
|
159
|
+
return request("POST", "/api/pro/analytics/report", params, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Cover ---
|
|
163
|
+
|
|
164
|
+
export async function coverGenerate(
|
|
165
|
+
params: { prompt: string; ratio: "16:9" | "4:3"; referenceImagePath?: string },
|
|
166
|
+
options?: ProApiOptions,
|
|
167
|
+
): Promise<ProApiResponse<{ imageUrl: string }>> {
|
|
168
|
+
return request("POST", "/api/pro/cover/generate", params, options);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- TTS & Digital Human ---
|
|
172
|
+
|
|
173
|
+
export async function ttsSynthesize(
|
|
174
|
+
params: { text: string; voiceId?: string },
|
|
175
|
+
options?: ProApiOptions,
|
|
176
|
+
): Promise<ProApiResponse<{ audioUrl: string; duration: number }>> {
|
|
177
|
+
return request("POST", "/api/pro/tts/synthesize", params, options);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function ttsClone(
|
|
181
|
+
params: { sampleAudioUrl: string; name: string },
|
|
182
|
+
options?: ProApiOptions,
|
|
183
|
+
): Promise<ProApiResponse<{ voiceId: string }>> {
|
|
184
|
+
return request("POST", "/api/pro/tts/clone", params, options);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function digitalHumanGenerate(
|
|
188
|
+
params: { scriptText: string; avatarId?: string; voiceId?: string },
|
|
189
|
+
options?: ProApiOptions,
|
|
190
|
+
): Promise<ProApiResponse<{ videoUrl: string; duration: number }>> {
|
|
191
|
+
return request("POST", "/api/pro/digital-human/generate", params, options);
|
|
192
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
getProStatus,
|
|
7
|
+
saveProKey,
|
|
8
|
+
removeProKey,
|
|
9
|
+
readProKey,
|
|
10
|
+
requirePro,
|
|
11
|
+
proGateResponse,
|
|
12
|
+
isProFeature,
|
|
13
|
+
PRO_FEATURES,
|
|
14
|
+
} from "../pro/gate.js";
|
|
15
|
+
|
|
16
|
+
let testDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "autocrew-gate-test-"));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("readProKey / saveProKey / removeProKey", () => {
|
|
27
|
+
it("returns null when .pro file does not exist", async () => {
|
|
28
|
+
const key = await readProKey(testDir);
|
|
29
|
+
expect(key).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("saves and reads back a key", async () => {
|
|
33
|
+
await saveProKey("test-key-abc123", testDir);
|
|
34
|
+
const key = await readProKey(testDir);
|
|
35
|
+
expect(key).toBe("test-key-abc123");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("trims whitespace from saved key", async () => {
|
|
39
|
+
await saveProKey(" my-key \n", testDir);
|
|
40
|
+
const key = await readProKey(testDir);
|
|
41
|
+
expect(key).toBe("my-key");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("removes the key file", async () => {
|
|
45
|
+
await saveProKey("some-key", testDir);
|
|
46
|
+
await removeProKey(testDir);
|
|
47
|
+
const key = await readProKey(testDir);
|
|
48
|
+
expect(key).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("removeProKey is safe when file does not exist", async () => {
|
|
52
|
+
await expect(removeProKey(testDir)).resolves.not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("getProStatus", () => {
|
|
57
|
+
it("returns isPro:false when no key", async () => {
|
|
58
|
+
const status = await getProStatus(testDir);
|
|
59
|
+
expect(status.isPro).toBe(false);
|
|
60
|
+
expect(status.apiKey).toBeNull();
|
|
61
|
+
expect(status.verified).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns isPro:true when key exists", async () => {
|
|
65
|
+
await saveProKey("valid-key", testDir);
|
|
66
|
+
const status = await getProStatus(testDir);
|
|
67
|
+
expect(status.isPro).toBe(true);
|
|
68
|
+
expect(status.apiKey).toBe("valid-key");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("requirePro", () => {
|
|
73
|
+
it("returns null (allow) when Pro key exists", async () => {
|
|
74
|
+
await saveProKey("valid-key", testDir);
|
|
75
|
+
const result = await requirePro("对标账号监控", testDir);
|
|
76
|
+
expect(result).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns error object when no Pro key", async () => {
|
|
80
|
+
const result = await requirePro("对标账号监控", testDir);
|
|
81
|
+
expect(result).not.toBeNull();
|
|
82
|
+
expect(result!.ok).toBe(false);
|
|
83
|
+
expect(result!.error).toContain("对标账号监控");
|
|
84
|
+
expect(result!.upgradeHint).toContain("autocrew upgrade");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("proGateResponse", () => {
|
|
89
|
+
it("includes feature name, upgrade hint, and free alternative", () => {
|
|
90
|
+
const r = proGateResponse("视频文案提取", "手动粘贴文案给我分析");
|
|
91
|
+
expect(r.ok).toBe(false);
|
|
92
|
+
expect(r.error).toContain("视频文案提取");
|
|
93
|
+
expect(r.freeAlternative).toBe("手动粘贴文案给我分析");
|
|
94
|
+
expect(r.upgradeHint).toBeTruthy();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("isProFeature", () => {
|
|
99
|
+
it("returns true for known Pro features", () => {
|
|
100
|
+
for (const f of PRO_FEATURES) {
|
|
101
|
+
expect(isProFeature(f)).toBe(true);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns false for Free features", () => {
|
|
106
|
+
expect(isProFeature("humanize")).toBe(false);
|
|
107
|
+
expect(isProFeature("write-script")).toBe(false);
|
|
108
|
+
expect(isProFeature("content-review")).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pro Gate — Feature gating for AutoCrew Pro.
|
|
3
|
+
*
|
|
4
|
+
* Checks whether the user has a valid Pro API key stored in ~/.autocrew/.pro
|
|
5
|
+
* and provides helpers to guard Pro-only tool actions.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface ProStatus {
|
|
11
|
+
isPro: boolean;
|
|
12
|
+
apiKey: string | null;
|
|
13
|
+
verified: boolean | null;
|
|
14
|
+
expiresAt: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PRO_FILE = ".pro";
|
|
18
|
+
|
|
19
|
+
function getDataDir(customDir?: string): string {
|
|
20
|
+
if (customDir) return customDir;
|
|
21
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
22
|
+
return path.join(home, ".autocrew");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readProKey(dataDir?: string): Promise<string | null> {
|
|
26
|
+
const filePath = path.join(getDataDir(dataDir), PRO_FILE);
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
29
|
+
const key = raw.trim();
|
|
30
|
+
return key.length > 0 ? key : null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function saveProKey(apiKey: string, dataDir?: string): Promise<void> {
|
|
37
|
+
const dir = getDataDir(dataDir);
|
|
38
|
+
await fs.mkdir(dir, { recursive: true });
|
|
39
|
+
await fs.writeFile(path.join(dir, PRO_FILE), apiKey.trim() + "\n", "utf-8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function removeProKey(dataDir?: string): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
await fs.unlink(path.join(getDataDir(dataDir), PRO_FILE));
|
|
45
|
+
} catch { /* already gone */ }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getProStatus(dataDir?: string): Promise<ProStatus> {
|
|
49
|
+
const apiKey = await readProKey(dataDir);
|
|
50
|
+
return { isPro: apiKey !== null, apiKey, verified: null, expiresAt: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Guard a Pro-only feature. Returns null if Pro, or an error object if Free.
|
|
55
|
+
*
|
|
56
|
+
* const gate = await requirePro("对标账号监控", dataDir);
|
|
57
|
+
* if (gate) return gate;
|
|
58
|
+
*/
|
|
59
|
+
export async function requirePro(
|
|
60
|
+
featureName: string,
|
|
61
|
+
dataDir?: string,
|
|
62
|
+
): Promise<{ ok: false; error: string; upgradeHint: string; freeAlternative?: string } | null> {
|
|
63
|
+
const status = await getProStatus(dataDir);
|
|
64
|
+
if (status.isPro) return null;
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: `「${featureName}」是 Pro 版功能。`,
|
|
68
|
+
upgradeHint: "运行 autocrew upgrade 了解 Pro 版详情。",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function proGateResponse(
|
|
73
|
+
featureName: string,
|
|
74
|
+
freeAlternative: string,
|
|
75
|
+
): { ok: false; error: string; upgradeHint: string; freeAlternative: string } {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: `「${featureName}」是 Pro 版功能。`,
|
|
79
|
+
upgradeHint: "运行 autocrew upgrade 了解 Pro 版详情。",
|
|
80
|
+
freeAlternative,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const PRO_FEATURES = [
|
|
85
|
+
"competitor-monitor",
|
|
86
|
+
"extract-video-script",
|
|
87
|
+
"video-analysis",
|
|
88
|
+
"analytics-report",
|
|
89
|
+
"research-crawl",
|
|
90
|
+
"research-trending",
|
|
91
|
+
"cover-multi-ratio",
|
|
92
|
+
"tts-synthesize",
|
|
93
|
+
"tts-clone",
|
|
94
|
+
"digital-human",
|
|
95
|
+
"publish-wechat-mp",
|
|
96
|
+
"publish-wechat-video",
|
|
97
|
+
"publish-bilibili",
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
export type ProFeature = (typeof PRO_FEATURES)[number];
|
|
101
|
+
|
|
102
|
+
export function isProFeature(featureId: string): boolean {
|
|
103
|
+
return (PRO_FEATURES as readonly string[]).includes(featureId);
|
|
104
|
+
}
|