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,232 @@
1
+ ---
2
+ name: write-script
3
+ description: |
4
+ Write one complete content draft for Chinese social media. Activate when user asks to write a post, create content, draft an article, or produce copy. This is the executor — it does the actual writing.
5
+ ---
6
+
7
+ # Write Script
8
+
9
+ > Executor skill. Single responsibility: generate one complete original script and save it.
10
+
11
+ ## Prerequisites
12
+
13
+ Before writing, load these reference documents:
14
+ - `HAMLETDEER.md` — Content philosophy, HKRR framework, Clock Theory, Micro-Retention Techniques
15
+ - `skills/title-craft/SKILL.md` — Title methodology (8 types + quality checklist)
16
+
17
+ For video scripts: apply the **Clock Theory** from HAMLETDEER.md. Map the script to clock positions (12/3/6/9) and ensure every position has a bang moment. Short-form video: pick ONE HKRR element and commit. Long-form: combine all four.
18
+
19
+ ## Steps
20
+
21
+ 1. **Load style & memory context:**
22
+
23
+ a. Read `~/.autocrew/STYLE.md` — absorb brand voice, personality, boundaries.
24
+ b. Read `~/.autocrew/MEMORY.md` — check for writing preferences, past feedback, audience persona.
25
+ c. Read `~/.autocrew/creator-profile.json` — check `styleCalibrated`, `platforms`, `writingRules`, `contentPillars`.
26
+ d. If none exist, proceed with sensible defaults and note that style calibration is recommended.
27
+
28
+ 2. **Content positioning check:**
29
+
30
+ a. Identify which **content pillar** this content belongs to (from `creator-profile.json` → `contentPillars`).
31
+ b. If no pillar match, ask the user: "这条内容属于你的哪个内容支柱?" and list available pillars.
32
+ c. Load the pillar's `targetPersona`, `toneGuide`, and `exampleAngles` as writing context.
33
+ d. If `contentPillars` is not configured, skip this step and note that content positioning is recommended.
34
+
35
+ 3. **Traffic hypothesis (MANDATORY):**
36
+
37
+ Before writing, state the hypothesis for this content:
38
+ - "I believe [this angle/hook/format] will [expected outcome] because [reason]"
39
+ - Classify: `title_test` / `hook_test` / `format_test` / `angle_test`
40
+ - If testing against a previous content, note the `controlRef` (content ID being compared to)
41
+
42
+ If the user doesn't provide a hypothesis, infer one from the topic and state it explicitly.
43
+
44
+ 4. If a topic was specified, load its details via `autocrew_topic` action="list" and find the matching topic.
45
+
46
+ 5. **Check for relevant teardowns:**
47
+
48
+ Search `~/.autocrew/data/pipeline/intel/_teardowns/` for teardown reports related to this topic. If found, use them as reference for what works in this space.
49
+
50
+ 6. **Write the script:**
51
+
52
+ a. **Hook** — pick the ONE strongest type for this topic:
53
+
54
+ | Type | When to use | Example pattern |
55
+ |------|-------------|-----------------|
56
+ | Pain point | Audience has an obvious unresolved frustration | "XX最大的问题不是YY,而是ZZ" |
57
+ | Suspense | Topic has a counterintuitive truth or surprising data | "我花了X万测试,结果发现…" |
58
+ | Ideal state | Topic sells a desirable outcome | "X个月后,我再也不用YY了" |
59
+ | Emotional resonance | Topic touches identity, belonging, or aspiration | "每个做XX的人都经历过这一刻" |
60
+ | Contrast | Clear gap between common belief and reality | "别人XX,你却在YY" |
61
+
62
+ Write 1-3 sentences. NEVER open with "哈喽大家好", "你有没有想过", or any generic greeting.
63
+
64
+ b. **Body** — the core content. NOT a list of points. A conversation.
65
+
66
+ **Writing rules (non-negotiable):**
67
+ - Write like you're talking to ONE person sitting across from you, not lecturing to a crowd.
68
+ - Vary sentence length deliberately: 3-4 short sentences, then one longer one. Then a one-word sentence. "真的。"
69
+ - Each claim needs: why it's true + concrete example (named, specific, verifiable when possible).
70
+ - Total body: 800-1500 characters (text) or platform-specific limit.
71
+
72
+ **Anti-patterns (NEVER do these):**
73
+ - ❌ "总而言之" / "综上所述" / "值得一提的是" — essay transitions that kill conversational tone
74
+ - ❌ "首先…其次…最后…" — numbered structure that screams AI/lecture
75
+ - ❌ Balanced "一方面…另一方面…" hedging — pick a side
76
+ - ❌ Lists of exactly 5 items — AI signature pattern
77
+ - ❌ Every paragraph same length — real people write unevenly
78
+ - ❌ Generic examples ("某个企业", "一家公司") — name it or don't use it
79
+
80
+ **Must-include elements:**
81
+ - 1-2 expectation-breaking twists (contrarian flip, data bomb)
82
+ - 1-2 interaction hooks (questions, "你猜怎么着", comment prompts)
83
+ - HKRR annotation: for each section, note which HKRR element is active (in your planning, not in output)
84
+
85
+ **Micro-retention techniques (use at least 2):**
86
+ - **Open Loop**: raise a question early, resolve it later — "后面告诉你为什么"
87
+ - **Curiosity Gap**: end paragraphs with forward momentum — "但这还不是最离谱的"
88
+ - **Visual Anchor**: standalone one-liner between paragraphs — a quotable insight
89
+ - **Rhythm Break**: sudden short sentence after buildup — "错。" / "但这不重要。"
90
+
91
+ c. **Comment triggers (MANDATORY — annotate 1-2):**
92
+
93
+ After writing the body, identify and annotate comment trigger points:
94
+
95
+ | Type | What it does | Example |
96
+ |------|-------------|---------|
97
+ | Controversy plant | Leave a debatable opinion | "我知道很多人不同意,但我觉得XX根本没用" |
98
+ | Unanswered question | Raise but don't fully answer | "至于为什么大厂不这么做?评论区聊" |
99
+ | Quote hook | One sentence worth screenshotting | "AI不是来替你干活的,是来替你做决定的" |
100
+
101
+ Record as: `commentTriggers: [{ type: "controversy", position: "paragraph 3" }]`
102
+
103
+ d. **CTA** — 1-2 sentences guiding a specific action (save/comment/follow).
104
+ Must connect to the content's value — "收藏这条,下次用得上" beats "觉得有用就点赞".
105
+
106
+ e. **Title — generate 3-5 candidates using title formulas:**
107
+
108
+ | Formula | Pattern | Example |
109
+ |---------|---------|---------|
110
+ | Number + Result | 数字+具体结果 | "用了3个月AI,我把团队从12人砍到3人" |
111
+ | Contrarian | 反直觉声明 | "AI写的代码比人快10倍,但我劝你别用" |
112
+ | Identity + Pain | 身份标签+痛点 | "传统老板看过来:这3种AI项目100%是坑" |
113
+ | Curiosity Gap | 悬念缺口 | "花了20万做AI系统,结果…" |
114
+ | Contrast | 对比结构 | "别人用AI赚钱,你用AI亏钱,差在哪?" |
115
+ | Resonance Question | 共鸣提问 | "为什么你学了那么多AI课,还是不会用?" |
116
+
117
+ Rules:
118
+ - Generate 3-5 candidates, annotate which formula each uses.
119
+ - Also call `generateForPlatform(baseTopic, platform)` from title-hashtag.ts for additional variants.
120
+ - Pick the best as primary. 15-25 characters. Can include emoji if it adds value.
121
+ - If `web_search` is available, search 2-3 trending keywords and embed 1 naturally.
122
+
123
+ f. **Hashtags** — generate platform-specific hashtags:
124
+ - Call `generateHashtags(topic, platform, tags)` from `title-hashtag.ts`.
125
+ - Append hashtags to the body (for platforms that use inline hashtags like XHS/Douyin).
126
+ - Save hashtags separately in the `hashtags` field.
127
+
128
+ g. **Clock mapping (video content):**
129
+ Before finalizing, map the script to clock positions:
130
+
131
+ | Clock | Content section | Bang moment type | HKRR element |
132
+ |-------|----------------|-----------------|-------------|
133
+ | 12:00 | [your hook] | [e.g., pattern break] | [e.g., Resonance] |
134
+ | 3:00 | [paragraph X] | [e.g., data bomb] | [e.g., Knowledge] |
135
+ | 6:00 | [paragraph Y] | [e.g., framework reveal] | [e.g., Knowledge] |
136
+ | 9:00 | [CTA section] | [e.g., audience mirror] | [e.g., Resonance] |
137
+
138
+ 7. **Self-review before saving** (fix any failure, don't just check):
139
+ - [ ] 800+ characters total?
140
+ - [ ] Contains at least 2 concrete examples or scenarios (not vague claims)?
141
+ - [ ] Has a non-obvious insight or twist?
142
+ - [ ] Tone matches STYLE.md profile (if available)?
143
+ - [ ] No generic greetings, no essay-style paragraphs?
144
+ - [ ] No anti-pattern violations (总而言之, 首先其次最后, balanced hedging)?
145
+ - [ ] Body is plain text with blank-line separators (no markdown headers)?
146
+ - [ ] Title within platform character limit? Uses a title formula?
147
+ - [ ] At least 1 comment trigger annotated?
148
+ - [ ] At least 2 micro-retention techniques used?
149
+ - [ ] Hypothesis stated?
150
+ - [ ] Content pillar specified (if pillars configured)?
151
+ - [ ] Hashtags generated and relevant?
152
+
153
+ 8. **Save via tool:**
154
+ ```json
155
+ {
156
+ "action": "save",
157
+ "title": "The single best title (no emoji in title field)",
158
+ "body": "Full script as plain text. Blank lines between sections.",
159
+ "platform": "xiaohongshu",
160
+ "topicId": "topic-xxx (if based on a topic)",
161
+ "tags": ["tag1", "tag2"],
162
+ "hashtags": ["#标签1", "#标签2"],
163
+ "status": "draft",
164
+ "hypothesis": "I believe [angle] will [outcome] because [reason]",
165
+ "experiment_type": "hook_test",
166
+ "content_pillar": "AI落地避坑",
167
+ "comment_triggers": [{"type": "controversy", "position": "paragraph 3"}]
168
+ }
169
+ ```
170
+
171
+ Note: Auto-humanize runs automatically inside the save tool. You do NOT need to call humanize separately.
172
+
173
+ 9. **Auto-review (MANDATORY — runs silently):**
174
+ - After saving, ALWAYS run content review:
175
+ ```json
176
+ { "action": "full_review", "content_id": "<saved-id>", "platform": "<platform>" }
177
+ ```
178
+ - If review passes → proceed to output.
179
+ - If review finds issues → auto-fix what can be fixed, then proceed to output.
180
+ - Do NOT ask the user "要审核吗?" — just do it.
181
+
182
+ 10. **Output to user:**
183
+ Show the complete draft in chat, including:
184
+ - Title (with 2-3 alternative variants + formula annotations)
185
+ - Content pillar tag
186
+ - Hypothesis
187
+ - Full body text
188
+ - Comment trigger annotations
189
+ - Micro-retention technique annotations
190
+ - Hashtags
191
+ - Clock mapping (for video)
192
+ - Review result summary (pass/issues found)
193
+ - **File location**:
194
+ > 📄 内容已保存到:`~/.autocrew/contents/{content-id}/draft.md`
195
+ > 打开方式:在终端执行 `open ~/.autocrew/contents/{content-id}/` 或用 Obsidian 打开 `~/.autocrew/` 文件夹
196
+ Then:
197
+ > 要修改的话直接说,或者确认后我帮你标记为待发布。
198
+
199
+ 11. **If adaptation is needed:**
200
+ - Do not just trim one draft for another platform.
201
+ - Use `platform-rewrite` / `autocrew_rewrite` to create the first platform-native version.
202
+
203
+ ## Platform-Specific Adjustments
204
+
205
+ | Platform | Chars | Style notes |
206
+ |----------|-------|-------------|
207
+ | xiaohongshu | 300-1000 | Emoji-rich, casual, hashtags at end (5-15) |
208
+ | douyin | Script format | [Scene] + [Voiceover] + [Text overlay], hook in 3s |
209
+ | wechat_mp | 1500-3000 | Subheadings every 300-500 chars, more structured |
210
+ | wechat_video | 300-800 | Educational tone, include text summary |
211
+ | bilibili | 500-2000 | 年轻化表达,可以用梗,【】标注类型 |
212
+
213
+ ## Title & Hashtag Integration
214
+
215
+ The `title-hashtag.ts` module provides:
216
+ - `generateTitleVariants(topic, platform)` → 3-5 title variants with style labels
217
+ - `generateHashtags(topic, platform, tags)` → deduplicated, platform-limited hashtags
218
+ - `generateForPlatform(topic, platform)` → titles + hashtags + tips in one call
219
+
220
+ Always use these for structured title/hashtag generation. The AI agent refines the output — these are starting points, not final answers.
221
+
222
+ ## Error Handling
223
+
224
+ | Failure | Action |
225
+ |---------|--------|
226
+ | Style/Memory files missing | Write with sensible defaults. Suggest running style-calibration. |
227
+ | Topic not found | Ask user for topic details directly. |
228
+ | Save fails | Output the content in chat so user can copy it. Retry save once. |
229
+ | Review fails to run | Save the draft anyway. Note that review was skipped. |
230
+ | title-hashtag returns empty | Fall back to manual title + basic hashtags from tags. |
231
+ | Content pillars not configured | Skip pillar check, suggest configuring pillars. |
232
+ | No teardowns found | Skip reference step, write from scratch. |
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: xhs-cover-review
3
+ description: |
4
+ 小红书封面审核 skill。基于真实原型生成 A/B/C 三个不同结构候选,进入 review_pending,等用户选定后推进到 publish_ready。
5
+ ---
6
+
7
+ # XHS Cover Review
8
+
9
+ ## Purpose
10
+
11
+ Turn one saved draft into a structured Xiaohongshu cover approval flow.
12
+
13
+ ## Rules
14
+
15
+ 1. A / B / C must represent three different structural prototypes, not three color variants.
16
+ 2. At least one variant should come from same-track references.
17
+ 3. At least one variant should come from cross-track transfer.
18
+ 4. Approval is mandatory before publish-ready.
19
+ 5. Do not silently skip review and jump to publish.
20
+
21
+ ## Tool Usage
22
+
23
+ Create candidates:
24
+
25
+ ```json
26
+ {
27
+ "action": "create_candidates",
28
+ "content_id": "content-xxx"
29
+ }
30
+ ```
31
+
32
+ Approve one:
33
+
34
+ ```json
35
+ {
36
+ "action": "approve",
37
+ "content_id": "content-xxx",
38
+ "label": "b"
39
+ }
40
+ ```
41
+
42
+ ## Output
43
+
44
+ Always report:
45
+
46
+ - review status
47
+ - the three prototype directions
48
+ - which cover was approved
@@ -0,0 +1,260 @@
1
+ import type {
2
+ BrowserAdapter,
3
+ BrowserPlatform,
4
+ BrowserResearchQuery,
5
+ BrowserSessionStatus,
6
+ ResearchItem,
7
+ } from "./types.js";
8
+
9
+ const DEFAULT_PROXY_URL = process.env.AUTOCREW_CDP_PROXY_URL || "http://127.0.0.1:3456";
10
+
11
+ type ProxyEvalResult = {
12
+ result?: { value?: unknown };
13
+ exceptionDetails?: unknown;
14
+ };
15
+
16
+ function sleep(ms: number): Promise<void> {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+
20
+ function buildPlatformUrl(platform: BrowserPlatform): string {
21
+ switch (platform) {
22
+ case "xiaohongshu":
23
+ return "https://www.xiaohongshu.com/";
24
+ case "douyin":
25
+ return "https://www.douyin.com/";
26
+ case "wechat_mp":
27
+ return "https://mp.weixin.qq.com/";
28
+ case "wechat_video":
29
+ return "https://channels.weixin.qq.com/platform/";
30
+ case "bilibili":
31
+ return "https://www.bilibili.com/";
32
+ default:
33
+ return "https://www.xiaohongshu.com/";
34
+ }
35
+ }
36
+
37
+ function buildSearchUrl(platform: BrowserPlatform, keyword: string): string {
38
+ const encoded = encodeURIComponent(keyword);
39
+ switch (platform) {
40
+ case "xiaohongshu":
41
+ return `https://www.xiaohongshu.com/search_result?keyword=${encoded}`;
42
+ case "douyin":
43
+ return `https://www.douyin.com/search/${encoded}?type=video`;
44
+ case "bilibili":
45
+ return `https://search.bilibili.com/all?keyword=${encoded}`;
46
+ case "wechat_mp":
47
+ return `https://weixin.sogou.com/weixin?type=2&query=${encoded}`;
48
+ case "wechat_video":
49
+ return `https://channels.weixin.qq.com/platform/`;
50
+ default:
51
+ return `https://www.xiaohongshu.com/search_result?keyword=${encoded}`;
52
+ }
53
+ }
54
+
55
+ function sessionCheckExpression(platform: BrowserPlatform): string {
56
+ return `(() => {
57
+ const href = location.href;
58
+ const text = (document.body?.innerText || "").slice(0, 2000);
59
+ const checks = {
60
+ xiaohongshu: /login|登录/.test(href) || /登录后查看更多|立即登录/.test(text),
61
+ douyin: /login|sso\\.douyin/.test(href) || /扫码登录|手机号登录/.test(text),
62
+ wechat_mp: /login/.test(href) || /扫码登录|微信公众平台/.test(text),
63
+ wechat_video: /login/.test(href) || /扫码登录|视频号助手/.test(text),
64
+ bilibili: /passport|login/.test(href) || /请先登录|登录后/.test(text),
65
+ };
66
+ const loggedIn = !checks[${JSON.stringify(platform)}];
67
+ return JSON.stringify({
68
+ href,
69
+ title: document.title,
70
+ loggedIn,
71
+ textHint: text.slice(0, 200),
72
+ });
73
+ })()`;
74
+ }
75
+
76
+ function researchExpression(platform: BrowserPlatform, limit: number): string {
77
+ return `(() => {
78
+ const limit = ${limit};
79
+ const abs = (href) => {
80
+ try { return new URL(href, location.href).href; } catch { return href || ""; }
81
+ };
82
+ const dedupe = new Set();
83
+ const push = (items, item) => {
84
+ if (!item || !item.title) return;
85
+ const key = item.title + "::" + (item.url || "");
86
+ if (dedupe.has(key)) return;
87
+ dedupe.add(key);
88
+ items.push(item);
89
+ };
90
+ const text = (el) => (el?.innerText || el?.textContent || "").replace(/\\s+/g, " ").trim();
91
+ const items = [];
92
+
93
+ if (${JSON.stringify(platform)} === "xiaohongshu") {
94
+ const cards = Array.from(document.querySelectorAll("section, .note-item, [data-index], a"));
95
+ for (const card of cards) {
96
+ const titleEl = card.querySelector?.("a[href*='/explore/'], a[href*='/discovery/item/'], a[href*='/note/'], .title span, .desc, .note-title") || card;
97
+ const linkEl = card.querySelector?.("a[href*='/explore/'], a[href*='/discovery/item/'], a[href*='/note/']") || card.closest?.("a");
98
+ const authorEl = card.querySelector?.(".author, .name");
99
+ const title = text(titleEl);
100
+ const url = linkEl?.href ? abs(linkEl.href) : "";
101
+ if (title.length >= 4 && url.includes("xiaohongshu.com")) {
102
+ push(items, { title, url, author: text(authorEl) });
103
+ }
104
+ if (items.length >= limit) break;
105
+ }
106
+ } else if (${JSON.stringify(platform)} === "douyin") {
107
+ const cards = Array.from(document.querySelectorAll("a[href*='/video/'], [data-e2e='search-result-container'] a, .ECMy_Zdt"));
108
+ for (const card of cards) {
109
+ const titleEl = card.querySelector?.("span, p, h3") || card;
110
+ const url = card.href ? abs(card.href) : abs(card.querySelector?.("a")?.href || "");
111
+ const title = text(titleEl);
112
+ if (title.length >= 4 && url.includes("douyin.com")) {
113
+ push(items, { title, url });
114
+ }
115
+ if (items.length >= limit) break;
116
+ }
117
+ } else if (${JSON.stringify(platform)} === "bilibili") {
118
+ const cards = Array.from(document.querySelectorAll("a[href*='/video/'], .bili-video-card a, .video-list-item a"));
119
+ for (const card of cards) {
120
+ const title = card.getAttribute?.("title") || text(card);
121
+ const url = card.href ? abs(card.href) : "";
122
+ if (title && url.includes("bilibili.com")) {
123
+ push(items, { title, url });
124
+ }
125
+ if (items.length >= limit) break;
126
+ }
127
+ } else {
128
+ const links = Array.from(document.querySelectorAll("a"));
129
+ for (const link of links) {
130
+ const title = text(link);
131
+ const url = link.href ? abs(link.href) : "";
132
+ if (title.length >= 6 && url) {
133
+ push(items, { title, url });
134
+ }
135
+ if (items.length >= limit) break;
136
+ }
137
+ }
138
+
139
+ return JSON.stringify(items.slice(0, limit));
140
+ })()`;
141
+ }
142
+
143
+ class WebAccessProxyClient {
144
+ constructor(private readonly baseUrl: string) {}
145
+
146
+ async isReachable(): Promise<boolean> {
147
+ try {
148
+ const response = await fetch(`${this.baseUrl}/json/version`);
149
+ return response.ok;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ async open(url: string): Promise<string> {
156
+ const response = await fetch(`${this.baseUrl}/new?url=${encodeURIComponent(url)}`);
157
+ if (!response.ok) {
158
+ throw new Error(`Failed to open tab for ${url}`);
159
+ }
160
+ const data = await response.json();
161
+ return data.id || data.targetId || data.tabId;
162
+ }
163
+
164
+ async eval(tabId: string, expression: string): Promise<unknown> {
165
+ const response = await fetch(`${this.baseUrl}/eval/${encodeURIComponent(tabId)}`, {
166
+ method: "POST",
167
+ headers: { "content-type": "text/plain;charset=UTF-8" },
168
+ body: expression,
169
+ });
170
+ if (!response.ok) {
171
+ throw new Error(`Failed to eval expression for tab ${tabId}`);
172
+ }
173
+ const data = (await response.json()) as ProxyEvalResult;
174
+ if (data.exceptionDetails) {
175
+ throw new Error(`Browser eval failed for tab ${tabId}`);
176
+ }
177
+ return data.result?.value;
178
+ }
179
+
180
+ async close(tabId: string): Promise<void> {
181
+ await fetch(`${this.baseUrl}/close/${encodeURIComponent(tabId)}`);
182
+ }
183
+ }
184
+
185
+ async function withTab<T>(url: string, fn: (client: WebAccessProxyClient, tabId: string) => Promise<T>): Promise<T> {
186
+ const client = new WebAccessProxyClient(DEFAULT_PROXY_URL);
187
+ const tabId = await client.open(url);
188
+ try {
189
+ await sleep(2500);
190
+ return await fn(client, tabId);
191
+ } finally {
192
+ await client.close(tabId).catch(() => undefined);
193
+ }
194
+ }
195
+
196
+ async function getSessionStatus(platform: BrowserPlatform): Promise<BrowserSessionStatus> {
197
+ const client = new WebAccessProxyClient(DEFAULT_PROXY_URL);
198
+ const reachable = await client.isReachable();
199
+ if (!reachable) {
200
+ return {
201
+ platform,
202
+ loggedIn: false,
203
+ note: `CDP proxy unreachable at ${DEFAULT_PROXY_URL}`,
204
+ };
205
+ }
206
+
207
+ try {
208
+ return await withTab(buildPlatformUrl(platform), async (tabClient, tabId) => {
209
+ const raw = await tabClient.eval(tabId, sessionCheckExpression(platform));
210
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : {};
211
+ return {
212
+ platform,
213
+ loggedIn: Boolean(parsed.loggedIn),
214
+ note: parsed.href || parsed.title || "session checked",
215
+ };
216
+ });
217
+ } catch (error: any) {
218
+ return {
219
+ platform,
220
+ loggedIn: false,
221
+ note: error?.message || "session check failed",
222
+ };
223
+ }
224
+ }
225
+
226
+ async function research(query: BrowserResearchQuery): Promise<ResearchItem[]> {
227
+ const client = new WebAccessProxyClient(DEFAULT_PROXY_URL);
228
+ const reachable = await client.isReachable();
229
+ if (!reachable) {
230
+ return [];
231
+ }
232
+
233
+ try {
234
+ return await withTab(buildSearchUrl(query.platform, query.keyword), async (tabClient, tabId) => {
235
+ const raw = await tabClient.eval(tabId, researchExpression(query.platform, query.limit || 5));
236
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : [];
237
+ if (!Array.isArray(parsed)) {
238
+ return [];
239
+ }
240
+ return parsed.map((item: any) => ({
241
+ title: String(item.title || "").trim(),
242
+ summary: item.author ? `参考账号:${String(item.author).trim()}` : "来自浏览器登录态搜索结果",
243
+ url: item.url ? String(item.url) : undefined,
244
+ author: item.author ? String(item.author) : undefined,
245
+ platform: query.platform,
246
+ source: "browser_cdp" as const,
247
+ }));
248
+ });
249
+ } catch {
250
+ return [];
251
+ }
252
+ }
253
+
254
+ export const browserCdpAdapter: BrowserAdapter = {
255
+ id: "browser_cdp",
256
+ description:
257
+ "Browser-first adapter using a web-access style CDP proxy. Reads the user's own logged-in browser session through HTTP endpoints.",
258
+ getSessionStatus,
259
+ research,
260
+ };