关于 competitor-pr-finder
competitor-pr-finder 是一项根据产品网址或描述研究竞品公关渠道的技能。它以 Tavily 搜索结果和产品页面为依据,统计渠道出现频次并起草定制推介。
主要功能
- 01发现竞品并等待确认
- 02研究媒体、播客和社区渠道
- 03按竞品出现频次划分渠道
- 04依据搜索证据查找记者和主持人
- 05结合产品差异点起草推介
适用场景
- 01查找竞品使用过的公关渠道
- 02优先关注多个竞品出现的媒体
- 03为一级渠道准备有证据支持的推介
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ varnan-tech/ opendirectory --skill competitor-pr-finder安装后在 Claude Code 中输入/competitor-pr-finder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.env.exampleREADME.mdevals/evals.jsonreferences/pitch-guide.mdpr-channel-types.mdtier-scoring.mdscripts/research.py详细介绍
竞争对手 PR 发现器
提供你的产品 URL。它会找到你的竞争对手,调查他们使用的每个 PR 渠道(新闻、播客、社区),找出多个竞争对手都出现过的渠道(你已验证有效的目标),为每个渠道找到记者或主持人,并为每个 Tier 1 渠道撰写针对你产品的个性化冷邮件。
零幻觉政策: 输出中的每个渠道、记者姓名、报道角度和邮件细节都必须能追溯到具体的 Tavily 搜索结果或抓取的产品页面。具体要求:
- 竞争对手名称:必须出现在 Tavily 搜索结果中,不得来自 AI 训练知识
- 渠道名称:必须在搜索结果中附带 URL
- 记者/主持人姓名:必须在 Tavily 摘要中原文出现
- 报道角度:只能从搜索结果中的文章/节目标题提取
- 邮件草稿:必须引用搜索数据中的具体证据和产品分析
常见错误
| Agent 可能会想…… | 为什么不对 |
|---|---|
| 根据训练知识说出记者姓名 | 每个记者姓名都必须能追溯到搜索结果摘要。凭记忆写出“Sarah Perez 在 TechCrunch 负责报道初创公司”就是幻觉。 |
| 列出没有证据 URL 的渠道 | 输出中的每个渠道都必须至少有一个 PR 搜索结果 URL,证明某个竞争对手曾在该渠道出现。 |
| 跳过竞争对手确认步骤 | 始终先展示发现的竞争对手,并等待用户确认。竞争对手找错了,搜索就白费了,输出也毫无用处。 |
| 生成泛泛的邮件(“我们很希望能获得报道”) | 每封邮件都必须引用证据中的具体角度,以及产品分析中的具体差异化优势。 |
| 仅凭 1 个竞争对手的出现就将渠道标记为 Tier 1 | Tier 1 = 3 个或更多竞争对手。Tier 2 = 恰好 2 个。Tier 3 = 1 个。不要把未经验证的渠道升级。 |
| 在输出中使用 em dash | 将所有 em dash(--)替换为连字符。 |
每次运行前阅读参考文件
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: 立即停止。告知用户:“研究竞争对手并查找 PR 报道需要 TAVILY_API_KEY。没有备用方案。请在 app.tavily.com 获取 -- 免费套餐:每月 1000 个额度(按每次运行约 23 次搜索计算,可完整运行约 43 次)。请将其添加到你的 .env 文件中。”
如果只缺少 FIRECRAWL_API_KEY: 继续。将使用 Tavily extract 获取 URL 内容。
步骤 2:解析输入
从对话中收集:
product_url:要抓取的 URL(必填,除非用户直接粘贴了描述)product_name:可选;如果未提供,则从页面中提取geography:可选 -- US / Europe / global。默认值:US
如果用户只提供了粘贴的描述(没有 URL): 跳过步骤 3 和 4。直接进入步骤 4(产品分析),并将粘贴的文本用作 product_content。将 page_source 设为 user_description,并在 data_quality_flags 中注明。
如果既没有 URL,也没有描述: 询问:“你的产品或初创公司的 URL 是什么?或者粘贴一段简短描述:它的功能、目标用户,以及它与竞争对手的不同之处。”
生成产品 slug:
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 项能让该产品区别于通用竞品的具体特点。这些内容会直接用于宣传文案草稿——请具体描述。示例:["自托管流水线运行器——数据不会离开你的基础设施", "原生支持单体仓库并可动态生成步骤"]icp:buyer_persona(职位),company_type(公司类型),company_size(公司规模)geography_bias:美国/欧洲/全球/不明确page_source:"live_page" 或 "user_description"
写入 /tmp/cprf-product-analysis.json:
python3 << 'PYEOF'
import json
analysis = {
# FILL from your analysis above
"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:阶段 1——竞品发现
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'))
# FILL: 5 competitors from the search results above
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\nFound 5 competitors for {analysis['product_name']} in {analysis['industry_taxonomy']['l3']}:\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
告诉用户:“以下是我将为 PR 报道研究的 5 家竞品。请添加、删除或替换任意竞品,或者回复‘看起来不错’以继续。”
等待确认。 如果用户编辑列表(添加/删除/替换),相应更新候选项。然后写入确认后的列表:
python3 << 'PYEOF'
import json
# FILL: confirmed competitor list (after user review)
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"Confirmed {len(confirmed)} competitors for PR research.")
for c in confirmed:
print(f" - {c['name']} ({c['url']})")
PYEOF
步骤 6:三轨 PR 研究(阶段 2)
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 episode)。https://www.reddit.com/r/devops/ → reddit.com/r/devops。
**第 2 步——统计出现次数:**每个频道根域名的结果中出现了多少个不同的竞争对手?如果某个频道同时出现在竞争对手 A 的 Track A 和竞争对手 B 的 Track 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": [
# FILL -- channels appearing in 3+ competitors
# Each: {"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": [
# FILL -- channels appearing in exactly 2 competitors
# Each: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": 2, "found_in_competitors": [str], "evidence_urls": [str],
# "story_angles_used": [str]}
],
"tier_3_channels": [
# FILL -- channels appearing in only 1 competitor (name + URL only)
# Each: {"channel_name": str, "channel_url": str, "found_in_competitor": str}
],
"data_quality_flags": []
}
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
PYEOF
验证:
python3 -c "
import json
p = json.load(open('/tmp/cprf-pr-patterns.json'))
print(f'Tier 1 channels: {len(p[\"tier_1_channels\"])}')
for ch in p['tier_1_channels']:
print(f' {ch[\"frequency\"]}x {ch[\"channel_name\"]} ({ch[\"channel_type\"]}) -- {ch[\"found_in_competitors\"]}')
print(f'Tier 2 channels: {len(p[\"tier_2_channels\"])}')
print(f'Tier 3 channels: {len(p[\"tier_3_channels\"])}')
"
**如果 Tier 1 频道少于 3 个:**在细分市场中这很常见。将频率最高的 Tier 2 频道提升层级,以便至少有 3 个频道进行深入分析。在 data_quality_flags 中注明此项提升。
第 8 步:记者/主持人查询
对每个 Tier 1 频道(最多 7 个),运行一次有针对性的 Tavily 搜索:
python3 << 'PYEOF'
import json, os, urllib.request
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
l2 = analysis['industry_taxonomy']['l2']
l3 = analysis['industry_taxonomy']['l3']
tavily_key = os.environ.get('TAVILY_API_KEY', '')
lookup_results = []
for channel in patterns.get('tier_1_channels', [])[:7]:
name = channel['channel_name']
ctype = channel['channel_type']
if ctype == 'editorial':
query = f'"{name}" journalist reporter writer covers {l2} {l3} startups technology'
elif ctype == 'podcast':
query = f'"{name}" podcast host interviewer {l2} {l3} founders'
else:
query = f'"{name}" moderator community manager {l2} {l3}'
payload = json.dumps({
"api_key": tavily_key,
"query": query,
"search_depth": "basic",
"max_results": 5
}).encode()
req = urllib.request.Request(
'https://api.tavily.com/search',
data=payload,
headers={'Content-Type': 'application/json'},
method='POST'
)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
data = json.loads(resp.read())
lookup_results.append({
'channel': name,
'channel_type': ctype,
'query': query,
'answer': data.get('answer', ''),
'results': [
{'title': r['title'], 'url': r['url'], 'content': r.get('content', '')[:400]}
for r in data.get('results', [])[:3]
]
})
print(f'Journalist lookup -- {name}: {len(data.get("results", []))} results')
except Exception as e:
lookup_results.append({
'channel': name, 'channel_type': ctype,
'query': query, 'answer': '', 'results': [], 'error': str(e)
})
print(f'Journalist lookup -- {name}: FAILED ({e})')
json.dump(lookup_results, open('/tmp/cprf-journalist-results.json', 'w'), indent=2)
print(f'Journalist lookups complete: {len(lookup_results)} channels')
PYEOF
打印结果供 AI 提取:
python3 -c "
import json
results = json.load(open('/tmp/cprf-journalist-results.json'))
for r in results:
print(f'\n=== {r[\"channel\"]} ({r[\"channel_type\"]}) ===')
print(f'Answer: {r.get(\"answer\",\"\")[:400]}')
for item in r.get('results', []):
print(f' {item[\"title\"]}')
print(f' {item.get(\"content\",\"\")[:300]}')
"
**AI 指令:**对每个 Tier 1 频道,从上方的搜索结果中提取:
journalist_name:原样填写片段中的人名。如果没有,则填写“未在搜索数据中找到”——不要根据训练知识补全。journalist_beat:从片段文本中提取其报道主题。如果没有,则填写“未在搜索数据中找到”。
更新 /tmp/cprf-pr-patterns.json,为每个 Tier 1 频道填写 journalist_name 和 journalist_beat:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
# FILL: update journalist_name and journalist_beat for each tier_1 channel
# journalist_name and journalist_beat come from search snippet text only
# Write "not found in search data" if the snippets don't name a person
# Example:
# patterns['tier_1_channels'][0]['journalist_name'] = 'Ingrid Lunden'
# patterns['tier_1_channels'][0]['journalist_beat'] = 'enterprise software and developer tools'
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Journalist data updated.')
for ch in patterns['tier_1_channels']:
print(f" {ch['channel_name']}: {ch.get('journalist_name','--')} | {ch.get('journalist_beat','--')}")
PYEOF
第 9 步:综合整理 —— 生成外联方案(AI)
输出汇总数据:
python3 -c "
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
print('=== PRODUCT ===')
print(f'Name: {analysis[\"product_name\"]}')
print(f'What it does: {analysis[\"one_line_description\"]}')
print(f'Differentiators:')
for d in analysis['differentiators']:
print(f' - {d}')
print(f'ICP: {analysis[\"icp\"]}')
print(f'Geography: {analysis[\"geography_bias\"]}')
print()
print('=== TIER 1 CHANNELS ===')
for ch in patterns['tier_1_channels']:
print(f'\n{ch[\"channel_name\"]} ({ch[\"channel_type\"]}, freq={ch[\"frequency\"]})')
print(f' Found in: {ch[\"found_in_competitors\"]}')
print(f' Evidence URLs: {ch[\"evidence_urls\"][:3]}')
print(f' Story angles: {ch[\"story_angles_used\"]}')
print(f' Journalist: {ch.get(\"journalist_name\",\"not found\")} | {ch.get(\"journalist_beat\",\"\")}')
print()
print('=== TIER 2 CHANNELS ===')
for ch in patterns['tier_2_channels']:
print(f' {ch[\"channel_name\"]} ({ch[\"channel_type\"]}) -- found in {ch[\"found_in_competitors\"]}')
"
AI 指令 —— 零幻觉规则:
- 渠道名称: 仅包含
/tmp/cprf-pr-patterns.json中的渠道。不得虚构渠道。 - 记者/主持人姓名: 仅使用第 8 步中填入的姓名。若为空,写“搜索数据中未找到”。不得用训练知识替代。
- 报道角度: 仅使用从搜索结果中的文章/节目标题提取的角度。不得根据训练知识推断。
- 冷启动推介草稿: 必须提及 (a) 证据中的一个具体报道角度;(b) 产品分析中的至少一个具体差异化优势;(c) 如果已找到记者的报道领域,还需提及该领域。不得使用“我们很希望获得报道”或“我们的产品具有革命性”等泛泛表述。
- 渠道概述: 仅根据搜索摘要撰写 1-2 句。如果摘要没有描述该渠道的报道重点,写“搜索数据中未找到”。
- 额外切入点: 提供 3 个竞争对手报道中未采用的角度。这些角度必须基于产品在第 4 步中的实际差异化优势,而非泛泛建议。
- 不得使用 em dash。不得使用禁用词(powerful、seamless、game-changing、revolutionary、cutting-edge、leverage、transform)。
针对每个 Tier 1 渠道生成:
channel_overview:用 1-2 句概述报道重点(根据摘要)why_they_covered_competitors:从证据标题中提取的具体角度journalist_name+journalist_beatapproach_method:冷邮件/播客推介表单/社区发帖/LinkedIn 私信(根据渠道类型)cold_pitch_draft:subject:“[记者姓名]:[其报道领域] + [你的具体角度]”body:3-4 句。结构:引子(提及该记者之前对某个竞争对手的报道)+ 你们的产品/服务(1 句)+ 为什么符合其报道领域(关联到具体差异化优势)+ 请求(明确且便于回应的行动号召)
同时生成 bonus_hooks:提供 3 个搜索结果中没有任何竞争对手采用的推介角度。每个角度都应基于一项具体的产品差异化优势。
写入 /tmp/cprf-final.json:
python3 << 'PYEOF'
import json
result = {
"product_summary": {
# FILL from analysis
},
"competitors_researched": [], # FILL: names of confirmed competitors
"tier_1_deep_dives": [
# FILL per tier 1 channel:
# {
# "channel_name": str,
# "channel_type": str, # editorial / podcast / community
# "frequency": int,
# "found_in_competitors": [str],
# "evidence_urls": [str],
# "channel_overview": str,
# "why_they_covered_competitors": str,
# "story_angles_used": [str],
# "journalist_name": str,
# "journalist_beat": str,
# "approach_method": str,
# "cold_pitch_draft": {"subject": str, "body": str}
# }
],
"tier_2_channels": [
# FILL: {channel_name, channel_type, frequency, found_in_competitors, evidence_urls}
],
"tier_3_channels": [
# FILL: {channel_name, found_in_competitor}
],
"bonus_hooks": [
# FILL: 3 strings -- pitch angles not used by competitors
],
"data_quality_flags": []
}
json.dump(result, open('/tmp/cprf-final.json', 'w'), indent=2)
print(f'Synthesis written.')
print(f'Tier 1 deep dives: {len(result.get("tier_1_deep_dives", []))}')
print(f'Bonus hooks: {len(result.get("bonus_hooks", []))}')
PYEOF
第 10 步:自检、呈现并保存
自检:
python3 << 'PYEOF'
import json
result = json.load(open('/tmp/cprf-final.json'))
failures = []
# Check 1: em dashes
full_text = json.dumps(result)
if '—' in full_text:
result = json.loads(full_text.replace('—', '-'))
failures.append('Fixed: em dashes replaced with hyphens')
# Check 2: banned words
banned = ['powerful', 'seamless', 'innovative', 'game-changing', 'revolutionize',
'excited to announce', 'cutting-edge', 'best-in-class', 'world-class',
'leverage', 'transform', 'disrupt']
for word in banned:
if word.lower() in json.dumps(result).lower():
failures.append(f'Warning: banned word "{word}" found in output -- review before presenting')
# Check 3: cold pitch subjects exist
for dd in result.get('tier_1_deep_dives', []):
pitch = dd.get('cold_pitch_draft', {})
if not pitch.get('subject') or len(pitch.get('subject', '')) < 10:
dd['cold_pitch_draft']['subject'] = 'not generated'
failures.append(f'Fixed: missing subject line for {dd.get("channel_name")}')
if not pitch.get('body') or len(pitch.get('body', '')) < 50:
failures.append(f'Warning: very short pitch body for {dd.get("channel_name")}')
# Check 4: bonus hooks count
if len(result.get('bonus_hooks', [])) != 3:
failures.append(f'Expected 3 bonus hooks, got {len(result.get("bonus_hooks", []))}')
# Check 5: "not found in search data" count
nf_count = json.dumps(result).count('not found in search data')
if nf_count > 0:
failures.append(f'INFO: {nf_count} field(s) marked "not found in search data" -- verify before outreach')
# Check 6: tier 1 channels have evidence URLs
for ch in result.get('tier_1_deep_dives', []):
if not ch.get('evidence_urls'):
failures.append(f'Warning: {ch["channel_name"]} has no evidence_urls')
if 'data_quality_flags' not in result:
result['data_quality_flags'] = []
result['data_quality_flags'].extend(failures)
json.dump(result, open('/tmp/cprf-final.json', 'w'), indent=2)
print(f'QA complete. {len(failures)} issues addressed.')
for f in failures:
print(f' - {f}')
if not failures:
print('All QA checks passed.')
PYEOF
呈现输出:
## PR Intel: [product_name]
Date: [today] | Competitors researched: [N] | Tier 1 channels: [N] | Tier 2 channels: [N]
---
### Your Product
[one_line_description]
Differentiators: [list]
Competitors researched: [names]
---
### Tier 1 Channels (Proven Beats -- Found in 3+ Competitors)
*These channels have already covered multiple companies in your space.*
| Channel | Type | Found in | Journalist/Host | Approach |
|---|---|---|---|---|
[one row per tier 1 channel]
---
### Deep Dives + Cold Pitches
#### 1. [Channel Name] (Tier 1 -- [Type], found in [N] competitors)
Covers: [channel_overview]
Covered competitors: [found_in_competitors with evidence URLs]
Story angle they used: [why_they_covered_competitors]
Journalist/Host: [journalist_name] | Beat: [journalist_beat]
How to reach: [approach_method]
**Cold pitch:**
Subject: [subject]
[body -- 3-4 sentences]
---
[repeat for each tier 1 channel]
---
### Tier 2 Channels (Warm -- Found in 2 Competitors)
| Channel | Type | Found in | URL |
|---|---|---|---|
[one row per tier 2 channel]
---
### Tier 3 Channels (Discovery -- Found in 1 Competitor)
[comma-separated list of channel names with URLs]
---
### 3 Bonus Hooks (Angles Your Competitors Didn't Use)
1. [hook_text]
2. [hook_text]
3. [hook_text]
---
Data notes: [data_quality_flags, or "None"]
Saved to: docs/pr-intel/[PRODUCT_SLUG]-[DATE].md
保存到文件并清理:
DATE=$(date +%Y-%m-%d)
OUTPUT_FILE="docs/pr-intel/${PRODUCT_SLUG}-${DATE}.md"
mkdir -p docs/pr-intel
echo "Saved to: $OUTPUT_FILE"
rm -f /tmp/cprf-product-raw.md /tmp/cprf-product-analysis.json \
/tmp/cprf-competitors-raw.json /tmp/cprf-competitors-confirmed.json \
/tmp/cprf-pr-raw.json /tmp/cprf-pr-patterns.json \
/tmp/cprf-journalist-results.json /tmp/cprf-final.json
echo "Temp files cleaned up."