mcpField
C
competitor-pr-finder

competitor-pr-finder

github.com/varnan-tech营销与增长68745 次安装2 周前更新
代码仓库

关于 competitor-pr-finder

competitor-pr-finder 是一项根据产品网址或描述研究竞品公关渠道的技能。它以 Tavily 搜索结果和产品页面为依据,统计渠道出现频次并起草定制推介。

主要功能

  1. 01发现竞品并等待确认
  2. 02研究媒体、播客和社区渠道
  3. 03按竞品出现频次划分渠道
  4. 04依据搜索证据查找记者和主持人
  5. 05结合产品差异点起草推介

适用场景

  1. 01查找竞品使用过的公关渠道
  2. 02优先关注多个竞品出现的媒体
  3. 03为一级渠道准备有证据支持的推介

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/varnan-tech/opendirectory --skill competitor-pr-finder

安装后在 Claude Code 中输入/competitor-pr-finder确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.env.example
README.md
evals/
evals.json
references/
pitch-guide.md
pr-channel-types.md
tier-scoring.md
scripts/
research.py

详细介绍

竞争对手 PR 发现器

提供你的产品 URL。它会找到你的竞争对手,调查他们使用的每个 PR 渠道(新闻、播客、社区),找出多个竞争对手都出现过的渠道(你已验证有效的目标),为每个渠道找到记者或主持人,并为每个 Tier 1 渠道撰写针对你产品的个性化冷邮件。


零幻觉政策: 输出中的每个渠道、记者姓名、报道角度和邮件细节都必须能追溯到具体的 Tavily 搜索结果或抓取的产品页面。具体要求:

  • 竞争对手名称:必须出现在 Tavily 搜索结果中,不得来自 AI 训练知识
  • 渠道名称:必须在搜索结果中附带 URL
  • 记者/主持人姓名:必须在 Tavily 摘要中原文出现
  • 报道角度:只能从搜索结果中的文章/节目标题提取
  • 邮件草稿:必须引用搜索数据中的具体证据和产品分析

常见错误

Agent 可能会想……为什么不对
根据训练知识说出记者姓名每个记者姓名都必须能追溯到搜索结果摘要。凭记忆写出“Sarah Perez 在 TechCrunch 负责报道初创公司”就是幻觉。
列出没有证据 URL 的渠道输出中的每个渠道都必须至少有一个 PR 搜索结果 URL,证明某个竞争对手曾在该渠道出现。
跳过竞争对手确认步骤始终先展示发现的竞争对手,并等待用户确认。竞争对手找错了,搜索就白费了,输出也毫无用处。
生成泛泛的邮件(“我们很希望能获得报道”)每封邮件都必须引用证据中的具体角度,以及产品分析中的具体差异化优势。
仅凭 1 个竞争对手的出现就将渠道标记为 Tier 1Tier 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 家竞品公司,要求:

  1. 在搜索结果标题、回答或摘要中被提及
  2. 与正在分析的产品属于同一个 L3 细分领域
  3. 是实际的竞品(不是代理机构、咨询公司或列表文章)
  4. 彼此不同(不是同一家公司使用不同名称)

为每家竞品填写: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 指令 —— 零幻觉规则:

  1. 渠道名称: 仅包含 /tmp/cprf-pr-patterns.json 中的渠道。不得虚构渠道。
  2. 记者/主持人姓名: 仅使用第 8 步中填入的姓名。若为空,写“搜索数据中未找到”。不得用训练知识替代。
  3. 报道角度: 仅使用从搜索结果中的文章/节目标题提取的角度。不得根据训练知识推断。
  4. 冷启动推介草稿: 必须提及 (a) 证据中的一个具体报道角度;(b) 产品分析中的至少一个具体差异化优势;(c) 如果已找到记者的报道领域,还需提及该领域。不得使用“我们很希望获得报道”或“我们的产品具有革命性”等泛泛表述。
  5. 渠道概述: 仅根据搜索摘要撰写 1-2 句。如果摘要没有描述该渠道的报道重点,写“搜索数据中未找到”。
  6. 额外切入点: 提供 3 个竞争对手报道中未采用的角度。这些角度必须基于产品在第 4 步中的实际差异化优势,而非泛泛建议。
  7. 不得使用 em dash。不得使用禁用词(powerful、seamless、game-changing、revolutionary、cutting-edge、leverage、transform)。

针对每个 Tier 1 渠道生成:

  • channel_overview:用 1-2 句概述报道重点(根据摘要)
  • why_they_covered_competitors:从证据标题中提取的具体角度
  • journalist_name + journalist_beat
  • approach_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."

评论

支持 Markdown