竞争对手公关发现器(Competitor PR Finder)
输入你的产品网址。它会找出你的竞争对手,研究他们使用的每个公关渠道(新闻、播客、社区),找出跨多个竞争对手出现的渠道(你的已验证目标),查找每个渠道的记者或主持人,并为每个一级渠道起草一封针对你产品的个性化冷邮件。
零幻觉政策: 输出中的每个渠道、记者姓名、故事角度和邮件细节都必须能够追溯到特定的 Tavily 搜索结果或获取的产品页面。这适用于:
- 竞争对手名称:必须出现在 Tavily 搜索结果中,而非 AI 训练知识
- 渠道名称:必须在搜索结果中包含 URL
- 记者/主持人姓名:必须原样出现在 Tavily 摘要中
- 故事角度:仅从搜索结果中的文章/节目标题中提取
- 邮件草稿:必须引用搜索数据和产品分析中的具体证据
常见错误
| 代理可能会... | 为什么这是错误的 |
|---|---|
| 从训练知识中提及记者姓名 | 每个记者姓名都必须能追溯到搜索结果摘要。凭记忆写“Sarah Perez 在 TechCrunch 报道初创公司”是幻觉。 |
| 列出没有证据 URL 的渠道 | 输出中的每个渠道必须至少有一个来自公关搜索结果中的 URL,证明有竞争对手在那里被报道过。 |
| 跳过竞争对手确认步骤 | 始终显示已发现的竞争对手并等待用户确认。错误的竞争对手会导致搜索浪费和输出无效。 |
| 生成通用邮件稿(“我们希望被报道”) | 每封邮件必须引用证据中的特定角度以及产品分析中的特定差异化点。 |
| 仅凭 1 个竞争对手的出现就将渠道标记为 Tier 1 | Tier 1 = 3 个以上竞争对手。Tier 2 = 恰好 2 个。Tier 3 = 1 个。不要提升尚未证明自己的渠道。 |
| 在输出中使用长破折号 | 将所有长破折号 (—) 替换为连字符。 |
每次运行前阅读参考文件
cat references/pr-channel-types.md
cat references/pitch-guide.md
cat references/tier-scoring.md
步骤 1:设置检查
echo "TAVILY_API_KEY: ${TAVILY_API_KEY:+set}${TAVILY_API_KEY:-NOT SET -- required}"
echo "FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:+set}${FIRECRAWL_API_KEY:-not set, Tavily extract will be used as fallback}"
如果 TAVILY_API_KEY 缺失: 立即停止。告知用户:“TAVILY_API_KEY 是研究竞争对手和查找 PR 报道所必需的。没有备用方案。请在 app.tavily.com 获取——免费版:每月 1000 个积分(大约 43 次完整运行,每次约 23 次搜索)。将其添加到你的 .env 文件中。”
如果仅 FIRECRAWL_API_KEY 缺失: 继续。将使用 Tavily extract 进行 URL 获取。
步骤 2:解析输入
从对话中收集:
product_url:要抓取的 URL(必需,除非用户直接粘贴描述)product_name:可选,如果未提供则从页面中推导geography:可选——美国/欧洲/全球。默认为美国
如果用户仅提供粘贴的描述(无 URL): 跳过步骤 3 和 4。直接使用粘贴的文本作为 product_content 进入步骤 4(产品分析)。将 page_source 设置为 user_description 并在 data_quality_flags 中注明。
如果既无 URL 也无描述: 询问:“你的产品或初创公司的 URL 是什么?或者粘贴一个简短描述:它做什么,为谁服务,以及它与竞争对手有何不同。”
推导产品短链名:
PRODUCT_SLUG=$(python3 -c "
from urllib.parse import urlparse
import sys
url = 'URL_HERE'
if url.startswith('http'):
host = urlparse(url).netloc.replace('www.', '')
print(host.split('.')[0])
else:
import re
print(re.sub(r'[^a-z0-9]', '-', url[:30].lower()).strip('-'))
")
echo "Product slug: $PRODUCT_SLUG"
步骤 3:获取产品页面
首选:Firecrawl(如果设置了 FIRECRAWL_API_KEY)
curl -s -X POST https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "URL_HERE", "formats": ["markdown"], "onlyMainContent": true}' \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('data', {}).get('markdown', '') or d.get('markdown', '')
print(f'Fetched via Firecrawl: {len(content)} characters')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
备用:Tavily extract(如果未设置 FIRECRAWL_API_KEY)
curl -s -X POST https://api.tavily.com/extract \
-H "Content-Type: application/json" \
-d "{\"api_key\": \"$TAVILY_API_KEY\", \"urls\": [\"URL_HERE\"]}" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('results', [{}])[0].get('raw_content', '')
print(f'Fetched via Tavily extract: {len(content)} characters')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
检查点:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()
if len(content) < 200:
print('ERROR: fewer than 200 characters fetched')
else:
print(f'Content OK: {len(content)} characters')
"
如果内容 < 200 个字符: 停止获取。告知用户:“产品页面没有返回可读内容——该网站可能因 JavaScript 渲染而被阻止抓取。请直接粘贴一个简短描述:它做什么,为谁服务,以及它与竞争对手有何不同。”
步骤 4:产品分析(AI)
打印页面内容:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()[:5000]
print('=== PRODUCT PAGE (first 5000 chars) ===')
print(content)
"
AI 指示: 分析上面的产品页面并提取:
product_name:产品或公司名称one_line_description:它做什么、为谁服务、核心价值主张。20 字以内。无营销语言。示例:“为自行托管管道的开发团队提供 CI/CD 自动化。”industry_taxonomy:l1(顶级:例如 开发者工具/金融科技/健康科技/消费类),l2(领域:例如 devops/支付/远程医疗),l3(具体细分:例如 CI/CD 自动化/嵌入式支付/异步视频咨询)。不接收入“技术”等模糊标签。differentiators:恰好 2-3 个能将该产品与通用竞争对手区分开来的具体点。这些将直接用于邮件草稿——要具体。示例:[“自托管管道运行器——数据不离开你的基础设施”,“原生支持包含动态步骤生成的 monorepo”]icp:buyer_persona(职位名称),company_type,company_sizegeography_bias:美国/欧洲/全球/不明确page_source:"live_page" 或 "user_description"
写入 /tmp/cprf-product-analysis.json:
python3 << 'PYEOF'
import json
analysis = {
# 根据你的分析填写
"product_name": "",
"one_line_description": "",
"industry_taxonomy": {"l1": "", "l2": "", "l3": ""},
"differentiators": [],
"icp": {"buyer_persona": "", "company_type": "", "company_size": ""},
"geography_bias": "US",
"page_source": "live_page"
}
json.dump(analysis, open('/tmp/cprf-product-analysis.json', 'w'), indent=2)
print('Product analysis written.')
PYEOF
验证:
python3 -c "
import json
a = json.load(open('/tmp/cprf-product-analysis.json'))
print('Product:', a['product_name'])
print('Industry:', a['industry_taxonomy']['l1'], '>', a['industry_taxonomy']['l2'], '>', a['industry_taxonomy']['l3'])
print('Differentiators:')
for d in a['differentiators']:
print(f' - {d}')
"
步骤 4b:第一阶段——竞争对手发现
ls scripts/research.py 2>/dev/null && echo "script found" || echo "ERROR: scripts/research.py not found -- cannot continue"
python3 scripts/research.py \
--phase discover \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-competitors-raw.json
打印结果以供 AI 审查:
python3 -c "
import json
data = json.load(open('/tmp/cprf-competitors-raw.json'))
print(f'Searches run: {len(data[\"competitor_searches\"])}')
for s in data['competitor_searches']:
print(f'\nQuery: {s[\"query\"]}')
print(f'Answer: {s.get(\"answer\",\"\")[:400]}')
for r in s.get('results', [])[:5]:
print(f' - {r[\"title\"]} | {r[\"url\"]}')
print(f' {r.get(\"content\",\"\")[:200]}')
"
AI 指示: 阅读上面的搜索结果。精确挑选 5 家竞争对手公司,它们必须:
- 出现在搜索结果的标题、答案或摘要中
- 与被分析产品处于相同的 L3 细分市场
- 是实际的竞品(而非代理商、咨询公司或列表文章)
- 彼此不同(不是同一家公司下的不同名称)
为每个竞争对手写入:name、url(来自其出现的搜索结果)、description(来自摘要的一句话)、source_url(发现它们的搜索结果 URL)。
步骤 5:竞争对手确认
向用户展示发现的竞争对手:
python3 << 'PYEOF'
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
# 填写:来自上述搜索结果的 5 个竞争对手
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\n在 {analysis['industry_taxonomy']['l3']} 中为 {analysis['product_name']} 找到 5 个竞争对手:\n")
for i, c in enumerate(candidates, 1):
print(f" {i}. {c['name']} -- {c['description']}")
print(f" {c['url']}")
data = json.load(open('/tmp/cprf-competitors-raw.json'))
data['competitor_candidates'] = candidates
json.dump(data, open('/tmp/cprf-competitors-raw.json', 'w'), indent=2)
PYEOF
告知用户:“这 5 个竞争对手就是我将要研究的 PR 报道来源。你可以添加、删除或替换任何一个——或者说‘看起来不错’继续。”
等待确认。 如果用户编辑了列表(添加/删除/替换),相应地更新候选人。然后写入已确认的列表:
python3 << 'PYEOF'
import json
# 填写:已确认的竞争对手列表(用户审查后)
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"已确认 {len(confirmed)} 个竞争对手用于 PR 研究。")
for c in confirmed:
print(f" - {c['name']} ({c['url']})")
PYEOF
步骤 6:三轨公关研究(第二阶段)
python3 scripts/research.py \
--phase pr-research \
--competitors /tmp/cprf-competitors-confirmed.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-pr-raw.json
这会为每个竞争对手运行 3 次搜索(共 15 次):
- 轨道 A(新闻):
"[competitor]" featured press coverage TechCrunch Forbes Wired article interview - 轨道 B(播客):
"[competitor]" founder CEO podcast interview appeared on episode - 轨道 C(社区):
"[competitor]" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com
打印报道摘要:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
print(f'Competitors researched: {data[\"competitors_researched\"]}')
print()
for r in data['results']:
print(f'{r[\"competitor\"]}:')
for track, tdata in r['tracks'].items():
n = len(tdata.get('results', []))
print(f' {track:12}: {n} results')
"
如果某个竞争对手的所有 3 个轨道都返回 0 个结果: 该竞争对手的媒体报道非常少。在 data_quality_flags 中注明并继续——跨竞争对手的模式分析仍可与其余 4 个一起进行。
步骤 7:模式分析(AI)
打印所有原始 PR 结果:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
for r in data['results']:
print(f'\n=== {r[\"competitor\"]} ===')
for track, tdata in r['tracks'].items():
print(f'\n--- Track {track.upper()} ---')
print(f'Query: {tdata[\"query\"]}')
print(f'Answer: {tdata.get(\"answer\",\"\")[:400]}')
for item in tdata.get('results', [])[:5]:
print(f' Title: {item[\"title\"]}')
print(f' URL: {item[\"url\"]}')
print(f' Snippet: {item.get(\"content\",\"\")[:200]}')
"
AI 指示: 阅读以上所有搜索结果。构建渠道频率映射。
步骤 1——将 URL 规范化为根域名: https://techcrunch.com/2023/06/article-title 变为 techcrunch.com。https://open.spotify.com/episode/... 标识为播客(spotify 节目)。https://www.reddit.com/r/devops/ 变为 reddit.com/r/devops。
步骤 2——计数出现次数: 有多少个不同的竞争对手出现在每个渠道根域名的结果中?一个出现在竞争对手 A 的轨道 A 和竞争对手 B 的轨道 A 中的渠道,频次计为 2。
步骤 3——渠道分层(遵循 references/tier-scoring.md):
- 一级 (Tier 1):出现在 3 个以上竞争对手中
- 二级 (Tier 2):恰好出现在 2 个竞争对手中
- 三级 (Tier 3):出现在 1 个竞争对手中
步骤 4——提取故事角度,从结果中的文章/节目标题中提取。将其分类为:融资公告/产品发布/创始人故事/趋势文章/品类开创/操作指南/比较/奖项。不要推断——仅对标题中可见的角度进行分类。
步骤 5——对每个渠道进行类型分类:新闻/播客/社区/新闻邮件。
写入 /tmp/cprf-pr-patterns.json:
python3 << 'PYEOF'
import json
patterns = {
"tier_1_channels": [
# 填写——出现在 3 个以上竞争对手中的渠道
# 每个:{"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_2_channels": [
# 填写——恰好出现在 2 个竞争对手中的渠道
# 每个:{"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_3_channels": [
# 填写——出现在 1 个竞争对手中的渠道
# 每个:{"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
]
}
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print("模式分析完成。所有数据基于可追踪的搜索结果。")
PYEOF
验证:
python3 -c "
import json
p = json.load(open('/tmp/cprf-pr-patterns.json'))
print('Tier 1:', len(p['tier_1_channels']))
print('Tier 2:', len(p['tier_2_channels']))
print('Tier 3:', len(p['tier_3_channels']))
"
步骤 8:火星/Podcast 联系人发现(第三阶段)
python3 scripts/research.py \
--phase journalist-discovery \
--patterns /tmp/cprf-pr-patterns.json \
--competitors /tmp/cprf-competitors-confirmed.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-journalists-raw.json
这会为目标一级渠道运行有针对性的搜索:
- 新闻渠道:
"journalist_name" site:techcrunch.com insightful content - 播客渠道:
"podcast_name" host或"podcast_name" co-founder interview
打印结果:
python3 -c "
import json
data = json.load(open('/tmp/cprf-journalists-raw.json'))
for r in data['results']:
print(f'// {r[\"channel_name\"]} ({r[\"channel_url\"]})')
print(f' Query: {r[\"query\"]}')
for item in r.get('results', [])[:3]:
print(f' - {item[\"title\"]} | {item[\"url\"]}')
print(f' {item.get(\"content\",\"\")[:200]}')
"
AI 指示: 对于每个一级渠道:
- 从结果中提取准确的记者或主持人姓名。不要从训练知识中推断或编造。 如果未提及姓名,则将
journalist_name设为空字符串 "" 并在data_quality_flags中注明。 - 提取记者/主持人的报道领域(例如 "AI startups", "enterprise SaaS", "developer productivity")——仅从搜索结果摘要中提取。
- 如果有多个姓名,将它们全部列出并注明是推测。
针对已发现的所有渠道更新 /tmp/cprf-pr-patterns.json。
步骤 9:生成邮件草稿(最终步骤)
AI 指示: 针对 /tmp/cprf-pr-patterns.json 中的每个一级渠道,使用你在步骤 4 中提取的产品分析和你在步骤 7-8 中发现的证据,起草一封个性化冷邮件。
邮件结构:
- 主题行: 引用故事角度 + 产品名称。示例:“Re: Your coverage of CI/CD pipeline automation – a self-hosted alternative”
- 开头(2 句): 提及记者覆盖的具体角度/主题 -> 链接到你的产品及其独特视角。提及竞争对手(例如 “You covered how Company X approached pipeline security – we’re a self-hosted alternative that keeps data on-premises.”)
- 核心价值主张(3-4 句): 解释你的产品及其差异化点。引用你在步骤 4 中发现的具体差异化点。提及一个与当前新闻周期/趋势相关的具体故事角度。
- 社会证明(1 句): 如果有的话,提及任何牵引力、用户或相关的认可。这可以来自你产品页面上的信息。
- 行动号召(1 句): 具体的下一步。(示例:“Would you like a demo? Our CEO is available Tuesday or Thursday.”)
零幻觉规则:
- 每个事实必须可追溯至:产品页面 (URL)、Tavily 搜索结果 (URL)、或用户输入。
- 记者姓名必须在搜索结果中出现。
- 竞争对手引用必须指代搜索结果中出现的确切竞争对手和主题。
- 故事角度必须来自搜索结果中的标题。
展示格式:
对于每个一级渠道打印:
## [渠道名称] ([渠道 URL])
**分类:** [新闻/播客/社区]
**频次:** [数字] 个竞争对手
**在以下竞争对手中发现:** [竞争对手 A, 竞争对手 B, ...]
**渠道联系人:** [姓名] 报道 [领域]
**故事角度:** [具体角度]
### 邮件草稿
**主题行:** ...
[... 正文 ...]
对于二级和三级渠道打印较小的输出:
## [渠道名称] ([渠道 URL])
**分类:** [新闻/播客/社区]
**频次:** [数字] 个竞争对手
**在以下竞争对手中发现:** [竞争对手]
**渠道联系人:** [姓名或“待发现”]
**故事角度:** [具体角度]
不要为二级或三级渠道生成邮件草稿——只需列出它们并提供角度。
步骤 10:最终输出汇总
将所有内容整合到一个整洁的输出中。包括:
- 执行摘要: 2-3 句总结发现的关键竞争渠道、顶级记者/主持人,以及你的“最容易赢取”的渠道(即:频次高、角度清晰的渠道)
- 一级渠道(带邮件草稿)——至少应有 3 个
- 二级渠道(仅基本信息和角度)
- 三级渠道(仅基本信息和角度)
- 数据质量备注——包括关于完成难易度的任何备注(例如,某些竞争对手未返回结果,某些记者姓名无法通过搜索验证)
使用连字符代替长破折号。保持邮件实用且可操作。