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,236 @@
1
+ /**
2
+ * Browser Relay Adapter — uses OpenClaw Gateway + Chrome Relay
3
+ *
4
+ * Controls the user's real browser via OpenClaw Gateway HTTP API.
5
+ * Falls back to the legacy CDP proxy adapter if Gateway is unavailable.
6
+ */
7
+ import { GatewayClient } from "./gateway-client.js";
8
+ import { browserCdpAdapter } from "./browser-cdp.js";
9
+ import type {
10
+ BrowserAdapter,
11
+ BrowserPlatform,
12
+ BrowserResearchQuery,
13
+ BrowserSessionStatus,
14
+ ResearchItem,
15
+ } from "./types.js";
16
+
17
+ function buildPlatformUrl(platform: BrowserPlatform): string {
18
+ const urls: Record<BrowserPlatform, string> = {
19
+ xiaohongshu: "https://www.xiaohongshu.com/",
20
+ douyin: "https://www.douyin.com/",
21
+ wechat_mp: "https://mp.weixin.qq.com/",
22
+ wechat_video: "https://channels.weixin.qq.com/platform/",
23
+ bilibili: "https://www.bilibili.com/",
24
+ };
25
+ return urls[platform] || urls.xiaohongshu;
26
+ }
27
+
28
+ function buildSearchUrl(platform: BrowserPlatform, keyword: string): string {
29
+ const encoded = encodeURIComponent(keyword);
30
+ const urls: Record<BrowserPlatform, string> = {
31
+ xiaohongshu: `https://www.xiaohongshu.com/search_result?keyword=${encoded}`,
32
+ douyin: `https://www.douyin.com/search/${encoded}?type=video`,
33
+ bilibili: `https://search.bilibili.com/all?keyword=${encoded}`,
34
+ wechat_mp: `https://weixin.sogou.com/weixin?type=2&query=${encoded}`,
35
+ wechat_video: `https://channels.weixin.qq.com/platform/`,
36
+ };
37
+ return urls[platform] || urls.xiaohongshu;
38
+ }
39
+
40
+ /** JS expression to check login status via page content */
41
+ function sessionCheckExpression(platform: BrowserPlatform): string {
42
+ return `(() => {
43
+ const href = location.href;
44
+ const text = (document.body?.innerText || "").slice(0, 2000);
45
+ const checks = {
46
+ xiaohongshu: /login|登录/.test(href) || /登录后查看更多|立即登录/.test(text),
47
+ douyin: /login|sso\\.douyin/.test(href) || /扫码登录|手机号登录/.test(text),
48
+ wechat_mp: /login/.test(href) || /扫码登录|微信公众平台/.test(text),
49
+ wechat_video: /login/.test(href) || /扫码登录|视频号助手/.test(text),
50
+ bilibili: /passport|login/.test(href) || /请先登录|登录后/.test(text),
51
+ };
52
+ const loggedIn = !checks[${JSON.stringify(platform)}];
53
+ return JSON.stringify({
54
+ href,
55
+ title: document.title,
56
+ loggedIn,
57
+ textHint: text.slice(0, 200),
58
+ });
59
+ })()`;
60
+ }
61
+
62
+ /** JS expression to extract research results from search page */
63
+ function researchExpression(platform: BrowserPlatform, limit: number): string {
64
+ return `(() => {
65
+ const limit = ${limit};
66
+ const abs = (href) => {
67
+ try { return new URL(href, location.href).href; } catch { return href || ""; }
68
+ };
69
+ const dedupe = new Set();
70
+ const push = (items, item) => {
71
+ if (!item || !item.title) return;
72
+ const key = item.title + "::" + (item.url || "");
73
+ if (dedupe.has(key)) return;
74
+ dedupe.add(key);
75
+ items.push(item);
76
+ };
77
+ const text = (el) => (el?.innerText || el?.textContent || "").replace(/\\s+/g, " ").trim();
78
+ const items = [];
79
+
80
+ if (${JSON.stringify(platform)} === "xiaohongshu") {
81
+ const cards = Array.from(document.querySelectorAll("section, .note-item, [data-index], a"));
82
+ for (const card of cards) {
83
+ const titleEl = card.querySelector?.("a[href*='/explore/'], a[href*='/discovery/item/'], a[href*='/note/'], .title span, .desc, .note-title") || card;
84
+ const linkEl = card.querySelector?.("a[href*='/explore/'], a[href*='/discovery/item/'], a[href*='/note/']") || card.closest?.("a");
85
+ const authorEl = card.querySelector?.(".author, .name");
86
+ const title = text(titleEl);
87
+ const url = linkEl?.href ? abs(linkEl.href) : "";
88
+ if (title.length >= 4 && url.includes("xiaohongshu.com")) {
89
+ push(items, { title, url, author: text(authorEl) });
90
+ }
91
+ if (items.length >= limit) break;
92
+ }
93
+ } else if (${JSON.stringify(platform)} === "douyin") {
94
+ const cards = Array.from(document.querySelectorAll("a[href*='/video/'], [data-e2e='search-result-container'] a, .ECMy_Zdt"));
95
+ for (const card of cards) {
96
+ const titleEl = card.querySelector?.("span, p, h3") || card;
97
+ const url = card.href ? abs(card.href) : abs(card.querySelector?.("a")?.href || "");
98
+ const title = text(titleEl);
99
+ if (title.length >= 4 && url.includes("douyin.com")) {
100
+ push(items, { title, url });
101
+ }
102
+ if (items.length >= limit) break;
103
+ }
104
+ } else if (${JSON.stringify(platform)} === "bilibili") {
105
+ const cards = Array.from(document.querySelectorAll("a[href*='/video/'], .bili-video-card a, .video-list-item a"));
106
+ for (const card of cards) {
107
+ const title = card.getAttribute?.("title") || text(card);
108
+ const url = card.href ? abs(card.href) : "";
109
+ if (title && url.includes("bilibili.com")) {
110
+ push(items, { title, url });
111
+ }
112
+ if (items.length >= limit) break;
113
+ }
114
+ } else {
115
+ const links = Array.from(document.querySelectorAll("a"));
116
+ for (const link of links) {
117
+ const title = text(link);
118
+ const url = link.href ? abs(link.href) : "";
119
+ if (title.length >= 6 && url) {
120
+ push(items, { title, url });
121
+ }
122
+ if (items.length >= limit) break;
123
+ }
124
+ }
125
+
126
+ return JSON.stringify(items.slice(0, limit));
127
+ })()`;
128
+ }
129
+
130
+ function sleep(ms: number): Promise<void> {
131
+ return new Promise((resolve) => setTimeout(resolve, ms));
132
+ }
133
+
134
+ // --- Relay Adapter ---
135
+
136
+ async function getSessionStatus(
137
+ platform: BrowserPlatform,
138
+ gatewayUrl?: string,
139
+ ): Promise<BrowserSessionStatus> {
140
+ const gw = new GatewayClient(gatewayUrl);
141
+ const available = await gw.isAvailable();
142
+
143
+ if (!available) {
144
+ // Fallback to legacy CDP adapter
145
+ if (browserCdpAdapter.getSessionStatus) {
146
+ return browserCdpAdapter.getSessionStatus(platform);
147
+ }
148
+ return { platform, loggedIn: false, note: "Gateway unavailable, no fallback" };
149
+ }
150
+
151
+ try {
152
+ // Navigate to platform homepage
153
+ const navResult = await gw.navigate(buildPlatformUrl(platform));
154
+ if (!navResult.ok) {
155
+ return { platform, loggedIn: false, note: navResult.error || "navigate failed" };
156
+ }
157
+
158
+ await sleep(2000);
159
+
160
+ // Evaluate login check expression
161
+ const evalResult = await gw.evaluate(sessionCheckExpression(platform));
162
+ if (!evalResult.ok || evalResult.value === undefined) {
163
+ return { platform, loggedIn: false, note: evalResult.error || "eval failed" };
164
+ }
165
+
166
+ const parsed = typeof evalResult.value === "string"
167
+ ? JSON.parse(evalResult.value)
168
+ : evalResult.value;
169
+
170
+ return {
171
+ platform,
172
+ loggedIn: Boolean(parsed.loggedIn),
173
+ note: parsed.href || parsed.title || "session checked via relay",
174
+ };
175
+ } catch (error: any) {
176
+ return { platform, loggedIn: false, note: error?.message || "relay session check failed" };
177
+ }
178
+ }
179
+
180
+ async function research(
181
+ query: BrowserResearchQuery,
182
+ gatewayUrl?: string,
183
+ ): Promise<ResearchItem[]> {
184
+ const gw = new GatewayClient(gatewayUrl);
185
+ const available = await gw.isAvailable();
186
+
187
+ if (!available) {
188
+ // Fallback to legacy CDP adapter
189
+ return browserCdpAdapter.research(query);
190
+ }
191
+
192
+ try {
193
+ // Navigate to search URL
194
+ const navResult = await gw.navigate(buildSearchUrl(query.platform, query.keyword));
195
+ if (!navResult.ok) return [];
196
+
197
+ await sleep(3000); // Wait for search results to load
198
+
199
+ // Extract results via JS evaluation
200
+ const evalResult = await gw.evaluate(researchExpression(query.platform, query.limit || 5));
201
+ if (!evalResult.ok || evalResult.value === undefined) return [];
202
+
203
+ const parsed = typeof evalResult.value === "string"
204
+ ? JSON.parse(evalResult.value as string)
205
+ : evalResult.value;
206
+
207
+ if (!Array.isArray(parsed)) return [];
208
+
209
+ return parsed.map((item: any) => ({
210
+ title: String(item.title || "").trim(),
211
+ summary: item.author
212
+ ? `参考账号:${String(item.author).trim()}`
213
+ : "来自 Chrome Relay 登录态搜索结果",
214
+ url: item.url ? String(item.url) : undefined,
215
+ author: item.author ? String(item.author) : undefined,
216
+ platform: query.platform,
217
+ source: "browser_relay" as const,
218
+ }));
219
+ } catch {
220
+ // Fallback to legacy CDP adapter
221
+ return browserCdpAdapter.research(query);
222
+ }
223
+ }
224
+
225
+ export function createBrowserRelayAdapter(gatewayUrl?: string): BrowserAdapter {
226
+ return {
227
+ id: "browser_relay",
228
+ description:
229
+ "Browser adapter using OpenClaw Gateway + Chrome Relay. Controls the user's real Chrome browser with existing login sessions.",
230
+ getSessionStatus: (platform) => getSessionStatus(platform, gatewayUrl),
231
+ research: (query) => research(query, gatewayUrl),
232
+ };
233
+ }
234
+
235
+ /** Default instance using env/default gateway URL */
236
+ export const browserRelayAdapter = createBrowserRelayAdapter();
@@ -0,0 +1,148 @@
1
+ /**
2
+ * OpenClaw Gateway HTTP Client
3
+ *
4
+ * Communicates with the OpenClaw Gateway to execute browser operations
5
+ * via Chrome Relay. Uses the user's real browser sessions (logged-in state).
6
+ *
7
+ * Gateway default: http://127.0.0.1:18789
8
+ */
9
+
10
+ const DEFAULT_GATEWAY_URL =
11
+ process.env.AUTOCREW_GATEWAY_URL || "http://127.0.0.1:18789";
12
+
13
+ const REQUEST_TIMEOUT_MS = 30_000;
14
+
15
+ export interface GatewayBrowserAction {
16
+ action: "navigate" | "click" | "type" | "screenshot" | "snapshot" | "evaluate";
17
+ url?: string;
18
+ /** Element reference ID (from snapshot) */
19
+ ref?: number;
20
+ /** Text to type */
21
+ text?: string;
22
+ /** JavaScript expression to evaluate */
23
+ expression?: string;
24
+ /** Coordinate-based click */
25
+ coordinate?: [number, number];
26
+ }
27
+
28
+ export interface GatewaySnapshot {
29
+ /** Page URL */
30
+ url: string;
31
+ /** Page title */
32
+ title: string;
33
+ /** Accessibility tree / element list with ref IDs */
34
+ elements: GatewaySnapshotElement[];
35
+ /** Raw page text (truncated) */
36
+ pageText?: string;
37
+ }
38
+
39
+ export interface GatewaySnapshotElement {
40
+ ref: number;
41
+ role: string;
42
+ name: string;
43
+ /** Additional attributes */
44
+ attrs?: Record<string, string>;
45
+ }
46
+
47
+ export interface GatewayBrowserResult {
48
+ ok: boolean;
49
+ /** Snapshot after the action */
50
+ snapshot?: GatewaySnapshot;
51
+ /** Screenshot base64 (if action=screenshot) */
52
+ screenshot?: string;
53
+ /** Evaluation result (if action=evaluate) */
54
+ value?: unknown;
55
+ error?: string;
56
+ }
57
+
58
+ export interface GatewaySession {
59
+ id: string;
60
+ /** Tab URL */
61
+ url: string;
62
+ title: string;
63
+ }
64
+
65
+ export class GatewayClient {
66
+ readonly baseUrl: string;
67
+
68
+ constructor(gatewayUrl?: string) {
69
+ this.baseUrl = (gatewayUrl || DEFAULT_GATEWAY_URL).replace(/\/+$/, "");
70
+ }
71
+
72
+ /** Check if the Gateway is running and reachable */
73
+ async isAvailable(): Promise<boolean> {
74
+ try {
75
+ const res = await fetch(`${this.baseUrl}/health`, {
76
+ signal: AbortSignal.timeout(3_000),
77
+ });
78
+ return res.ok;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ /** Execute a browser action through the Gateway */
85
+ async browser(action: GatewayBrowserAction): Promise<GatewayBrowserResult> {
86
+ const res = await fetch(`${this.baseUrl}/tools/browser`, {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json" },
89
+ body: JSON.stringify(action),
90
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
91
+ });
92
+
93
+ if (!res.ok) {
94
+ const text = await res.text().catch(() => "");
95
+ return { ok: false, error: `Gateway returned ${res.status}: ${text}` };
96
+ }
97
+
98
+ try {
99
+ const data = await res.json();
100
+ return { ok: true, ...data };
101
+ } catch {
102
+ return { ok: false, error: "Gateway returned non-JSON response" };
103
+ }
104
+ }
105
+
106
+ /** Navigate to a URL and return a snapshot */
107
+ async navigate(url: string): Promise<GatewayBrowserResult> {
108
+ return this.browser({ action: "navigate", url });
109
+ }
110
+
111
+ /** Take a page snapshot (accessibility tree with element refs) */
112
+ async snapshot(): Promise<GatewayBrowserResult> {
113
+ return this.browser({ action: "snapshot" });
114
+ }
115
+
116
+ /** Click an element by ref ID */
117
+ async click(ref: number): Promise<GatewayBrowserResult> {
118
+ return this.browser({ action: "click", ref });
119
+ }
120
+
121
+ /** Type text into the focused element */
122
+ async type(text: string): Promise<GatewayBrowserResult> {
123
+ return this.browser({ action: "type", text });
124
+ }
125
+
126
+ /** Take a screenshot (returns base64 PNG) */
127
+ async screenshot(): Promise<GatewayBrowserResult> {
128
+ return this.browser({ action: "screenshot" });
129
+ }
130
+
131
+ /** Evaluate a JavaScript expression in the page context */
132
+ async evaluate(expression: string): Promise<GatewayBrowserResult> {
133
+ return this.browser({ action: "evaluate", expression });
134
+ }
135
+
136
+ /** List active browser sessions */
137
+ async listSessions(): Promise<GatewaySession[]> {
138
+ try {
139
+ const res = await fetch(`${this.baseUrl}/sessions`, {
140
+ signal: AbortSignal.timeout(5_000),
141
+ });
142
+ if (!res.ok) return [];
143
+ return (await res.json()) as GatewaySession[];
144
+ } catch {
145
+ return [];
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,36 @@
1
+ export type BrowserPlatform =
2
+ | "xiaohongshu"
3
+ | "douyin"
4
+ | "wechat_mp"
5
+ | "wechat_video"
6
+ | "bilibili";
7
+
8
+ export interface BrowserSessionStatus {
9
+ platform: BrowserPlatform;
10
+ loggedIn: boolean;
11
+ profileName?: string;
12
+ note?: string;
13
+ }
14
+
15
+ export interface ResearchItem {
16
+ title: string;
17
+ summary?: string;
18
+ url?: string;
19
+ author?: string;
20
+ metrics?: Record<string, number | string>;
21
+ platform: BrowserPlatform;
22
+ source: "browser_cdp" | "browser_relay" | "api_provider" | "manual";
23
+ }
24
+
25
+ export interface BrowserResearchQuery {
26
+ platform: BrowserPlatform;
27
+ keyword: string;
28
+ limit?: number;
29
+ }
30
+
31
+ export interface BrowserAdapter {
32
+ id: string;
33
+ description: string;
34
+ getSessionStatus?(platform: BrowserPlatform): Promise<BrowserSessionStatus>;
35
+ research(query: BrowserResearchQuery): Promise<ResearchItem[]>;
36
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Gemini Image Adapter — generates images via Gemini API.
3
+ *
4
+ * Supports two models:
5
+ * - gemini-native: Gemini 2.5 Flash Image (multimodal output, supports reference images)
6
+ * - imagen-4: Imagen 4.0 (text-to-image only, being deprecated June 2026)
7
+ *
8
+ * "auto" mode tries gemini-native first (recommended), falls back to imagen-4.
9
+ */
10
+ import fs from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { withRetry, checkFetchResponse } from "../../utils/retry.js";
13
+
14
+ // --- Types ---
15
+
16
+ export type GeminiModel = "gemini-native" | "imagen-4" | "auto";
17
+ export type AspectRatio = "3:4" | "16:9" | "4:3" | "1:1";
18
+
19
+ export interface GeminiImageOptions {
20
+ prompt: string;
21
+ aspectRatio: AspectRatio;
22
+ model: GeminiModel;
23
+ apiKey: string;
24
+ /** Optional reference image paths (e.g. personal IP photos) */
25
+ referenceImagePaths?: string[];
26
+ /** Where to save the generated image */
27
+ outputPath: string;
28
+ /** Image resolution: "1K" | "2K". Default "1K" */
29
+ resolution?: string;
30
+ }
31
+
32
+ export interface GeminiImageResult {
33
+ ok: boolean;
34
+ imagePath: string;
35
+ model: string;
36
+ mimeType?: string;
37
+ error?: string;
38
+ }
39
+
40
+ // --- Constants ---
41
+
42
+ const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
43
+ const MODEL_NATIVE = "gemini-2.5-flash-preview-image-generation";
44
+ const MODEL_IMAGEN = "imagen-4.0-generate-001";
45
+
46
+ // --- Helpers ---
47
+
48
+ async function fileToBase64(filePath: string): Promise<{ data: string; mimeType: string }> {
49
+ const ext = path.extname(filePath).toLowerCase();
50
+ const mimeMap: Record<string, string> = {
51
+ ".jpg": "image/jpeg",
52
+ ".jpeg": "image/jpeg",
53
+ ".png": "image/png",
54
+ ".webp": "image/webp",
55
+ };
56
+ const mimeType = mimeMap[ext] || "image/jpeg";
57
+ const buffer = await fs.readFile(filePath);
58
+ return { data: buffer.toString("base64"), mimeType };
59
+ }
60
+
61
+ // --- Gemini Native (2.5 Flash Image) ---
62
+
63
+ async function generateNative(options: GeminiImageOptions): Promise<GeminiImageResult> {
64
+ const { prompt, aspectRatio, apiKey, referenceImagePaths, outputPath, resolution } = options;
65
+
66
+ // Build parts array
67
+ const parts: any[] = [{ text: prompt }];
68
+
69
+ // Add reference images if provided
70
+ if (referenceImagePaths?.length) {
71
+ for (const refPath of referenceImagePaths) {
72
+ try {
73
+ const { data, mimeType } = await fileToBase64(refPath);
74
+ parts.push({ inline_data: { mime_type: mimeType, data } });
75
+ } catch {
76
+ // Skip unreadable reference images
77
+ }
78
+ }
79
+ }
80
+
81
+ const body = {
82
+ contents: [{ parts }],
83
+ generationConfig: {
84
+ responseModalities: ["TEXT", "IMAGE"],
85
+ imageConfig: {
86
+ aspectRatio,
87
+ imageSize: resolution || "1K",
88
+ },
89
+ },
90
+ };
91
+
92
+ const url = `${GEMINI_API_BASE}/models/${MODEL_NATIVE}:generateContent?key=${apiKey}`;
93
+
94
+ let json: any;
95
+ try {
96
+ json = await withRetry(async () => {
97
+ const res = await fetch(url, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify(body),
101
+ });
102
+ checkFetchResponse(res, "Gemini Native");
103
+ return res.json();
104
+ });
105
+ } catch (err: unknown) {
106
+ const message = err instanceof Error ? err.message : String(err);
107
+ return { ok: false, imagePath: "", model: MODEL_NATIVE, error: message };
108
+ }
109
+
110
+ // Extract image from response
111
+ const candidates = json.candidates || [];
112
+ for (const candidate of candidates) {
113
+ for (const part of candidate.content?.parts || []) {
114
+ if (part.inlineData?.data) {
115
+ const buffer = Buffer.from(part.inlineData.data, "base64");
116
+ const mimeType = part.inlineData.mimeType || "image/png";
117
+ const ext = mimeType.includes("jpeg") ? ".jpg" : ".png";
118
+ const finalPath = outputPath.endsWith(ext) ? outputPath : outputPath + ext;
119
+
120
+ await fs.mkdir(path.dirname(finalPath), { recursive: true });
121
+ await fs.writeFile(finalPath, buffer);
122
+
123
+ return { ok: true, imagePath: finalPath, model: MODEL_NATIVE, mimeType };
124
+ }
125
+ }
126
+ }
127
+
128
+ return { ok: false, imagePath: "", model: MODEL_NATIVE, error: "No image in response" };
129
+ }
130
+
131
+ // --- Imagen 4 ---
132
+
133
+ async function generateImagen(options: GeminiImageOptions): Promise<GeminiImageResult> {
134
+ const { prompt, aspectRatio, apiKey, outputPath } = options;
135
+
136
+ // Imagen 4 uses a different API format
137
+ const body = {
138
+ instances: [{ prompt }],
139
+ parameters: {
140
+ sampleCount: 1,
141
+ aspectRatio,
142
+ },
143
+ };
144
+
145
+ const url = `${GEMINI_API_BASE}/models/${MODEL_IMAGEN}:predict?key=${apiKey}`;
146
+
147
+ let json: any;
148
+ try {
149
+ json = await withRetry(async () => {
150
+ const res = await fetch(url, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ body: JSON.stringify(body),
154
+ });
155
+ checkFetchResponse(res, "Imagen 4");
156
+ return res.json();
157
+ });
158
+ } catch (err: unknown) {
159
+ const message = err instanceof Error ? err.message : String(err);
160
+ return { ok: false, imagePath: "", model: MODEL_IMAGEN, error: message };
161
+ }
162
+ const predictions = json.predictions || [];
163
+
164
+ if (predictions.length > 0 && predictions[0].bytesBase64Encoded) {
165
+ const buffer = Buffer.from(predictions[0].bytesBase64Encoded, "base64");
166
+ const finalPath = outputPath.endsWith(".png") ? outputPath : outputPath + ".png";
167
+
168
+ await fs.mkdir(path.dirname(finalPath), { recursive: true });
169
+ await fs.writeFile(finalPath, buffer);
170
+
171
+ return { ok: true, imagePath: finalPath, model: MODEL_IMAGEN, mimeType: "image/png" };
172
+ }
173
+
174
+ return { ok: false, imagePath: "", model: MODEL_IMAGEN, error: "No image in Imagen response" };
175
+ }
176
+
177
+ // --- Public API ---
178
+
179
+ /**
180
+ * Generate an image using Gemini API.
181
+ *
182
+ * "auto" mode tries gemini-native first, falls back to imagen-4.
183
+ * gemini-native supports reference images; imagen-4 does not.
184
+ */
185
+ export async function generateImage(options: GeminiImageOptions): Promise<GeminiImageResult> {
186
+ const model = options.model || "auto";
187
+
188
+ if (model === "gemini-native") {
189
+ return generateNative(options);
190
+ }
191
+
192
+ if (model === "imagen-4") {
193
+ return generateImagen(options);
194
+ }
195
+
196
+ // auto: try native first, fall back to imagen-4
197
+ const nativeResult = await generateNative(options);
198
+ if (nativeResult.ok) return nativeResult;
199
+
200
+ // Fallback — imagen-4 doesn't support reference images
201
+ const imagenOptions = { ...options, referenceImagePaths: undefined };
202
+ return generateImagen(imagenOptions);
203
+ }
204
+
205
+ /**
206
+ * List available personal IP reference photos from the templates directory.
207
+ */
208
+ export async function listReferencePhotos(dataDir?: string): Promise<string[]> {
209
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
210
+ const dir = path.join(dataDir || path.join(home, ".autocrew"), "covers", "templates");
211
+ try {
212
+ const files = await fs.readdir(dir);
213
+ return files
214
+ .filter((f) => /\.(jpg|jpeg|png|webp)$/i.test(f))
215
+ .map((f) => path.join(dir, f));
216
+ } catch {
217
+ return [];
218
+ }
219
+ }
@@ -0,0 +1,19 @@
1
+ import type { BrowserPlatform, ResearchItem } from "../browser/types.js";
2
+
3
+ export interface TikHubResearchQuery {
4
+ platform: BrowserPlatform;
5
+ keyword: string;
6
+ limit?: number;
7
+ }
8
+
9
+ export async function researchWithTikHub(
10
+ query: TikHubResearchQuery,
11
+ ): Promise<ResearchItem[]> {
12
+ const limit = query.limit || 5;
13
+ return Array.from({ length: limit }).map((_, index) => ({
14
+ title: `${query.keyword} API 候选 ${index + 1}`,
15
+ summary: "TikHub fallback placeholder. Replace with a real provider call only when browser-first mode is unavailable.",
16
+ platform: query.platform,
17
+ source: "api_provider",
18
+ }));
19
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ASCII art banner for AutoCrew CLI.
3
+ * Only displayed when stdout is a TTY.
4
+ */
5
+
6
+ const LOGO = `
7
+ \x1b[38;5;196m _ _ ____
8
+ / \\ _ _| |_ ___ / ___|_ __ _____ __
9
+ / _ \\| | | | __/ _ \\| | | '__/ _ \\ \\ /\\ / /
10
+ / ___ \\ |_| | || (_) | |___| | | __/\\ V V /
11
+ /_/ \\_\\__,_|\\__\\___/ \\____|_| \\___| \\_/\\_/\x1b[0m`;
12
+
13
+ export function showBanner(version: string): void {
14
+ if (!process.stdout.isTTY) return;
15
+ console.log(LOGO);
16
+ console.log(`\x1b[2m v${version} — AI content operations crew\x1b[0m`);
17
+ console.log();
18
+ }