mcpField
W
where-your-customer-lives

where-your-customer-lives

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

关于 where-your-customer-lives

这项技能将理想客户画像的痛点信号追溯到具体线上社群,结合竞争对手讨论并按证据为渠道排序。它使用 Reddit、Hacker News、DuckDuckGo 及可选的 GitHub 数据,而非泛泛推荐渠道。

主要功能

  1. 01收集 ICP 痛点信号
  2. 02发现具体线上社群
  3. 03加入竞争对手讨论信号
  4. 04为渠道评分和排序
  5. 05生成证据型行动方案

适用场景

  1. 01为产品的 ICP 寻找社群
  2. 02依据实际讨论规划渠道切入方式
  3. 03比较不同客户画像的渠道优先级

安装

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

$npx skills add https://github.com/varnan-tech/opendirectory --skill where-your-customer-lives

安装后在 Claude Code 中输入/where-your-customer-lives确认已加载。

包含的文件

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

.env.example
README.md
evals/
evals.json
references/
channel-types.md
entry-tactics.md
scoring-guide.md
scripts/
fetch.py

详细介绍

客户身在何处

根据产品用途和 ICP,追溯真实的 ICP 痛点帖子,找到其来源社区。加入竞品讨论信号。通过 DuckDuckGo 发现 Slack/Discord/Newsletter/播客/会议渠道。根据 ICP 信号数量、规模、活跃度和竞品存在情况为每个渠道评分。输出排名后的行动手册:证据、切入策略、内容角度、反模式——每个渠道一份。禁止猜测。只列出经过信号追踪的渠道。


**关键规则:**输出中的每个渠道名称都必须出现在本次运行的 Reddit API 响应或 DuckDuckGo 搜索结果中。每个成员数量都必须来自 about.json API 或搜索摘要,绝不能估算。每个 ICP 信号数量都必须与原始数据一致。如果某种渠道类型返回 0 条结果,就报告 0,不要编造渠道。


常见错误

Agent 会想要……为什么这是错的
推荐泛泛的渠道(“LinkedIn”“Twitter”)每个渠道都必须具体到名称、成员数量和 URL。“LinkedIn 群组:DevOps for Enterprise Teams(45K 名成员)”——不能只写“LinkedIn”。
所有 ICP 都用相同的渠道信号追踪需针对具体 ICP。DevOps ICP 和 Finance ICP 得出的渠道列表截然不同。每个 ICP 都要重新运行脚本。
编造成员数量或社区名称每个渠道名称都必须来自 DuckDuckGo 结果或 Reddit API。每个成员数量都必须来自 API 或搜索摘要。如果查不到,就写“未找到成员数量”。
跳过竞品分析讨论竞品的地方 = 你的 ICP 正在评估替代方案 = 最适合开展外联的场景。即使用户没有要求,也要始终搜索竞品。
把产品推销当作切入策略“在 r/devops 发布关于你产品的内容”不是切入策略。切入策略要说明具体的帖子类型、内容形式和社区规范。
把 Reddit 当作唯一的渠道类型输出必须至少包含 3 种渠道类型。如果只找到 Reddit,就先明确通过 DuckDuckGo 搜索 Slack/Discord/Newsletter/会议,再停止。

第 1 步:检查设置

echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set -- competitor layer runs at 60 req/hr unauthenticated}"
echo ""
echo "Data sources this run will use:"
echo "  Reddit public JSON   (no auth, signal-trace)"
echo "  Reddit about.json    (no auth, subreddit metadata)"
echo "  HN Algolia API       (no auth, signal-trace)"
echo "  DuckDuckGo HTML      (no auth, channel discovery)"
echo "  GitHub API           (${GITHUB_TOKEN:+authenticated, }optional for competitor enrichment)"

如果未设置 GITHUB_TOKEN:继续。所有核心渠道发现功能均无需该令牌。


第 2 步:解析 ICP

从对话中收集:

  • product -- 产品的用途(一句话)
  • icp_role -- ICP 的身份(例如“技术联合创始人”“A 轮公司的 DevOps 工程师”)
  • icp_pain -- 他们的主要问题(例如“客户获取”“告警疲劳”)
  • category -- 市场类别关键词(例如“startup gtm sales”“devops monitoring”)
  • competitors -- 可选的竞品名称(例如“Clay、Apollo、HubSpot”)

ICP 逐级补充:

  1. 如果用户的提示中包含 product + icp_role + icp_pain:直接提取并继续。
  2. 如果提示信息不足(只有类别或只有产品名称):检查 docs/icp.md 中是否保存了 ICP 资料。与提示中的详细信息合并。
  3. 如果信息仍然不足(缺少 icp_role 或 icp_pain):逐一询问以下 3 个问题:
    • “用一句话说明你的产品是做什么的。”
    • “谁是你的理想客户?(职位、公司类型、团队规模)”
    • “他们找到你的产品之前,面临的主要问题是什么?”
  4. 将最终 ICP 保存到 docs/icp.md,以便其他技能复用。

如果 docs/icp.md 中尚未包含此产品,则保存 ICP 文件:

python3 << 'PYEOF'
import json, os

icp = {
    "product": "PRODUCT_HERE",
    "icp_role": "ICP_ROLE_HERE",
    "icp_pain": "ICP_PAIN_HERE",
    "competitors": ["COMP_1", "COMP_2"],
    "category": "CATEGORY_HERE"
}

os.makedirs("docs", exist_ok=True)
with open("/tmp/wcl-input.json", "w") as f:
    json.dump(icp, f, indent=2)

# Update docs/icp.md
icp_md_path = "docs/icp.md"
new_block = f"""## {icp['product']}
- **ICP role:** {icp['icp_role']}
- **ICP pain:** {icp['icp_pain']}
- **Competitors:** {', '.join(icp['competitors']) if icp['competitors'] else 'none'}
- **Category:** {icp['category']}
"""
existing = open(icp_md_path).read() if os.path.exists(icp_md_path) else ""
if icp['product'] not in existing:
    with open(icp_md_path, "a") as f:
        f.write(new_block)
    print(f"ICP saved to {icp_md_path}")
else:
    print(f"ICP already in {icp_md_path}")

print(f"Product: {icp['product']}")
print(f"ICP role: {icp['icp_role']}")
print(f"ICP pain: {icp['icp_pain']}")
print(f"Competitors: {', '.join(icp['competitors']) if icp['competitors'] else 'none'}")
PYEOF

第 3 步:运行独立的数据收集脚本

检查脚本是否存在:

ls scripts/fetch.py 2>/dev/null && echo "script available" || echo "not found"

运行渠道发现:

GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
    "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['category'])")" \
    --icp-role "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_role'])")" \
    --icp-pain "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_pain'])")" \
    --product "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['product'])")" \
    --competitors "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(','.join(d['competitors']))")" \
    --output /tmp/wcl-raw.json

等待完成(最多等待 5 分钟——Reddit + DuckDuckGo 搜索总计约需 120 秒)。

验证输出:

python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
    d = json.load(f)
print(f'Reddit posts found: {d[\"reddit_posts_found\"]}')
print(f'HN signals found:   {d[\"hn_signals_found\"]}')
print(f'Channels discovered: {d[\"summary\"][\"total_channels\"]}')
print(f'Top priority:        {len(d[\"summary\"][\"top_priority\"])}')
print(f'By type:             {d[\"summary\"][\"by_type\"]}')
print(f'Competitor layer ran: {d[\"summary\"][\"competitor_layer_ran\"]}')
"

如果 total_channels < 3:告知用户:“找到的渠道少于 3 个。ICP 描述可能过于狭窄,无法覆盖 Reddit/DDG。请尝试使用更宽泛的类别关键词,或添加竞品名称以启用竞品分析。”然后使用更宽泛的类别关键词重试一次,再停止。


步骤 4:打印频道摘要

加载原始数据并打印排名摘要表:

python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
    d = json.load(f)
channels = d['channels_discovered']
print(f'Channels found: {len(channels)}')
print()
print(f'{'#':<4} {'Channel':<35} {'Type':<14} {'Members':<12} {'ICP signals':<13} {'Score':<8} Tier')
print('-' * 100)
for i, ch in enumerate(channels[:15], 1):
    members = ch.get('members', 0)
    m_str = f'{members//1000}K' if members >= 1000 else str(members) if members else '?'
    print(f'{i:<4} {ch[\"name\"]:<35} {ch[\"type\"]:<14} {m_str:<12} {ch.get(\"icp_signal_count\",0):<13} {ch.get(\"channel_score\",0):<8} {ch.get(\"tier\",\"\")}')
"

打印得分最高频道中的前 3 条证据帖子:

python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
    d = json.load(f)
channels = d['channels_discovered']
if channels:
    top = channels[0]
    print(f'Top channel: {top[\"name\"]}')
    print(f'Evidence posts:')
    for ep in top.get('evidence_posts', [])[:3]:
        print(f'  [{ep.get(\"score\",0):.0f}] {ep.get(\"title\",\"\")}')
        print(f'       {ep.get(\"url\",\"\")}')
"

步骤 5:AI 频道补充信息

现在你已获得原始频道数据。针对优先级最高和高优先级中的每个频道,生成一条操作手册。

加载所有频道:

python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
    d = json.load(f)
top_channels = [ch for ch in d['channels_discovered'] if ch.get('tier') in ('top-priority', 'high')]
print(json.dumps(top_channels, indent=2))
"

针对上述每个频道,生成:

who_is_here: 用 2 句话描述该频道中具体的 ICP 类型。根据证据帖子、subreddit 描述和 ICP 画像推导。不要写“你的目标受众”——要具体。例如:“在员工人数为 50–500 人的公司任职、负责整个基础设施技术栈且没有专职 SRE 团队的 DevOps 工程师。他们会发帖讨论值班倦怠、Kubernetes 泛滥,以及在云原生和自托管可观测性方案之间如何选择。”

entry_tactic: 一种具体、可执行的切入方式。写明帖子类型、发帖形式和社区规范。不要写“参与社区互动”。例如:“找到每周一由自动版主发布的‘你正在做什么?’帖子。用 3 句话回复,介绍你解决的一个技术难题——出了什么问题、尝试了什么、最终如何解决。不要提及产品。先积累 karma,再单独发帖。”

content_angle: 根据证据帖的标题和得分,说明该频道中互动率最高的内容形式。例如:“在这里,技术复盘帖的互动量是产品公告的 5 倍。形式:‘我们将 20 万用户从 X 迁移到 Y——以下是出问题的地方及原因。’具体数字加上失败经验最容易获得赞同。”

anti_patterns: 列出 2–3 种会导致帖子被删除或损害声誉的具体行为。根据 subreddit 规则(如描述中有提供)和证据帖模式推导。例如:["在非推广帖中发布产品链接——版主会在几小时内删除", "不提供具体背景就问‘你们用什么工具?’——会被视为借市场调研套取信息"]

将补充后的操作手册写入 /tmp/wcl-channels.json:

{
  "playbook": [
    {
      "channel": "r/devops",
      "evidence": "34 ICP signals traced here, avg pain score 180",
      "who_is_here": "...",
      "entry_tactic": "...",
      "content_angle": "...",
      "anti_patterns": ["...", "..."]
    }
  ]
}
python3 -c "
import json
with open('/tmp/wcl-channels.json') as f:
    d = json.load(f)
print(f'Playbook entries: {len(d[\"playbook\"])}')
for p in d['playbook']:
    print(f'  {p[\"channel\"]}')
"

步骤 6:生成完整排名输出

将完整的排名操作手册写入 /tmp/wcl-output.json:

python3 << 'PYEOF'
import json
from datetime import datetime

with open('/tmp/wcl-input.json') as f:
    inp = json.load(f)
with open('/tmp/wcl-raw.json') as f:
    raw = json.load(f)
with open('/tmp/wcl-channels.json') as f:
    enriched = json.load(f)

playbook_by_channel = {p['channel']: p for p in enriched['playbook']}
channels = raw['channels_discovered']

output = {
    "date": raw['date'],
    "product": inp['product'],
    "icp_role": inp['icp_role'],
    "icp_pain": inp['icp_pain'],
    "competitors": inp.get('competitors', []),
    "total_channels": raw['summary']['total_channels'],
    "channels": []
}

for ch in channels:
    name = ch['name']
    playbook = playbook_by_channel.get(name, {})
    output['channels'].append({
        "rank": channels.index(ch) + 1,
        "name": name,
        "type": ch['type'],
        "url": ch['url'],
        "members": ch.get('members', 0),
        "active_users": ch.get('active_users', 0),
        "icp_signal_count": ch.get('icp_signal_count', 0),
        "competitor_mentions": ch.get('competitor_mentions', 0),
        "channel_score": ch.get('channel_score', 0),
        "tier": ch.get('tier', ''),
        "entry_type": ch.get('entry_type', 'open'),
        "evidence_posts": ch.get('evidence_posts', []),
        "who_is_here": playbook.get('who_is_here', ''),
        "entry_tactic": playbook.get('entry_tactic', ''),
        "content_angle": playbook.get('content_angle', ''),
        "anti_patterns": playbook.get('anti_patterns', []),
    })

with open('/tmp/wcl-output.json', 'w') as f:
    json.dump(output, f, indent=2)

print(f"Output written: /tmp/wcl-output.json")
print(f"Total channels: {len(output['channels'])}")
PYEOF

步骤 7:自检

python3 -c "
import json

with open('/tmp/wcl-raw.json') as f:
    raw = json.load(f)
with open('/tmp/wcl-output.json') as f:
    output = json.load(f)

full_text = json.dumps(output)
raw_channel_names = {ch['name'].lower() for ch in raw['channels_discovered']}
passes = 0
fails = 0

# Check 1: No em dashes
if chr(8212) in full_text:
    print('FAIL: em dash found in output -- replace with hyphen')
    fails += 1
else:
    print('PASS: no em dashes')
    passes += 1

# Check 2: No banned words
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
          'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
    print(f'FAIL: banned words: {found}')
    fails += 1
else:
    print('PASS: no banned words')
    passes += 1

# Check 3: At least 3 channel types
types = {ch['type'] for ch in output['channels']}
if len(types) < 3:
    print(f'FAIL: only {len(types)} channel type(s) in output: {types}')
    fails += 1
else:
    print(f'PASS: {len(types)} channel types: {types}')
    passes += 1

# Check 4: All channel names exist in raw data
for ch in output['channels']:
    if ch['name'].lower() not in raw_channel_names:
        print(f'FAIL: channel not in raw data: {ch[\"name\"]}')
        fails += 1

if fails == 0:
    print('PASS: all channel names verified in raw data')
    passes += 1

# Check 5: No generic entry tactics
generic_phrases = ['engage with the community', 'post about your product', 'share your content']
for ch in output['channels']:
    tactic = ch.get('entry_tactic', '').lower()
    for phrase in generic_phrases:
        if phrase in tactic:
            print(f'FAIL: generic entry tactic in {ch[\"name\"]}: contains \"{phrase}\"')
            fails += 1

if fails == 0:
    print('PASS: entry tactics are channel-specific')

print()
print(f'Result: {passes} passed, {fails} failed')
if fails > 0:
    print('Fix failures before saving.')
else:
    print('All checks passed. Ready to save.')
"

继续执行步骤 8 前,修复所有未通过项。


步骤 8:保存输出并清理

python3 << 'PYEOF'
import json, os, re
from datetime import datetime

with open('/tmp/wcl-input.json') as f:
    inp = json.load(f)
with open('/tmp/wcl-raw.json') as f:
    raw = json.load(f)
with open('/tmp/wcl-output.json') as f:
    output = json.load(f)

slug = re.sub(r'[^a-z0-9]+', '-', (inp.get('icp_role') or inp['category']).lower()).strip('-')[:40]
date = datetime.now().strftime('%Y-%m-%d')
os.makedirs('docs/channel-map', exist_ok=True)

outpath_md = f"docs/channel-map/{slug}-{date}.md"
outpath_json = f"docs/channel-map/{slug}-{date}.json"

channels = output['channels']
by_type = {}
for ch in channels:
    by_type.setdefault(ch['type'], []).append(ch)

lines = [
    f"# Where Your Customer Lives: {inp['product'] or inp['category'].title()}",
    f"ICP: {inp['icp_role']} | Date: {date} | Channels found: {len(channels)}",
    "",
    "---",
    "",
    "## Channel Ranking",
    "",
]

tier_labels = {"top-priority": "TOP PRIORITY", "high": "HIGH", "medium": "MEDIUM", "low": "LOW"}

for ch in channels:
    members = ch.get('members', 0)
    m_str = f"{members//1000}K" if members >= 1000 else str(members) if members else "member count not found"
    tier_label = tier_labels.get(ch.get('tier', ''), ch.get('tier', '').upper())
    
    lines.append(f"### #{ch['rank']}: {ch['name']} [score: {ch['channel_score']}] -- {tier_label}")
    
    active = ch.get('active_users', 0)
    active_str = f" | Active: {active//1000}K/day" if active >= 1000 else f" | Active: {active}/day" if active else ""
    lines.append(f"Type: {ch['type'].title()} | Members: {m_str}{active_str} | {ch.get('entry_type', 'open').title()} to join")
    
    evidence_str = f"{ch['icp_signal_count']} ICP signals traced here" if ch['icp_signal_count'] > 0 else "Discovered via DuckDuckGo search"
    lines.append(f"Evidence: {evidence_str}")
    
    if ch.get('competitor_mentions', 0) > 0 and inp.get('competitors'):
        lines.append(f"Competitor mentions: {ch['competitor_mentions']} across {', '.join(inp['competitors'][:3])}")
    
    lines.append("")
    
    if ch.get('who_is_here'):
        lines.append(f"**Who is here:** {ch['who_is_here']}")
        lines.append("")
    
    if ch.get('entry_tactic'):
        lines.append(f"**Entry tactic:** {ch['entry_tactic']}")
        lines.append("")
    
    if ch.get('content_angle'):
        lines.append(f"**Content angle:** {ch['content_angle']}")
        lines.append("")
    
    if ch.get('anti_patterns'):
        lines.append("**Anti-patterns:**")
        for ap in ch['anti_patterns']:
            lines.append(f"- {ap}")
        lines.append("")
    
    lines.append("---")
    lines.append("")

lines += [
    "## Channel Summary by Type",
    "",
    "| Type | Count | Best channel | Score |",
    "|---|---|---|---|",
]
for ch_type, chs in sorted(by_type.items(), key=lambda x: -max(c['channel_score'] for c in x[1])):
    best = max(chs, key=lambda x: x['channel_score'])
    lines.append(f"| {ch_type.title()} | {len(chs)} | {best['name']} | {best['channel_score']} |")

lines += [
    "",
    "---",
    "",
    "## Data Quality Notes",
    f"- All channel names exist in Reddit API response or DuckDuckGo search results",
    f"- Member counts from Reddit about.json API or search snippets",
    f"- ICP signal counts match raw data ({raw['reddit_posts_found']} Reddit posts, {raw['hn_signals_found']} HN signals)",
    f"- Competitor layer ran: {raw['summary']['competitor_layer_ran']}",
    f"- Sources: Reddit signal-trace, HN signal-trace, DuckDuckGo channel discovery",
    "",
    f"Saved to: {outpath_md}",
    f"JSON snapshot: {outpath_json}",
]

with open(outpath_md, 'w') as f:
    f.write('\n'.join(lines))

# JSON snapshot
snapshot = {
    "input": inp,
    "channels": channels,
    "summary": raw['summary'],
    "date": date,
}
with open(outpath_json, 'w') as f:
    json.dump(snapshot, f, indent=2)

print(f"Report saved: {outpath_md}")
print(f"JSON snapshot: {outpath_json}")
PYEOF

清理临时文件:

rm -f /tmp/wcl-input.json /tmp/wcl-raw.json /tmp/wcl-channels.json /tmp/wcl-output.json
echo "Done. Channel map saved to docs/channel-map/"

向用户展示已保存的 .md 文件的完整内容。

评论

支持 Markdown