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.
Files changed (165) hide show
  1. package/HAMLETDEER.md +562 -0
  2. package/LICENSE +21 -0
  3. package/README.md +190 -0
  4. package/README_CN.md +190 -0
  5. package/adapters/openclaw/index.ts +68 -0
  6. package/bin/autocrew.mjs +23 -0
  7. package/bin/autocrew.ts +13 -0
  8. package/openclaw.plugin.json +36 -0
  9. package/package.json +74 -0
  10. package/skills/_writing-style/SKILL.md +68 -0
  11. package/skills/audience-profiler/SKILL.md +241 -0
  12. package/skills/content-attribution/SKILL.md +128 -0
  13. package/skills/content-review/SKILL.md +257 -0
  14. package/skills/cover-generator/SKILL.md +93 -0
  15. package/skills/humanizer-zh/SKILL.md +75 -0
  16. package/skills/intel-digest/SKILL.md +57 -0
  17. package/skills/intel-pull/SKILL.md +74 -0
  18. package/skills/manage-pipeline/SKILL.md +63 -0
  19. package/skills/memory-distill/SKILL.md +89 -0
  20. package/skills/onboarding/SKILL.md +117 -0
  21. package/skills/pipeline-status/SKILL.md +51 -0
  22. package/skills/platform-rewrite/SKILL.md +125 -0
  23. package/skills/pre-publish/SKILL.md +142 -0
  24. package/skills/publish-content/SKILL.md +500 -0
  25. package/skills/remix-content/SKILL.md +77 -0
  26. package/skills/research/SKILL.md +127 -0
  27. package/skills/setup/SKILL.md +353 -0
  28. package/skills/spawn-batch-writer/SKILL.md +66 -0
  29. package/skills/spawn-planner/SKILL.md +72 -0
  30. package/skills/spawn-writer/SKILL.md +60 -0
  31. package/skills/teardown/SKILL.md +144 -0
  32. package/skills/title-craft/SKILL.md +234 -0
  33. package/skills/topic-ideas/SKILL.md +105 -0
  34. package/skills/video-timeline/SKILL.md +117 -0
  35. package/skills/write-script/SKILL.md +232 -0
  36. package/skills/xhs-cover-review/SKILL.md +48 -0
  37. package/src/adapters/browser/browser-cdp.ts +260 -0
  38. package/src/adapters/browser/browser-relay.ts +236 -0
  39. package/src/adapters/browser/gateway-client.ts +148 -0
  40. package/src/adapters/browser/types.ts +36 -0
  41. package/src/adapters/image/gemini.ts +219 -0
  42. package/src/adapters/research/tikhub.ts +19 -0
  43. package/src/cli/banner.ts +18 -0
  44. package/src/cli/bootstrap.ts +33 -0
  45. package/src/cli/commands/adapt.ts +28 -0
  46. package/src/cli/commands/advance.ts +28 -0
  47. package/src/cli/commands/assets.ts +24 -0
  48. package/src/cli/commands/audit.ts +18 -0
  49. package/src/cli/commands/contents.ts +18 -0
  50. package/src/cli/commands/cover.ts +58 -0
  51. package/src/cli/commands/events.ts +17 -0
  52. package/src/cli/commands/humanize.ts +27 -0
  53. package/src/cli/commands/index.ts +80 -0
  54. package/src/cli/commands/init.ts +28 -0
  55. package/src/cli/commands/intel.ts +55 -0
  56. package/src/cli/commands/learn.ts +34 -0
  57. package/src/cli/commands/memory.ts +18 -0
  58. package/src/cli/commands/migrate.ts +24 -0
  59. package/src/cli/commands/open.ts +21 -0
  60. package/src/cli/commands/pipelines.ts +18 -0
  61. package/src/cli/commands/pre-publish.ts +27 -0
  62. package/src/cli/commands/profile.ts +31 -0
  63. package/src/cli/commands/research.ts +36 -0
  64. package/src/cli/commands/restore.ts +28 -0
  65. package/src/cli/commands/review.ts +61 -0
  66. package/src/cli/commands/start.ts +28 -0
  67. package/src/cli/commands/status.ts +14 -0
  68. package/src/cli/commands/templates.ts +15 -0
  69. package/src/cli/commands/topics.ts +18 -0
  70. package/src/cli/commands/trash.ts +28 -0
  71. package/src/cli/commands/upgrade.ts +48 -0
  72. package/src/cli/commands/versions.ts +24 -0
  73. package/src/cli/index.ts +40 -0
  74. package/src/data/sensitive-words-builtin.json +114 -0
  75. package/src/data/source-presets.yaml +54 -0
  76. package/src/e2e.test.ts +596 -0
  77. package/src/modules/auth/cookie-manager.ts +113 -0
  78. package/src/modules/cards/template-engine.ts +74 -0
  79. package/src/modules/cards/templates/comparison-table.ts +71 -0
  80. package/src/modules/cards/templates/data-chart.ts +76 -0
  81. package/src/modules/cards/templates/flow-chart.ts +49 -0
  82. package/src/modules/cards/templates/key-points.ts +59 -0
  83. package/src/modules/cover/prompt-builder.test.ts +157 -0
  84. package/src/modules/cover/prompt-builder.ts +212 -0
  85. package/src/modules/cover/ratio-adapter.test.ts +122 -0
  86. package/src/modules/cover/ratio-adapter.ts +104 -0
  87. package/src/modules/filter/sensitive-words.test.ts +72 -0
  88. package/src/modules/filter/sensitive-words.ts +212 -0
  89. package/src/modules/humanizer/zh.test.ts +75 -0
  90. package/src/modules/humanizer/zh.ts +175 -0
  91. package/src/modules/intel/collector.ts +19 -0
  92. package/src/modules/intel/collectors/competitor.test.ts +71 -0
  93. package/src/modules/intel/collectors/competitor.ts +65 -0
  94. package/src/modules/intel/collectors/rss.test.ts +56 -0
  95. package/src/modules/intel/collectors/rss.ts +70 -0
  96. package/src/modules/intel/collectors/trends.test.ts +80 -0
  97. package/src/modules/intel/collectors/trends.ts +107 -0
  98. package/src/modules/intel/collectors/web-search.test.ts +85 -0
  99. package/src/modules/intel/collectors/web-search.ts +81 -0
  100. package/src/modules/intel/integration.test.ts +203 -0
  101. package/src/modules/intel/intel-engine.test.ts +103 -0
  102. package/src/modules/intel/intel-engine.ts +96 -0
  103. package/src/modules/intel/source-config.test.ts +113 -0
  104. package/src/modules/intel/source-config.ts +131 -0
  105. package/src/modules/learnings/diff-tracker.test.ts +144 -0
  106. package/src/modules/learnings/diff-tracker.ts +189 -0
  107. package/src/modules/learnings/rule-distiller.ts +141 -0
  108. package/src/modules/memory/distill.ts +208 -0
  109. package/src/modules/migrate/legacy-migrate.test.ts +169 -0
  110. package/src/modules/migrate/legacy-migrate.ts +229 -0
  111. package/src/modules/pro/api-client.ts +192 -0
  112. package/src/modules/pro/gate.test.ts +110 -0
  113. package/src/modules/pro/gate.ts +104 -0
  114. package/src/modules/profile/creator-profile.test.ts +178 -0
  115. package/src/modules/profile/creator-profile.ts +248 -0
  116. package/src/modules/publish/douyin-api.ts +34 -0
  117. package/src/modules/publish/wechat-mp.ts +320 -0
  118. package/src/modules/publish/xiaohongshu-api.ts +127 -0
  119. package/src/modules/research/free-engine.ts +360 -0
  120. package/src/modules/timeline/markup-generator.ts +63 -0
  121. package/src/modules/timeline/parser.ts +275 -0
  122. package/src/modules/workflow/templates.ts +124 -0
  123. package/src/modules/writing/platform-rewrite.ts +190 -0
  124. package/src/modules/writing/title-hashtag.ts +385 -0
  125. package/src/runtime/context.test.ts +97 -0
  126. package/src/runtime/context.ts +129 -0
  127. package/src/runtime/events.test.ts +83 -0
  128. package/src/runtime/events.ts +104 -0
  129. package/src/runtime/hooks.ts +174 -0
  130. package/src/runtime/tool-runner.test.ts +204 -0
  131. package/src/runtime/tool-runner.ts +282 -0
  132. package/src/runtime/workflow-engine.test.ts +455 -0
  133. package/src/runtime/workflow-engine.ts +391 -0
  134. package/src/server/index.ts +409 -0
  135. package/src/server/start.ts +39 -0
  136. package/src/storage/local-store.test.ts +304 -0
  137. package/src/storage/local-store.ts +704 -0
  138. package/src/storage/pipeline-store.test.ts +363 -0
  139. package/src/storage/pipeline-store.ts +698 -0
  140. package/src/tools/asset.ts +96 -0
  141. package/src/tools/content-save.ts +276 -0
  142. package/src/tools/cover-review.ts +221 -0
  143. package/src/tools/humanize.ts +54 -0
  144. package/src/tools/init.ts +133 -0
  145. package/src/tools/intel.ts +92 -0
  146. package/src/tools/memory.ts +76 -0
  147. package/src/tools/pipeline-ops.ts +109 -0
  148. package/src/tools/pipeline.ts +168 -0
  149. package/src/tools/pre-publish.ts +232 -0
  150. package/src/tools/publish.ts +183 -0
  151. package/src/tools/registry.ts +198 -0
  152. package/src/tools/research.ts +304 -0
  153. package/src/tools/review.ts +305 -0
  154. package/src/tools/rewrite.ts +165 -0
  155. package/src/tools/status.ts +30 -0
  156. package/src/tools/timeline.ts +234 -0
  157. package/src/tools/topic-create.ts +50 -0
  158. package/src/types/providers.ts +69 -0
  159. package/src/types/timeline.test.ts +147 -0
  160. package/src/types/timeline.ts +83 -0
  161. package/src/utils/retry.test.ts +97 -0
  162. package/src/utils/retry.ts +85 -0
  163. package/templates/AGENTS.md +99 -0
  164. package/templates/SOUL.md +31 -0
  165. package/templates/TOOLS.md +76 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Predefined workflow templates for AutoCrew.
3
+ *
4
+ * Each template defines a multi-step content pipeline that can be
5
+ * instantiated via the workflow engine.
6
+ */
7
+
8
+ import type { WorkflowDefinition } from "../../runtime/workflow-engine.js";
9
+
10
+ const TEMPLATES: WorkflowDefinition[] = [
11
+ {
12
+ id: "xiaohongshu_full",
13
+ name: "小红书一键发布",
14
+ description: "从选题调研到发布的完整小红书内容流水线。包含 AI 写作、去 AI 痕迹、敏感词审查、封面设计等全流程。",
15
+ steps: [
16
+ {
17
+ id: "research",
18
+ name: "选题调研",
19
+ tool: "autocrew_research",
20
+ params: { action: "discover", platform: "xhs" },
21
+ },
22
+ {
23
+ id: "topic",
24
+ name: "创建选题",
25
+ tool: "autocrew_topic",
26
+ params: { action: "create", source: "${research.bestResult}" },
27
+ },
28
+ {
29
+ id: "content",
30
+ name: "AI 写稿",
31
+ tool: "autocrew_content",
32
+ params: { action: "save", topic_id: "${topic.id}" },
33
+ requiresApproval: true,
34
+ },
35
+ {
36
+ id: "humanize",
37
+ name: "去 AI 痕迹",
38
+ tool: "autocrew_humanize",
39
+ params: { action: "rewrite", content_id: "${content.id}" },
40
+ },
41
+ {
42
+ id: "review",
43
+ name: "内容审查",
44
+ tool: "autocrew_review",
45
+ params: { action: "check", content_id: "${content.id}" },
46
+ },
47
+ {
48
+ id: "cover",
49
+ name: "封面设计",
50
+ tool: "autocrew_cover_review",
51
+ params: { action: "generate", content_id: "${content.id}" },
52
+ requiresApproval: true,
53
+ },
54
+ {
55
+ id: "pre_publish",
56
+ name: "发布前检查",
57
+ tool: "autocrew_pre_publish",
58
+ params: { action: "checklist", content_id: "${content.id}" },
59
+ },
60
+ {
61
+ id: "publish",
62
+ name: "发布",
63
+ tool: "autocrew_publish",
64
+ params: { action: "publish", content_id: "${content.id}", platform: "xhs" },
65
+ requiresApproval: true,
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ id: "quick_publish",
71
+ name: "快速发布",
72
+ description: "已有内容的快速发布流程。去 AI 痕迹 → 审查 → 平台适配 → 封面 → 发布。",
73
+ steps: [
74
+ {
75
+ id: "humanize",
76
+ name: "去 AI 痕迹",
77
+ tool: "autocrew_humanize",
78
+ params: { action: "rewrite", content_id: "${content_id}" },
79
+ },
80
+ {
81
+ id: "review",
82
+ name: "内容审查",
83
+ tool: "autocrew_review",
84
+ params: { action: "check", content_id: "${content_id}" },
85
+ },
86
+ {
87
+ id: "rewrite",
88
+ name: "平台适配",
89
+ tool: "autocrew_rewrite",
90
+ params: { action: "adapt", content_id: "${content_id}" },
91
+ },
92
+ {
93
+ id: "cover",
94
+ name: "封面设计",
95
+ tool: "autocrew_cover_review",
96
+ params: { action: "generate", content_id: "${content_id}" },
97
+ requiresApproval: true,
98
+ },
99
+ {
100
+ id: "pre_publish",
101
+ name: "发布前检查",
102
+ tool: "autocrew_pre_publish",
103
+ params: { action: "checklist", content_id: "${content_id}" },
104
+ },
105
+ {
106
+ id: "publish",
107
+ name: "发布",
108
+ tool: "autocrew_publish",
109
+ params: { action: "publish", content_id: "${content_id}" },
110
+ requiresApproval: true,
111
+ },
112
+ ],
113
+ },
114
+ ];
115
+
116
+ const templateMap = new Map(TEMPLATES.map((t) => [t.id, t]));
117
+
118
+ export function getTemplates(): WorkflowDefinition[] {
119
+ return TEMPLATES;
120
+ }
121
+
122
+ export function getTemplate(id: string): WorkflowDefinition | undefined {
123
+ return templateMap.get(id);
124
+ }
@@ -0,0 +1,190 @@
1
+ export type SupportedPlatform =
2
+ | "xiaohongshu"
3
+ | "douyin"
4
+ | "wechat_mp"
5
+ | "wechat_video"
6
+ | "bilibili";
7
+
8
+ export interface AdaptPlatformOptions {
9
+ title: string;
10
+ body: string;
11
+ targetPlatform: SupportedPlatform;
12
+ tags?: string[];
13
+ }
14
+
15
+ export interface AdaptPlatformResult {
16
+ ok: boolean;
17
+ platform: SupportedPlatform;
18
+ title: string;
19
+ body: string;
20
+ notes: string[];
21
+ }
22
+
23
+ function cleanTitle(title: string): string {
24
+ return title.replace(/\s+/g, " ").trim();
25
+ }
26
+
27
+ function splitParagraphs(body: string): string[] {
28
+ return body
29
+ .split(/\n{2,}/)
30
+ .map((item) => item.trim())
31
+ .filter(Boolean);
32
+ }
33
+
34
+ function ensureSentenceEnding(text: string): string {
35
+ if (!text) return text;
36
+ if (/[。!?.!?]$/.test(text)) return text;
37
+ return `${text}。`;
38
+ }
39
+
40
+ function trimTitle(title: string, maxLength: number): string {
41
+ if (title.length <= maxLength) return title;
42
+ return `${title.slice(0, maxLength - 1)}…`;
43
+ }
44
+
45
+ function hashtags(tags: string[] = []): string {
46
+ if (tags.length === 0) return "";
47
+ return tags.map((tag) => (tag.startsWith("#") ? tag : `#${tag}`)).join(" ");
48
+ }
49
+
50
+ function adaptForXiaohongshu(title: string, body: string, tags: string[]): AdaptPlatformResult {
51
+ const paragraphs = splitParagraphs(body).map((paragraph) => {
52
+ const parts = paragraph
53
+ .replace(/([。!?])/g, "$1\n")
54
+ .split("\n")
55
+ .map((line) => line.trim())
56
+ .filter(Boolean);
57
+ return parts.slice(0, 2).join("\n");
58
+ });
59
+
60
+ const bodyWithTags = [...paragraphs];
61
+ const tagLine = hashtags(tags);
62
+ if (tagLine) bodyWithTags.push(tagLine);
63
+
64
+ return {
65
+ ok: true,
66
+ platform: "xiaohongshu",
67
+ title: trimTitle(cleanTitle(title), 20),
68
+ body: bodyWithTags.join("\n\n"),
69
+ notes: [
70
+ "按小红书习惯压成短段落和短句。",
71
+ "保留标签行,适合直接做图文文案基底。",
72
+ ],
73
+ };
74
+ }
75
+
76
+ function adaptForDouyin(title: string, body: string, tags: string[]): AdaptPlatformResult {
77
+ const paragraphs = splitParagraphs(body);
78
+ const hook = ensureSentenceEnding(paragraphs[0] || title);
79
+ const voiceover = paragraphs.slice(1).join("\n\n") || paragraphs[0] || body;
80
+ const tagLine = hashtags(tags.slice(0, 5));
81
+
82
+ return {
83
+ ok: true,
84
+ platform: "douyin",
85
+ title: trimTitle(cleanTitle(title), 30),
86
+ body: [
87
+ "[3秒开头]",
88
+ hook,
89
+ "",
90
+ "[口播]",
91
+ voiceover,
92
+ "",
93
+ "[字幕重点]",
94
+ trimTitle(cleanTitle(title), 18),
95
+ "",
96
+ "[互动引导]",
97
+ "你最卡的是哪一步?评论区告诉我。",
98
+ tagLine ? `\n${tagLine}` : "",
99
+ ]
100
+ .join("\n")
101
+ .trim(),
102
+ notes: [
103
+ "改成短视频脚本结构,先给 3 秒钩子。",
104
+ "默认附带一条互动引导,方便评论区承接。",
105
+ ],
106
+ };
107
+ }
108
+
109
+ function adaptForWechatMp(title: string, body: string): AdaptPlatformResult {
110
+ const paragraphs = splitParagraphs(body);
111
+ const intro = paragraphs[0] || body;
112
+ const middle = paragraphs.slice(1);
113
+
114
+ const sections = ["先说结论", "为什么这件事会卡住", "真正值得做的动作"];
115
+ const blocks: string[] = [trimTitle(cleanTitle(title), 64), "", ensureSentenceEnding(intro), ""];
116
+
117
+ middle.forEach((paragraph, index) => {
118
+ const section = sections[index % sections.length];
119
+ blocks.push(section, "", ensureSentenceEnding(paragraph), "");
120
+ });
121
+
122
+ return {
123
+ ok: true,
124
+ platform: "wechat_mp",
125
+ title: trimTitle(cleanTitle(title), 64),
126
+ body: blocks.join("\n").trim(),
127
+ notes: [
128
+ "改成更适合公众号长文继续扩写的结构。",
129
+ "自动补了分节标题,方便后续加案例和配图。",
130
+ ],
131
+ };
132
+ }
133
+
134
+ function adaptForWechatVideo(title: string, body: string, tags: string[]): AdaptPlatformResult {
135
+ const paragraphs = splitParagraphs(body);
136
+ const description = [trimTitle(cleanTitle(title), 40), ...paragraphs.slice(0, 2)].join("\n");
137
+ const tagLine = hashtags(tags.slice(0, 5));
138
+ return {
139
+ ok: true,
140
+ platform: "wechat_video",
141
+ title: trimTitle(cleanTitle(title), 40),
142
+ body: `${description}${tagLine ? `\n\n${tagLine}` : ""}`.trim(),
143
+ notes: [
144
+ "压成视频号创作描述格式。",
145
+ "保留简洁摘要,避免正文过长。",
146
+ ],
147
+ };
148
+ }
149
+
150
+ function adaptForBilibili(title: string, body: string, tags: string[]): AdaptPlatformResult {
151
+ const paragraphs = splitParagraphs(body);
152
+ const description = paragraphs.join("\n\n");
153
+ const tagLine = hashtags(tags.slice(0, 10));
154
+ return {
155
+ ok: true,
156
+ platform: "bilibili",
157
+ title: trimTitle(cleanTitle(title), 80),
158
+ body: `${description}${tagLine ? `\n\n${tagLine}` : ""}`.trim(),
159
+ notes: [
160
+ "保留信息密度,适合 B 站视频简介或动态文案。",
161
+ ],
162
+ };
163
+ }
164
+
165
+ export function adaptPlatformDraft(options: AdaptPlatformOptions): AdaptPlatformResult {
166
+ const title = options.title || "未命名内容";
167
+ const body = options.body || "";
168
+ const tags = options.tags || [];
169
+
170
+ switch (options.targetPlatform) {
171
+ case "xiaohongshu":
172
+ return adaptForXiaohongshu(title, body, tags);
173
+ case "douyin":
174
+ return adaptForDouyin(title, body, tags);
175
+ case "wechat_mp":
176
+ return adaptForWechatMp(title, body);
177
+ case "wechat_video":
178
+ return adaptForWechatVideo(title, body, tags);
179
+ case "bilibili":
180
+ return adaptForBilibili(title, body, tags);
181
+ default:
182
+ return {
183
+ ok: false,
184
+ platform: options.targetPlatform,
185
+ title,
186
+ body,
187
+ notes: ["Unsupported platform"],
188
+ };
189
+ }
190
+ }
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Multi-Platform Title & Hashtag Generator
3
+ *
4
+ * Generates platform-native title variants and hashtag suggestions
5
+ * based on each platform's conventions and character limits.
6
+ *
7
+ * PRD §14.2: "各版本的标题和 hashtag 按平台独立生成"
8
+ */
9
+
10
+ // --- Types ---
11
+
12
+ export interface PlatformRules {
13
+ name: string;
14
+ /** Display name in Chinese */
15
+ displayName: string;
16
+ /** Max title length (chars) */
17
+ maxTitleLength: number;
18
+ /** Recommended title length range */
19
+ titleLengthRange: [number, number];
20
+ /** Max hashtags */
21
+ maxHashtags: number;
22
+ /** Hashtag format */
23
+ hashtagPrefix: string;
24
+ /** Platform-specific title tips */
25
+ titleTips: string[];
26
+ /** Common high-performing hashtag patterns */
27
+ hotHashtagPatterns: string[];
28
+ }
29
+
30
+ export interface TitleVariant {
31
+ title: string;
32
+ style: "hook" | "list" | "question" | "story" | "direct";
33
+ /** Why this variant works for the platform */
34
+ note: string;
35
+ }
36
+
37
+ export interface HashtagSuggestion {
38
+ tag: string;
39
+ /** "topic" = content-related, "trending" = platform trend, "niche" = audience-specific */
40
+ type: "topic" | "trending" | "niche";
41
+ }
42
+
43
+ export interface PlatformTitleResult {
44
+ platform: string;
45
+ titles: TitleVariant[];
46
+ hashtags: HashtagSuggestion[];
47
+ tips: string[];
48
+ }
49
+
50
+ // --- Platform Rules ---
51
+
52
+ const PLATFORM_RULES: Record<string, PlatformRules> = {
53
+ xhs: {
54
+ name: "xhs",
55
+ displayName: "小红书",
56
+ maxTitleLength: 20,
57
+ titleLengthRange: [10, 18],
58
+ maxHashtags: 10,
59
+ hashtagPrefix: "#",
60
+ titleTips: [
61
+ "用 emoji 开头增加视觉吸引力",
62
+ "数字 + 结果型标题表现最好",
63
+ "避免标题党,小红书会限流",
64
+ "口语化、第一人称更亲切",
65
+ ],
66
+ hotHashtagPatterns: [
67
+ "干货分享", "经验分享", "避坑指南", "真实体验",
68
+ "好物推荐", "自我提升", "效率工具", "学习打卡",
69
+ ],
70
+ },
71
+ douyin: {
72
+ name: "douyin",
73
+ displayName: "抖音",
74
+ maxTitleLength: 300, // Full caption field; visible ~55 chars before fold
75
+ titleLengthRange: [15, 55],
76
+ maxHashtags: 5,
77
+ hashtagPrefix: "#",
78
+ titleTips: [
79
+ "标题和描述是同一字段,前 55 字可见,后面需展开",
80
+ "前 3 秒决定完播率,标题要制造悬念",
81
+ "用「没想到」「居然」等反转词",
82
+ "数字型标题点击率高",
83
+ "可以用 | 分隔主副标题",
84
+ ],
85
+ hotHashtagPatterns: [
86
+ "涨知识", "干货", "必看", "真相",
87
+ "生活小妙招", "职场", "创业", "副业",
88
+ ],
89
+ },
90
+ wechat_mp: {
91
+ name: "wechat_mp",
92
+ displayName: "微信公众号",
93
+ maxTitleLength: 64,
94
+ titleLengthRange: [20, 30],
95
+ maxHashtags: 3,
96
+ hashtagPrefix: "#",
97
+ titleTips: [
98
+ "公众号标题可以更长,信息量更大",
99
+ "用「:」分隔前后半句制造节奏",
100
+ "数据型标题增加可信度",
101
+ "避免全大写或过多感叹号",
102
+ ],
103
+ hotHashtagPatterns: [
104
+ "深度", "观点", "行业分析", "趋势",
105
+ ],
106
+ },
107
+ wechat_video: {
108
+ name: "wechat_video",
109
+ displayName: "视频号",
110
+ maxTitleLength: 30,
111
+ titleLengthRange: [10, 25],
112
+ maxHashtags: 5,
113
+ hashtagPrefix: "#",
114
+ titleTips: [
115
+ "视频号用户偏成熟,标题要稳重",
116
+ "避免过度网感,保持专业感",
117
+ "可以用提问式引发评论",
118
+ ],
119
+ hotHashtagPatterns: [
120
+ "知识分享", "行业洞察", "职场经验", "创业心得",
121
+ ],
122
+ },
123
+ bilibili: {
124
+ name: "bilibili",
125
+ displayName: "B站",
126
+ maxTitleLength: 80,
127
+ titleLengthRange: [12, 24],
128
+ maxHashtags: 5,
129
+ hashtagPrefix: "#",
130
+ titleTips: [
131
+ "系统限制 80 字,但移动端只显示 20-24 中文字",
132
+ "用【】标注视频类型(如【干货】【避坑】)",
133
+ "年轻化表达,可以用梗",
134
+ "副标题补充信息量",
135
+ ],
136
+ hotHashtagPatterns: [
137
+ "干货", "教程", "测评", "避坑",
138
+ "知识区", "科技", "生活", "学习",
139
+ ],
140
+ },
141
+ toutiao: {
142
+ name: "toutiao",
143
+ displayName: "今日头条",
144
+ maxTitleLength: 30,
145
+ titleLengthRange: [15, 30],
146
+ maxHashtags: 5,
147
+ hashtagPrefix: "#",
148
+ titleTips: [
149
+ "标题最少 5 字,最多 30 字",
150
+ "两段式标题(主题+价值点)表现最好",
151
+ "视频标题建议 26-30 字",
152
+ "直接点明价值,不要含糊",
153
+ ],
154
+ hotHashtagPatterns: [
155
+ "干货", "涨知识", "实用", "经验",
156
+ "职场", "创业", "生活", "科技",
157
+ ],
158
+ },
159
+ youtube: {
160
+ name: "youtube",
161
+ displayName: "YouTube",
162
+ maxTitleLength: 100,
163
+ titleLengthRange: [30, 70],
164
+ maxHashtags: 15,
165
+ hashtagPrefix: "#",
166
+ titleTips: [
167
+ "搜索结果只显示约 60 字,YouTube 界面约 70 字",
168
+ "前置关键词有利于 SEO",
169
+ "中英双语受众可混合使用",
170
+ "避免全大写",
171
+ ],
172
+ hotHashtagPatterns: [
173
+ "tutorial", "howto", "review", "tips",
174
+ ],
175
+ },
176
+ twitter: {
177
+ name: "twitter",
178
+ displayName: "Twitter/X",
179
+ maxTitleLength: 280, // Whole tweet, no separate title
180
+ titleLengthRange: [60, 140],
181
+ maxHashtags: 3,
182
+ hashtagPrefix: "#",
183
+ titleTips: [
184
+ "没有独立标题,第一行就是标题",
185
+ "简短有力,留空间给讨论",
186
+ "Emoji 算 2 个字符",
187
+ "URL 固定算 23 字符",
188
+ ],
189
+ hotHashtagPatterns: [],
190
+ },
191
+ instagram: {
192
+ name: "instagram",
193
+ displayName: "Instagram",
194
+ maxTitleLength: 2200, // Whole caption
195
+ titleLengthRange: [60, 125],
196
+ maxHashtags: 30,
197
+ hashtagPrefix: "#",
198
+ titleTips: [
199
+ "第一行就是标题,125 字后被折叠",
200
+ "Reels 只显示前 55-60 字",
201
+ "Emoji 文化浓厚",
202
+ "Hashtag 放评论区也可以",
203
+ ],
204
+ hotHashtagPatterns: [],
205
+ },
206
+ };
207
+
208
+ // Alias
209
+ PLATFORM_RULES["xiaohongshu"] = PLATFORM_RULES["xhs"];
210
+
211
+ /**
212
+ * Get platform rules. Returns null if platform is unknown.
213
+ */
214
+ export function getPlatformRules(platform: string): PlatformRules | null {
215
+ return PLATFORM_RULES[platform] || null;
216
+ }
217
+
218
+ /**
219
+ * Generate title variants for a specific platform.
220
+ *
221
+ * Takes a base title/topic and produces 3-5 platform-optimized variants.
222
+ * This is a rule-based generator — the AI agent can use these as starting
223
+ * points and refine further.
224
+ */
225
+ export function generateTitleVariants(
226
+ baseTopic: string,
227
+ platform: string,
228
+ opts?: { keyword?: string },
229
+ ): TitleVariant[] {
230
+ const rules = PLATFORM_RULES[platform];
231
+ if (!rules) return [{ title: baseTopic, style: "direct", note: "未知平台,返回原标题" }];
232
+
233
+ const keyword = opts?.keyword || "";
234
+ const variants: TitleVariant[] = [];
235
+ const [minLen, maxLen] = rules.titleLengthRange;
236
+
237
+ // 1. Hook style — emotional trigger
238
+ const hookTitle = buildHookTitle(baseTopic, maxLen);
239
+ if (hookTitle) {
240
+ variants.push({ title: hookTitle, style: "hook", note: "情绪触发型,适合引发好奇" });
241
+ }
242
+
243
+ // 2. List style — number + result
244
+ const listTitle = buildListTitle(baseTopic, keyword, maxLen);
245
+ variants.push({ title: listTitle, style: "list", note: "数字型,点击率通常最高" });
246
+
247
+ // 3. Question style
248
+ const questionTitle = buildQuestionTitle(baseTopic, maxLen);
249
+ variants.push({ title: questionTitle, style: "question", note: "提问型,引发思考和评论" });
250
+
251
+ // 4. Story style (first person)
252
+ const storyTitle = buildStoryTitle(baseTopic, maxLen);
253
+ variants.push({ title: storyTitle, style: "story", note: "故事型,第一人称更有代入感" });
254
+
255
+ // 5. Direct style — clean and informative
256
+ let directTitle = baseTopic;
257
+ if (directTitle.length > maxLen) directTitle = directTitle.slice(0, maxLen);
258
+ variants.push({ title: directTitle, style: "direct", note: "直述型,清晰传达主题" });
259
+
260
+ // Enforce length limits
261
+ return variants
262
+ .map((v) => ({
263
+ ...v,
264
+ title: v.title.length > rules.maxTitleLength ? v.title.slice(0, rules.maxTitleLength) : v.title,
265
+ }))
266
+ .filter((v) => v.title.length >= Math.min(minLen, 5));
267
+ }
268
+
269
+ /**
270
+ * Generate hashtag suggestions for a platform.
271
+ */
272
+ export function generateHashtags(
273
+ topic: string,
274
+ platform: string,
275
+ tags: string[] = [],
276
+ ): HashtagSuggestion[] {
277
+ const rules = PLATFORM_RULES[platform];
278
+ if (!rules) return tags.map((t) => ({ tag: `#${t}`, type: "topic" as const }));
279
+
280
+ const suggestions: HashtagSuggestion[] = [];
281
+ const prefix = rules.hashtagPrefix;
282
+
283
+ // 1. Topic-based hashtags from provided tags
284
+ for (const tag of tags.slice(0, 3)) {
285
+ suggestions.push({ tag: `${prefix}${tag}`, type: "topic" });
286
+ }
287
+
288
+ // 2. Topic-derived hashtag
289
+ if (topic.length <= 10) {
290
+ suggestions.push({ tag: `${prefix}${topic}`, type: "topic" });
291
+ }
292
+
293
+ // 3. Platform trending patterns
294
+ const patterns = rules.hotHashtagPatterns;
295
+ // Pick 2-3 relevant patterns
296
+ const relevant = patterns.filter((p) =>
297
+ topic.includes(p.slice(0, 2)) || tags.some((t) => t.includes(p.slice(0, 2))),
298
+ );
299
+ const selected = relevant.length > 0 ? relevant.slice(0, 2) : patterns.slice(0, 2);
300
+ for (const p of selected) {
301
+ suggestions.push({ tag: `${prefix}${p}`, type: "trending" });
302
+ }
303
+
304
+ // 4. Niche hashtag (combine keyword + platform pattern)
305
+ if (tags[0] && patterns[0]) {
306
+ suggestions.push({ tag: `${prefix}${tags[0]}${patterns[0]}`, type: "niche" });
307
+ }
308
+
309
+ // Deduplicate and limit
310
+ const seen = new Set<string>();
311
+ return suggestions
312
+ .filter((s) => {
313
+ if (seen.has(s.tag)) return false;
314
+ seen.add(s.tag);
315
+ return true;
316
+ })
317
+ .slice(0, rules.maxHashtags);
318
+ }
319
+
320
+ /**
321
+ * Generate titles + hashtags for a specific platform in one call.
322
+ */
323
+ export function generateForPlatform(
324
+ baseTopic: string,
325
+ platform: string,
326
+ opts?: { keyword?: string; tags?: string[] },
327
+ ): PlatformTitleResult {
328
+ const rules = PLATFORM_RULES[platform];
329
+ const titles = generateTitleVariants(baseTopic, platform, opts);
330
+ const hashtags = generateHashtags(baseTopic, platform, opts?.tags);
331
+ const tips = rules?.titleTips || [];
332
+
333
+ return { platform, titles, hashtags, tips };
334
+ }
335
+
336
+ /**
337
+ * Generate titles + hashtags for multiple platforms at once.
338
+ */
339
+ export function generateForAllPlatforms(
340
+ baseTopic: string,
341
+ platforms: string[],
342
+ opts?: { keyword?: string; tags?: string[] },
343
+ ): PlatformTitleResult[] {
344
+ return platforms.map((p) => generateForPlatform(baseTopic, p, opts));
345
+ }
346
+
347
+ // --- Title Builders ---
348
+
349
+ function buildHookTitle(topic: string, maxLen: number): string | null {
350
+ const hooks = [
351
+ `别再${topic.slice(0, 4)}了,试试这个`,
352
+ `${topic.slice(0, 6)}的真相,没人告诉你`,
353
+ `后悔没早知道的${topic.slice(0, 6)}`,
354
+ ];
355
+ const valid = hooks.filter((h) => h.length <= maxLen);
356
+ return valid[0] || null;
357
+ }
358
+
359
+ function buildListTitle(topic: string, keyword: string, maxLen: number): string {
360
+ const core = keyword || topic.slice(0, 6);
361
+ const candidates = [
362
+ `${core}必知的5个要点`,
363
+ `3个${core}技巧,亲测有效`,
364
+ `${core}避坑指南:7条经验`,
365
+ ];
366
+ return candidates.find((c) => c.length <= maxLen) || candidates[0].slice(0, maxLen);
367
+ }
368
+
369
+ function buildQuestionTitle(topic: string, maxLen: number): string {
370
+ const candidates = [
371
+ `${topic.slice(0, 8)},你真的会吗?`,
372
+ `为什么${topic.slice(0, 6)}总是做不好?`,
373
+ `${topic.slice(0, 8)}到底怎么选?`,
374
+ ];
375
+ return candidates.find((c) => c.length <= maxLen) || candidates[0].slice(0, maxLen);
376
+ }
377
+
378
+ function buildStoryTitle(topic: string, maxLen: number): string {
379
+ const candidates = [
380
+ `我用${topic.slice(0, 6)}的真实经历`,
381
+ `做了3个月${topic.slice(0, 4)},说说感受`,
382
+ `从零开始${topic.slice(0, 6)},踩过的坑`,
383
+ ];
384
+ return candidates.find((c) => c.length <= maxLen) || candidates[0].slice(0, maxLen);
385
+ }