mcpField
O
ops-marketing

ops-marketing

github.com/davepoon营销与增长3.7k70 次安装今天更新
代码仓库

关于 ops-marketing

ops-marketing 是用于查看营销表现并管理营销活动的 Claude skill,覆盖邮件、广告、分析、SEO 和社交渠道。它提供分渠道报告,并将请求导向相应的数据面板或管理流程。

主要功能

  1. 01Klaviyo 邮件指标
  2. 02Meta Ads 表现与管理
  3. 03Google Ads 面板和广告活动
  4. 04GA4 分析与归因
  5. 05Search Console 与社交报告

适用场景

  1. 01查看订阅者、邮件活动和流程情况
  2. 02检查广告支出与活动表现
  3. 03分析网站会话、转化和搜索指标
  4. 04按渠道处理营销工作

安装

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

$npx skills add https://github.com/davepoon/buildwithclaude --skill ops-marketing

安装后在 Claude Code 中输入/ops-marketing确认已加载。

详细介绍

OPS ► 营销指挥中心

运行时上下文

执行前,加载可用上下文:

  1. 偏好设置:读取 ${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json

    • timezone — 正确显示所有时间戳
    • klaviyo_private_key、meta_ads_token、meta_ad_account_id、ga4_property_id、google_search_console_site — 在检查环境变量前,先检查 userConfig 中的键
    • google_ads_developer_token、google_ads_client_id、google_ads_client_secret、google_ads_refresh_token、google_ads_customer_id、google_ads_login_customer_id — Google Ads 凭据
  2. 守护进程健康状态:读取 ${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json

    • 如果 action_needed 不为 null → 在运行任何渠道查询前展示该内容
  3. 密钥:通过 userConfig → 环境变量 → Doppler MCP(mcp__doppler__*)→ Doppler CLI 回退方案解析 API 密钥(参见下文“凭据解析”部分)

CLI/API 参考

Klaviyo REST API
端点方法描述
https://a.klaviyo.com/api/lists/?fields[list]=name,id,profile_countGET所有列表 + 订阅者数量
https://a.klaviyo.com/api/campaigns/?filter=equals(messages.channel,'email')&sort=-created_atGET最近的营销活动
https://a.klaviyo.com/api/flows/?filter=equals(status,'live')GET活跃流程
https://a.klaviyo.com/api/metrics/GET可用指标

身份验证标头:Authorization: Klaviyo-API-Key ${KLAVIYO_KEY} | 版本标头:revision: 2024-10-15

Meta Graph API
端点方法描述
https://graph.facebook.com/v18.0/${META_ACCOUNT}/insights?fields=spend,...&date_preset=last_7dGET账户级广告支出
https://graph.facebook.com/v18.0/${META_ACCOUNT}/campaigns?fields=name,status,insights{...}GET营销活动明细
https://graph.facebook.com/v18.0/me/accounts?fields=instagram_business_accountGET已关联的 Instagram 账户

身份验证标头:Authorization: Bearer ${META_TOKEN}

Google Analytics 4(Data API)
端点方法描述
https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReportPOST运行自定义报告

身份验证:gcloud ADC — GA4_TOKEN=$(gcloud auth application-default print-access-token)

Google Search Console
端点方法描述
https://searchconsole.googleapis.com/webmasters/v3/sites/${GSC_SITE_ENCODED}/searchAnalytics/queryPOST搜索效果数据

身份验证:与 GA4 相同的 gcloud ADC 令牌

Agent Teams 支持

如果设置了 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1,则在并行收集渠道数据时使用 Agent Teams。这可以:

  • 让 Agent 共享上下文,并在执行过程中协调
  • 让你实时调整优先级
  • 让 Agent 在完成任务时报告进度

团队设置(仅在启用标志时):

TeamCreate("marketing-team")
Agent(team_name="marketing-team", name="email-metrics", prompt="Pull Klaviyo subscriber counts, campaign stats, and flow metrics")
Agent(team_name="marketing-team", name="ads-metrics", prompt="Pull Meta Ads spend, ROAS, and campaign breakdown")
Agent(team_name="marketing-team", name="analytics-metrics", prompt="Pull GA4 sessions, conversions, and traffic sources")
Agent(team_name="marketing-team", name="seo-metrics", prompt="Pull Search Console clicks, impressions, and top queries")

如果未设置该标志,则使用标准的即发即弃子 Agent。

凭据解析

按以下顺序解析各服务的凭据:

Klaviyo
KLAVIYO_KEY="${KLAVIYO_PRIVATE_KEY:-$(claude plugin config get klaviyo_private_key 2>/dev/null)}"
if [ -z "$KLAVIYO_KEY" ]; then
  KLAVIYO_KEY="$(doppler secrets get KLAVIYO_PRIVATE_KEY --plain 2>/dev/null)"
fi
Meta Ads
META_TOKEN="${META_ADS_TOKEN:-$(claude plugin config get meta_ads_token 2>/dev/null)}"
META_ACCOUNT="${META_AD_ACCOUNT_ID:-$(claude plugin config get meta_ad_account_id 2>/dev/null)}"
if [ -z "$META_TOKEN" ]; then
  META_TOKEN="$(doppler secrets get META_ADS_TOKEN --plain 2>/dev/null)"
fi
GA4
GA4_PROPERTY="${GA4_PROPERTY_ID:-$(claude plugin config get ga4_property_id 2>/dev/null)}"
# GA4 uses gcloud application default credentials — check if configured:
gcloud auth application-default print-access-token 2>/dev/null
Google Search Console
GSC_SITE="${GOOGLE_SEARCH_CONSOLE_SITE:-$(claude plugin config get google_search_console_site 2>/dev/null)}"
# Uses same gcloud ADC as GA4
Google Ads
GADS_API_VERSION="v23"
GADS_DEV_TOKEN="${GOOGLE_ADS_DEVELOPER_TOKEN:-$(claude plugin config get google_ads_developer_token 2>/dev/null)}"
GADS_CLIENT_ID="${GOOGLE_ADS_CLIENT_ID:-$(claude plugin config get google_ads_client_id 2>/dev/null)}"
GADS_CLIENT_SECRET="${GOOGLE_ADS_CLIENT_SECRET:-$(claude plugin config get google_ads_client_secret 2>/dev/null)}"
GADS_REFRESH_TOKEN="${GOOGLE_ADS_REFRESH_TOKEN:-$(claude plugin config get google_ads_refresh_token 2>/dev/null)}"
GADS_CUSTOMER_ID="${GOOGLE_ADS_CUSTOMER_ID:-$(claude plugin config get google_ads_customer_id 2>/dev/null)}"
GADS_LOGIN_CUSTOMER_ID="${GOOGLE_ADS_LOGIN_CUSTOMER_ID:-$(claude plugin config get google_ads_login_customer_id 2>/dev/null)}"

# Doppler fallback
if [ -z "$GADS_REFRESH_TOKEN" ]; then
  GADS_REFRESH_TOKEN="$(doppler secrets get GOOGLE_ADS_REFRESH_TOKEN --plain 2>/dev/null)"
fi
if [ -z "$GADS_DEV_TOKEN" ]; then
  GADS_DEV_TOKEN="$(doppler secrets get GOOGLE_ADS_DEVELOPER_TOKEN --plain 2>/dev/null)"
fi

# Strip dashes from customer ID (API requires no dashes)
GADS_CUSTOMER_ID="${GADS_CUSTOMER_ID//-/}"

# Refresh access token (expires in ~1 hour — always refresh before API calls)
GADS_ACCESS_TOKEN=$(curl -s -X POST https://oauth2.googleapis.com/token \
  --data "client_id=${GADS_CLIENT_ID}" \
  --data "client_secret=${GADS_CLIENT_SECRET}" \
  --data "refresh_token=${GADS_REFRESH_TOKEN}" \
  --data "grant_type=refresh_token" | jq -r '.access_token')

# Common headers for all Google Ads API calls
GADS_HEADERS=(-H "Content-Type: application/json" -H "Authorization: Bearer ${GADS_ACCESS_TOKEN}" -H "developer-token: ${GADS_DEV_TOKEN}")
if [ -n "$GADS_LOGIN_CUSTOMER_ID" ]; then
  GADS_HEADERS+=(-H "login-customer-id: ${GADS_LOGIN_CUSTOMER_ID}")
fi

子命令路由

将 $ARGUMENTS 路由到以下相应部分:

输入操作
(空),dashboard运行完整营销仪表板
email,klaviyoKlaviyo 邮件指标
ads,metaMeta Ads 表现(只读概览)
meta-manage,meta create-campaign,meta target,meta creative,meta rules,meta audiences,meta advantageMeta Ads 广告系列管理(参见 ## meta-manage 部分)
google-ads,gadsGoogle Ads 仪表板 + 广告系列管理(参见 ## google-ads 部分)
analytics,ga4GA4 会话 + 转化
ga4 realtime,ga4 funnel,ga4 cohort,ga4 audience,ga4 pivotGA4 高级分析(参见 ## ga4-advanced 部分)
seo,gscSearch Console 指标
social社交媒体聚合器
instagram,instagram post,instagram reel,instagram story,instagram insights,instagram demographicsInstagram 发布 + 洞察(参见 ## instagram 部分)
campaigns跨渠道广告系列概览(所有平台)
optimize跨平台广告优化 Agent
attribution统一归因表(Meta + Google + Klaviyo + GA4)
setup配置 API 密钥

email / klaviyo

提取最近 30 天的 Klaviyo 指标。

订阅者数量
curl -s "https://a.klaviyo.com/api/lists/?fields[list]=name,id,profile_count" \
  -H "Authorization: Klaviyo-API-Key ${KLAVIYO_KEY}" \
  -H "revision: 2024-10-15" | jq '.data[] | {name: .attributes.name, id: .id, count: .attributes.profile_count}'
最近的广告系列(最近 10 个)
curl -s "https://a.klaviyo.com/api/campaigns/?filter=equals(messages.channel,'email')&sort=-created_at&page[size]=10&fields[campaign]=name,status,created_at,send_time" \
  -H "Authorization: Klaviyo-API-Key ${KLAVIYO_KEY}" \
  -H "revision: 2024-10-15" | jq '.data[] | {name: .attributes.name, status: .attributes.status, sent: .attributes.send_time}'
工作流指标(活跃工作流)
curl -s "https://a.klaviyo.com/api/flows/?filter=equals(status,'live')&fields[flow]=name,status,created,trigger_type" \
  -H "Authorization: Klaviyo-API-Key ${KLAVIYO_KEY}" \
  -H "revision: 2024-10-15" | jq '.data[] | {name: .attributes.name, trigger: .attributes.trigger_type}'
关键邮件指标(通过指标聚合计算打开、点击、收入)
# Get metric IDs first
curl -s "https://a.klaviyo.com/api/metrics/" \
  -H "Authorization: Klaviyo-API-Key ${KLAVIYO_KEY}" \
  -H "revision: 2024-10-15" | jq '.data[] | select(.attributes.name | test("Opened Email|Clicked Email|Placed Order")) | {name: .attributes.name, id: .id}'
输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 EMAIL (KLAVIYO) — last 30d
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Lists:        [list_name] — [N] subscribers
 Campaigns:    [N sent] | [N drafts]
 Active Flows: [N]

 RECENT CAMPAIGNS
 [name]  [status]  sent [date]
 ...

ads / meta

提取已配置广告账户的 Meta Ads 洞察。

账户级支出(最近 7 天)
curl -s "https://graph.facebook.com/v18.0/${META_ACCOUNT}/insights?fields=spend,impressions,clicks,ctr,cpc,actions,action_values&date_preset=last_7d&level=account" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq '{spend: .data[0].spend, impressions: .data[0].impressions, clicks: .data[0].clicks, ctr: .data[0].ctr, cpc: .data[0].cpc}'
广告系列明细(最近 7 天)
curl -s "https://graph.facebook.com/v18.0/${META_ACCOUNT}/campaigns?fields=name,status,daily_budget,lifetime_budget,insights{spend,impressions,clicks,actions,action_values}&date_preset=last_7d" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq '.data[] | {name: .name, status: .status, spend: .insights.data[0].spend}'
ROAS 计算

从 action_values 数组中提取 action_type == "purchase" 的值,再除以支出。

表现最佳的广告(最近 7 天)
curl -s "https://graph.facebook.com/v18.0/${META_ACCOUNT}/ads?fields=name,adset_id,insights{spend,impressions,clicks,actions,action_values,ctr,cpc}&date_preset=last_7d&limit=10" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq '.data | sort_by(.insights.data[0].spend | tonumber) | reverse | .[0:5] | .[] | {name: .name, spend: .insights.data[0].spend, ctr: .insights.data[0].ctr}'
输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 META ADS — last 7d
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Spend:       $[X]
 ROAS:        [X]x
 Purchases:   [N]  ($[X] revenue)
 Impressions: [N]  CTR: [X]%
 CPC:         $[X]

 CAMPAIGNS
 [name]  [status]  $[spend]  [roas]x ROAS
 ...

 TOP ADS (by spend)
 [name]  $[spend]  [ctr]% CTR

meta-manage

完整的 Meta Ads 广告系列管理。使用与只读 ads 部分相同的 META_TOKEN 和 META_ACCOUNT 凭据。

凭据检查:如果 META_TOKEN 为空,则打印 Meta Ads not configured. Run /ops:marketing setup. 并停止。

在 meta-manage 中根据 $ARGUMENTS 路由:

输入操作
create-campaign创建新的广告系列(始终为 PAUSED)
target <ADSET_ID>配置广告组定向
creative <CAMPAIGN_ID>上传图片 + 创建带文案的广告
rules列出 / 创建自动化规则
audiences创建自定义受众或类似受众
advantage创建由 AI 优化的 Advantage+ 广告系列
create-campaign

通过 AskUserQuestion 收集信息(每次调用最多 4 个选项):

  1. 广告系列目标 — [OUTCOME_TRAFFIC, OUTCOME_SALES, OUTCOME_LEADS, OUTCOME_AWARENESS]
  2. 每日预算(美元,自由文本)
  3. 广告系列名称(自由文本)

然后通过 AskUserQuestion 确认:"Create Meta campaign '<NAME>' with $<BUDGET>/day budget?" 选项 [Create, Cancel]

BUDGET_CENTS=$(awk "BEGIN {printf \"%d\", ${BUDGET_DOLLARS} * 100}")
curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/campaigns" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "name=${CAMPAIGN_NAME}" \
  -F "objective=${OBJECTIVE}" \
  -F "status=PAUSED" \
  -F "special_ad_categories=[]" \
  -F "daily_budget=${BUDGET_CENTS}" | jq '{id: .id, error: .error.message}'

打印:Campaign "${CAMPAIGN_NAME}" created (ID: <ID>, status: PAUSED, budget: $<BUDGET>/day). Enable via Meta Ads Manager or add ad sets first.

如果出错,则打印错误消息。

target <ADSET_ID>

配置现有广告组的定向。通过 AskUserQuestion 收集信息:

  1. 目标国家/地区(以逗号分隔的 ISO 代码,例如 US,CA,GB)— 自由文本
  2. 年龄范围:[18-34, 25-54, 35-65, 18-65]
  3. 性别:[All, Men only, Women only, Skip]
# Build geo_locations JSON
GEO_JSON=$(echo "$COUNTRIES" | tr ',' '\n' | jq -Rc '.' | jq -sc '{"countries": .}')

# Build targeting spec
TARGETING_JSON=$(jq -n \
  --argjson geo "$GEO_JSON" \
  --arg age_min "$AGE_MIN" \
  --arg age_max "$AGE_MAX" \
  '{
    geo_locations: $geo,
    age_min: ($age_min | tonumber),
    age_max: ($age_max | tonumber)
  }')

# Add gender filter if requested
if [ "$GENDER" = "Men only" ]; then
  TARGETING_JSON=$(echo "$TARGETING_JSON" | jq '. + {"genders": [1]}')
elif [ "$GENDER" = "Women only" ]; then
  TARGETING_JSON=$(echo "$TARGETING_JSON" | jq '. + {"genders": [2]}')
fi

curl -s -X POST "https://graph.facebook.com/v20.0/${ADSET_ID}" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "targeting=${TARGETING_JSON}" | jq '{success: .success, error: .error.message}'

打印:Ad set ${ADSET_ID} targeting updated: ${COUNTRIES}, ages ${AGE_MIN}-${AGE_MAX}${GENDER_LABEL}.

creative <CAMPAIGN_ID>

上传图片并创建广告。通过 AskUserQuestion 收集信息:

  1. 图片文件路径或 URL(自由文本)
  2. 要关联该广告的广告组 ID(自由文本)
  3. 正文(广告文案,自由文本 — 建议最多 125 个字符)

然后通过第二次 AskUserQuestion 收集标题(自由文本,最多 40 个字符)。

# Step 1: Upload image
if [[ "$IMAGE_INPUT" == http* ]]; then
  # Upload by URL
  UPLOAD_RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/adimages" \
    -H "Authorization: Bearer ${META_TOKEN}" \
    -F "url=${IMAGE_INPUT}")
else
  # Upload by file (multipart)
  UPLOAD_RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/adimages" \
    -H "Authorization: Bearer ${META_TOKEN}" \
    -F "filename=@${IMAGE_INPUT}")
fi
IMAGE_HASH=$(echo "$UPLOAD_RESP" | jq -r '.images | to_entries[0].value.hash // empty')

if [ -z "$IMAGE_HASH" ]; then
  echo "Image upload failed: $(echo "$UPLOAD_RESP" | jq -r '.error.message // "unknown error"')"
  exit 0
fi

# Resolve the Facebook Page ID. Meta's `object_story_spec.page_id` requires a
# real Page ID — the ad account ID (with `act_` stripped) is NOT a Page ID and
# the API call will fail. Require META_PAGE_ID in env or plugin prefs.
META_PAGE_ID="${META_PAGE_ID:-$(claude plugin config get meta_page_id 2>/dev/null || echo "")}"
if [ -z "$META_PAGE_ID" ]; then
  echo "META_PAGE_ID is required to create an ad creative. Set it via:"
  echo "  claude plugin config set meta_page_id <your_fb_page_id>"
  echo "Find your Page ID at https://www.facebook.com/<your-page>/about_profile_transparency"
  exit 0
fi

# Step 2: Create ad creative
CREATIVE_RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/adcreatives" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "name=Creative for ${AD_NAME}" \
  -F "object_story_spec={\"page_id\": \"${META_PAGE_ID}\", \"link_data\": {\"image_hash\": \"${IMAGE_HASH}\", \"message\": \"${PRIMARY_TEXT}\", \"name\": \"${HEADLINE}\"}}")
CREATIVE_ID=$(echo "$CREATIVE_RESP" | jq -r '.id // empty')

if [ -z "$CREATIVE_ID" ]; then
  echo "Creative creation failed: $(echo "$CREATIVE_RESP" | jq -r '.error.message // "unknown error"')"
  exit 0
fi

# Step 3: Create ad (status PAUSED — Rule 5)
curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/ads" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "name=${AD_NAME}" \
  -F "adset_id=${ADSET_ID}" \
  -F "creative={\"creative_id\": \"${CREATIVE_ID}\"}" \
  -F "status=PAUSED" | jq '{id: .id, error: .error.message}'

打印:Ad created (ID: <ID>, creative: <CREATIVE_ID>, status: PAUSED). Enable via Meta Ads Manager when ready.

规则

列出现有规则或创建新的自动化规则。

列出规则:

curl -s "https://graph.facebook.com/v20.0/${META_ACCOUNT}/adrules_library?fields=name,status,evaluation_spec,execution_spec" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq '.data[] | {id: .id, name: .name, status: .status}'

创建规则(通过 AskUserQuestion 提示):

  1. 规则类型:[Pause low performers, Scale winners, Increase budget, Decrease budget]

对于“暂停表现不佳的广告”:

# Pause ads where CPA > $50 and spend > $20 in last 7 days
curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/adrules_library" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pause high CPA ads",
    "schedule_spec": {"schedule_type": "SEMI_HOURLY"},
    "evaluation_spec": {
      "evaluation_type": "SCHEDULE",
      "filters": [
        {"field": "cost_per_result", "value": [50], "operator": "GREATER_THAN"},
        {"field": "spent", "value": [20], "operator": "GREATER_THAN"},
        {"field": "entity_type", "value": ["AD"], "operator": "EQUAL"},
        {"field": "time_preset", "value": ["LAST_7_DAYS"], "operator": "EQUAL"}
      ]
    },
    "execution_spec": {
      "execution_type": "PAUSE"
    },
    "status": "ENABLED"
  }' | jq '{id: .id, error: .error.message}'

对于“扩大赢家投放”:

⚠️ 请将规则限定为拓客广告组。 单独使用 purchase_roas > 3 筛选条件会自动扩大再营销广告组的投放规模——这类广告组的 ROAS 会因面向熟悉品牌的受众进行需求捕获而虚高(这些转化即使不投放广告也会发生),并非来自增量增长。对它们一概扩大投放,会把预算投入已有需求,同时挤占拓客预算,一个月后漏斗就会收缩。请添加广告组名称/受众筛选条件,排除再营销(或将规则限制为拓客广告组),并在仅依据 ROAS 扩大投放前,通过留出组确认赢家广告组带来的提升。

# Increase budget 20% for ad sets with ROAS > 3x in last 7 days
# NOTE: restrict to prospecting ad sets — see caveat above
curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/adrules_library" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Scale winning ad sets",
    "schedule_spec": {"schedule_type": "DAILY"},
    "evaluation_spec": {
      "evaluation_type": "SCHEDULE",
      "filters": [
        {"field": "purchase_roas", "value": [3], "operator": "GREATER_THAN"},
        {"field": "entity_type", "value": ["ADSET"], "operator": "EQUAL"},
        {"field": "time_preset", "value": ["LAST_7_DAYS"], "operator": "EQUAL"}
      ]
    },
    "execution_spec": {
      "execution_type": "INCREASE_BUDGET",
      "execution_options": [{"field": "budget_value", "value": "20", "operator": "PERCENTAGE"}]
    },
    "status": "ENABLED"
  }' | jq '{id: .id, error: .error.message}'

输出:规则已创建(ID:<ID>)。每半小时运行一次,并会自动暂停 CPA > $50 的广告。

受众

创建自定义受众或类似受众。

通过 AskUserQuestion 提示:

  1. 受众类型:[Custom — website, Custom — customer list, Lookalike, Skip]

自定义受众 — 网站(基于 Pixel):

curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/customaudiences" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Website visitors — last 30 days",
    "subtype": "WEBSITE",
    "retention_days": 30,
    "rule": {"inclusions": {"operator": "or", "rules": [{"event_sources": [{"id": "<PIXEL_ID>", "type": "pixel"}], "retention_seconds": 2592000, "filter": {"operator": "and", "filters": [{"field": "event", "operator": "eq", "value": "PageView"}]}}]}}
  }' | jq '{id: .id, name: .name, error: .error.message}'

注意:将 <PIXEL_ID> 替换为 Meta Events Manager 中的实际像素 ID。

类似受众(需要至少有 100 个匹配个人资料的来源受众):

# Prompt for origin audience ID via AskUserQuestion (free text)
curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/customaudiences" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Lookalike — ${ORIGIN_AUDIENCE_NAME} 1%\",
    \"subtype\": \"LOOKALIKE\",
    \"origin_audience_id\": \"${ORIGIN_AUDIENCE_ID}\",
    \"lookalike_spec\": {
      \"country\": \"US\",
      \"ratio\": 0.01,
      \"type\": \"similarity\"
    }
  }" | jq '{id: .id, name: .name, error: .error.message}'

输出:类似受众已创建(ID:<ID>)。通常需要 1-6 小时才能填充。

Advantage+

创建 Advantage+ Shopping Campaign(由 AI 优化)。

通过 AskUserQuestion 收集:

  1. 每日预算(美元,自由输入)
  2. 广告系列名称(自由输入)

然后确认:"Create Advantage+ campaign '<NAME>' with $<BUDGET>/day?" 选项 [Create, Cancel]

BUDGET_CENTS=$(awk "BEGIN {printf \"%d\", ${BUDGET_DOLLARS} * 100}")
curl -s -X POST "https://graph.facebook.com/v20.0/${META_ACCOUNT}/campaigns" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"${CAMPAIGN_NAME}\",
    \"objective\": \"OUTCOME_SALES\",
    \"status\": \"PAUSED\",
    \"special_ad_categories\": [],
    \"daily_budget\": ${BUDGET_CENTS},
    \"smart_promotion_type\": \"AUTOMATED_SHOPPING_ADS\"
  }" | jq '{id: .id, error: .error.message}'

输出:Advantage+ campaign "${CAMPAIGN_NAME}" 已创建(ID:<ID>,状态:PAUSED)。启用后,Meta AI 将优化定向和创意投放。


分析 / ga4

使用 gcloud ADC,通过 Data API 获取 GA4 数据。

获取访问令牌
GA4_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)
会话和转化(过去 7 天)
curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}],
    "metrics": [
      {"name": "sessions"},
      {"name": "totalUsers"},
      {"name": "conversions"},
      {"name": "totalRevenue"},
      {"name": "bounceRate"},
      {"name": "averageSessionDuration"}
    ]
  }' | jq '.rows[0].metricValues | {sessions: .[0].value, users: .[1].value, conversions: .[2].value, revenue: .[3].value, bounce_rate: .[4].value}'
流量来源(过去 7 天)
curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}],
    "dimensions": [{"name": "sessionDefaultChannelGrouping"}],
    "metrics": [{"name": "sessions"}, {"name": "conversions"}],
    "orderBys": [{"metric": {"metricName": "sessions"}, "desc": true}],
    "limit": 8
  }' | jq '.rows[] | {channel: .dimensionValues[0].value, sessions: .metricValues[0].value, conversions: .metricValues[1].value}'
热门页面(过去 7 天)
curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}],
    "dimensions": [{"name": "pagePath"}],
    "metrics": [{"name": "screenPageViews"}, {"name": "averageSessionDuration"}],
    "orderBys": [{"metric": {"metricName": "screenPageViews"}, "desc": true}],
    "limit": 10
  }' | jq '.rows[] | {page: .dimensionValues[0].value, views: .metricValues[0].value}'

如果 GA4_TOKEN 为空或 gcloud 不可用,则输出:GA4 not configured — run /ops:marketing setup or configure gcloud ADC。

输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 ANALYTICS (GA4) — last 7d
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Sessions:     [N]     Users: [N]
 Conversions:  [N]     CVR:   [X]%
 Revenue:      $[X]
 Bounce Rate:  [X]%    Avg Session: [Xm Xs]

 TRAFFIC SOURCES
 [channel]  [N sessions]  [N conversions]
 ...

 TOP PAGES
 [path]  [N views]

ga4-advanced

高级 GA4 分析:实时数据、漏斗、群组、受众导出和数据透视报告。

凭据检查:如果 GA4_TOKEN 为空或缺少 GA4_PROPERTY,则输出 GA4 not configured — run /ops:marketing setup or configure gcloud ADC 并停止。

在 ga4-advanced 中根据 $ARGUMENTS 路由(匹配 ga4 <sub> 模式):

输入操作
realtime查看当前活跃用户(过去 30 分钟)
funnel转化漏斗及步骤可视化
cohort按设备分析群组留存情况
audience异步导出受众细分
pivot多维数据透视报告
realtime
GA4_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)
RESULT=$(curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runRealtimeReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "minuteRanges": [{"startMinutesAgo": 29, "endMinutesAgo": 0}],
    "dimensions": [
      {"name": "unifiedScreenName"},
      {"name": "deviceCategory"}
    ],
    "metrics": [{"name": "activeUsers"}],
    "orderBys": [{"metric": {"metricName": "activeUsers"}, "desc": true}],
    "limit": 10
  }')

TOTAL=$(echo "$RESULT" | jq '[.rows[]?.metricValues[0].value | tonumber] | add // 0')

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  GA4 REALTIME — Last 30 Minutes"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Active Users Right Now: ${TOTAL}"
echo ""
echo "Top Pages:"
printf "| %-40s | %-8s | %-7s |\n" "Page" "Device" "Users"
printf "|%s|%s|%s|\n" "------------------------------------------" "----------" "---------"
echo "$RESULT" | jq -r '.rows[]? | [.dimensionValues[0].value, .dimensionValues[1].value, .metricValues[0].value] | @tsv' 2>/dev/null | \
  while IFS=$'\t' read -r page device users; do
    printf "| %-40s | %-8s | %-7s |\n" "${page:0:40}" "$device" "$users"
  done
funnel

通过 AskUserQuestion 询问用户漏斗步骤(自由输入)。默认模板使用 session_start → page_view → purchase。

# NOTE: Uses v1alpha — breaking changes possible per Google's versioning policy
GA4_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)

# Prompt for funnel type first
# AskUserQuestion: "Funnel mode?" options [Closed funnel, Open funnel]

IS_OPEN=$([ "$FUNNEL_MODE" = "Open funnel" ] && echo "true" || echo "false")

RESULT=$(curl -s -X POST "https://analyticsdata.googleapis.com/v1alpha/properties/${GA4_PROPERTY}:runFunnelReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"dateRanges\": [{\"startDate\": \"30daysAgo\", \"endDate\": \"today\"}],
    \"funnel\": {
      \"isOpenFunnel\": ${IS_OPEN},
      \"steps\": [
        {
          \"name\": \"Session Start\",
          \"filterExpression\": {\"funnelEventFilter\": {\"eventName\": \"session_start\"}}
        },
        {
          \"name\": \"Page View\",
          \"filterExpression\": {\"funnelEventFilter\": {\"eventName\": \"page_view\"}}
        },
        {
          \"name\": \"Purchase\",
          \"filterExpression\": {\"funnelEventFilter\": {\"eventName\": \"purchase\"}}
        }
      ]
    },
    \"funnelBreakdown\": {
      \"breakdownDimension\": {\"name\": \"deviceCategory\"},
      \"limit\": 4
    }
  }")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  GA4 FUNNEL — Last 30 Days (${FUNNEL_MODE})"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "$RESULT" | jq -r '
  .funnelTable.rows[]? |
  "Step: \(.dimensionValues[0].value)  Users: \(.metricValues[0].value)  Completion: \(.metricValues[1].value)%  Abandoned: \(.metricValues[2].value)"
' 2>/dev/null || echo "No funnel data — ensure purchase events are firing in GA4."
cohort

分析过去一个月获取的用户的每周群组留存情况,并按设备细分。

GA4_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)
START_DATE=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d)
END_DATE=$(date +%Y-%m-%d)

RESULT=$(curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"dimensions\": [
      {\"name\": \"cohort\"},
      {\"name\": \"cohortNthWeek\"},
      {\"name\": \"deviceCategory\"}
    ],
    \"metrics\": [
      {\"name\": \"cohortActiveUsers\"},
      {\"name\": \"cohortRetentionFraction\"}
    ],
    \"cohortSpec\": {
      \"cohorts\": [{
        \"dimension\": \"firstSessionDate\",
        \"dateRange\": {\"startDate\": \"${START_DATE}\", \"endDate\": \"${END_DATE}\"}
      }],
      \"cohortsRange\": {
        \"granularity\": \"WEEKLY\",
        \"startOffset\": 0,
        \"endOffset\": 5
      }
    }
  }")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  GA4 COHORT RETENTION — Last 30 Days"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
printf "| %-12s | %-6s | %-8s | %-10s | %-10s |\n" "Cohort" "Week" "Device" "Users" "Retention%"
printf "|%s|%s|%s|%s|%s|\n" "--------------" "--------" "----------" "------------" "------------"
echo "$RESULT" | jq -r '.rows[]? | [
  .dimensionValues[0].value,
  .dimensionValues[1].value,
  .dimensionValues[2].value,
  .metricValues[0].value,
  (.metricValues[1].value | tonumber * 100 | tostring | split(".")[0])
] | @tsv' 2>/dev/null | \
  while IFS=$'\t' read -r cohort week device users retention; do
    printf "| %-12s | %-6s | %-8s | %-10s | %-10s |\n" "$cohort" "$week" "$device" "$users" "${retention}%"
  done
audience

异步导出受众:创建 → 轮询直至状态为 ACTIVE → 显示用户数。

GA4_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)

# Step 1: List available audiences so user can pick one
AUDIENCES=$(curl -s "https://analyticsadmin.googleapis.com/v1alpha/properties/${GA4_PROPERTY}/audiences" \
  -H "Authorization: Bearer ${GA4_TOKEN}")
echo "Available audiences:"
echo "$AUDIENCES" | jq -r '.audiences[]? | "\(.name | split("/") | last): \(.displayName)"' 2>/dev/null

# AskUserQuestion: "Enter audience ID from list above:" (free text)

# Step 2: Create export
EXPORT_RESP=$(curl -s -X POST \
  "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}/audienceExports" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"audience\": \"properties/${GA4_PROPERTY}/audiences/${AUDIENCE_ID}\",
    \"dimensions\": [
      {\"dimensionName\": \"deviceId\"},
      {\"dimensionName\": \"isAdsPersonalizationAllowed\"}
    ]
  }")
EXPORT_NAME=$(echo "$EXPORT_RESP" | jq -r '.name // empty')

if [ -z "$EXPORT_NAME" ]; then
  echo "Failed to create export: $(echo "$EXPORT_RESP" | jq -r '.error.message // "unknown error"')"
  exit 0
fi

echo "Export created: ${EXPORT_NAME}"
echo "Polling for completion (small audiences: ~30s, large: up to 15 min)..."

# Step 3: Poll until ACTIVE or FAILED
ATTEMPTS=0
while [ $ATTEMPTS -lt 60 ]; do
  STATUS_RESP=$(curl -s "https://analyticsdata.googleapis.com/v1beta/${EXPORT_NAME}" \
    -H "Authorization: Bearer ${GA4_TOKEN}")
  STATE=$(echo "$STATUS_RESP" | jq -r '.state // "UNKNOWN"')
  PCT=$(echo "$STATUS_RESP" | jq -r '.percentageCompleted // 0')
  
  if [ "$STATE" = "ACTIVE" ]; then break; fi
  if [ "$STATE" = "FAILED" ]; then
    echo "Export failed. Try again or check GA4 audience configuration."
    exit 0
  fi
  echo "  State: ${STATE} (${PCT}% complete)..."
  sleep 10
  ATTEMPTS=$((ATTEMPTS + 1))
done

# Step 4: Query results
QUERY_RESP=$(curl -s -X POST \
  "https://analyticsdata.googleapis.com/v1beta/${EXPORT_NAME}:query" \
  -H "Authorization: Bearer ${GA4_TOKEN}")
ROW_COUNT=$(echo "$QUERY_RESP" | jq '.rowCount // 0')

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  GA4 AUDIENCE EXPORT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  Audience ID: ${AUDIENCE_ID}"
echo "  Users exported: ${ROW_COUNT}"
echo "  Ads-eligible: $(echo "$QUERY_RESP" | jq '[.audienceRows[]? | select(.dimensionValues[1].value == "true")] | length') users"
echo ""
echo "Export ready. Use this audience for retargeting in Meta or Google Ads."
pivot

多维数据透视:渠道组 × 设备类别 × 转化次数。

GA4_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)

RESULT=$(curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runPivotReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "dateRanges": [{"startDate": "30daysAgo", "endDate": "today"}],
    "dimensions": [
      {"name": "sessionDefaultChannelGrouping"},
      {"name": "deviceCategory"}
    ],
    "metrics": [
      {"name": "sessions"},
      {"name": "conversions"},
      {"name": "totalRevenue"}
    ],
    "pivots": [
      {
        "fieldNames": ["sessionDefaultChannelGrouping"],
        "limit": 6
      },
      {
        "fieldNames": ["deviceCategory"],
        "limit": 3
      }
    ]
  }')

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  GA4 PIVOT — Channel × Device (Last 30 Days)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
printf "| %-20s | %-8s | %-10s | %-11s | %-10s |\n" "Channel" "Device" "Sessions" "Conversions" "Revenue"
printf "|%s|%s|%s|%s|%s|\n" "----------------------" "----------" "------------" "-------------" "------------"
echo "$RESULT" | jq -r '.rows[]? | [
  .dimensionValues[0].value,
  .dimensionValues[1].value,
  .metricValues[0].value,
  .metricValues[1].value,
  (.metricValues[2].value | tonumber | . * 100 | round / 100 | tostring)
] | @tsv' 2>/dev/null | \
  while IFS=$'\t' read -r channel device sessions convs revenue; do
    printf "| %-20s | %-8s | %-10s | %-11s | \$%-9s |\n" "${channel:0:20}" "$device" "$sessions" "$convs" "$revenue"
  done

seo / gsc

获取 Google Search Console 数据。

获取访问令牌(使用相同的 gcloud ADC)
GSC_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)
GSC_SITE_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${GSC_SITE}', safe=''))" 2>/dev/null || echo "${GSC_SITE}" | sed 's|:|%3A|g; s|/|%2F|g')
搜索表现(过去 28 天)
curl -s -X POST "https://searchconsole.googleapis.com/webmasters/v3/sites/${GSC_SITE_ENCODED}/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "'$(date -v-28d +%Y-%m-%d 2>/dev/null || date -d '28 days ago' +%Y-%m-%d)'",
    "endDate": "'$(date +%Y-%m-%d)'",
    "dimensions": [],
    "rowLimit": 1
  }' | jq '{clicks: .rows[0].clicks, impressions: .rows[0].impressions, ctr: .rows[0].ctr, position: .rows[0].position}'
热门查询(过去 28 天)
curl -s -X POST "https://searchconsole.googleapis.com/webmasters/v3/sites/${GSC_SITE_ENCODED}/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "'$(date -v-28d +%Y-%m-%d 2>/dev/null || date -d '28 days ago' +%Y-%m-%d)'",
    "endDate": "'$(date +%Y-%m-%d)'",
    "dimensions": ["query"],
    "rowLimit": 20,
    "dimensionFilterGroups": []
  }' | jq '.rows[] | {query: .keys[0], clicks: .clicks, impressions: .impressions, position: (.position | floor)}'
按点击次数排序的热门页面
curl -s -X POST "https://searchconsole.googleapis.com/webmasters/v3/sites/${GSC_SITE_ENCODED}/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "'$(date -v-28d +%Y-%m-%d 2>/dev/null || date -d '28 days ago' +%Y-%m-%d)'",
    "endDate": "'$(date +%Y-%m-%d)'",
    "dimensions": ["page"],
    "rowLimit": 10
  }' | jq '.rows[] | {page: .keys[0], clicks: .clicks, impressions: .impressions, position: (.position | floor)}'

如果未配置 GSC,则输出:Search Console not configured — run /ops:marketing setup。

输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 SEO (SEARCH CONSOLE) — last 28d
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Clicks:       [N]
 Impressions:  [N]
 CTR:          [X]%
 Avg Position: [X]

 TOP QUERIES
 [query]  [clicks] clicks  pos [N]
 ...

 TOP PAGES
 [url]  [clicks] clicks  [impressions] impr

social

汇总可用的社交媒体指标。检查哪些渠道已配置。

Instagram(通过 Meta Graph API,使用与 Meta Ads 相同的令牌)
# Get Instagram Business Account ID linked to the ad account
curl -s "https://graph.facebook.com/v18.0/me/accounts?fields=instagram_business_account" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq '.data[].instagram_business_account.id' 2>/dev/null

# Then pull media insights
curl -s "https://graph.facebook.com/v18.0/${IG_ACCOUNT_ID}?fields=followers_count,media_count,profile_views" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq '{followers: .followers_count, posts: .media_count, profile_views: .profile_views}'
YouTube(如果已通过 gcloud 配置)
YT_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)
curl -s "https://www.googleapis.com/youtube/v3/channels?part=statistics&mine=true" \
  -H "Authorization: Bearer ${YT_TOKEN}" | jq '.items[0].statistics | {subscribers: .subscriberCount, views: .viewCount, videos: .videoCount}'

对于任何未配置的渠道,请显示 [not configured],而不是报错。

输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 SOCIAL MEDIA
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Instagram:  [N followers]  [N posts]  [N profile views]
 YouTube:    [N subscribers]  [N total views]
 TikTok:     [not configured] — set TIKTOK_ACCESS_TOKEN

instagram

通过 Instagram Graph API 发布内容并查看数据分析(与 Meta Ads 使用相同的 META_TOKEN)。

前提条件:

  • 已配置 META_TOKEN(与 Meta Ads 相同)
  • Instagram Business 账号已关联 Facebook Page
  • 通过以下命令解析 IG_ACCOUNT_ID:curl "https://graph.facebook.com/v21.0/me/accounts?fields=instagram_business_account" -H "Authorization: Bearer ${META_TOKEN}" → data[0].instagram_business_account.id

**速率限制:**每个应用每小时 200 次 API 调用。受众统计数据有 48 小时的报告延迟。媒体数据分析要求账号拥有超过 1,000 名粉丝。

每次调用 instagram 时,都要在开始时解析 IG 账号 ID:

IG_ACCOUNT_ID=$(claude plugin config get instagram_account_id 2>/dev/null)
if [ -z "$IG_ACCOUNT_ID" ]; then
  IG_ACCOUNT_ID=$(curl -s "https://graph.facebook.com/v21.0/me/accounts?fields=instagram_business_account" \
    -H "Authorization: Bearer ${META_TOKEN}" | jq -r '.data[0].instagram_business_account.id // empty')
  # Cache it
  [ -n "$IG_ACCOUNT_ID" ] && claude plugin config set instagram_account_id "$IG_ACCOUNT_ID" 2>/dev/null
fi
if [ -z "$IG_ACCOUNT_ID" ]; then
  echo "Instagram Business account not linked to your Meta token. Ensure your Facebook Page has an Instagram Business account connected."
  exit 0
fi

在 instagram 中根据 $ARGUMENTS 路由:

输入操作
post <IMAGE_URL>向动态发布图片帖子
reel <VIDEO_URL>发布 Reel
story <IMAGE_URL|VIDEO_URL>发布 Story
insights <MEDIA_ID>查看单条帖子的指标
account-insights [days]查看账号级覆盖人数和展示次数
demographics查看受众年龄/性别/地区
post

发布图片帖子(分两步:创建容器 → 发布)。

通过 AskUserQuestion 收集:

  1. 图片 URL(可公开访问的 HTTPS URL)——自由文本
  2. 说明文字——自由文本
# Step 1: Create media container
CONTAINER=$(curl -s -X POST "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/media" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "image_url=${IMAGE_URL}" \
  -F "caption=${CAPTION}" \
  -F "media_type=IMAGE")
CONTAINER_ID=$(echo "$CONTAINER" | jq -r '.id // empty')

if [ -z "$CONTAINER_ID" ]; then
  echo "Failed to create media container: $(echo "$CONTAINER" | jq -r '.error.message // "unknown error"')"
  exit 0
fi

# Step 2: Publish
PUBLISH=$(curl -s -X POST "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/media_publish" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "creation_id=${CONTAINER_ID}")
MEDIA_ID=$(echo "$PUBLISH" | jq -r '.id // empty')

if [ -n "$MEDIA_ID" ]; then
  echo "Post published (Media ID: ${MEDIA_ID}). View at https://www.instagram.com/ — may take 1-2 min to appear."
else
  echo "Publish failed: $(echo "$PUBLISH" | jq -r '.error.message // "unknown error"')"
fi
reel

发布 Reel。视频必须是 HTTPS URL(MP4、H.264,最长 15 分钟,宽度至少 500px)。

通过 AskUserQuestion 收集:

  1. 视频 URL(HTTPS)——自由文本
  2. 说明文字——自由文本
# Step 1: Create video container (async — must poll for status)
CONTAINER=$(curl -s -X POST "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/media" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "media_type=REELS" \
  -F "video_url=${VIDEO_URL}" \
  -F "caption=${CAPTION}" \
  -F "share_to_feed=true")
CONTAINER_ID=$(echo "$CONTAINER" | jq -r '.id // empty')

if [ -z "$CONTAINER_ID" ]; then
  echo "Failed to create Reel container: $(echo "$CONTAINER" | jq -r '.error.message // "unknown error"')"
  exit 0
fi

echo "Reel uploading... polling for ready status."

# Poll until status is FINISHED
ATTEMPTS=0
while [ $ATTEMPTS -lt 30 ]; do
  STATUS=$(curl -s "https://graph.facebook.com/v21.0/${CONTAINER_ID}?fields=status_code" \
    -H "Authorization: Bearer ${META_TOKEN}" | jq -r '.status_code // "UNKNOWN"')
  [ "$STATUS" = "FINISHED" ] && break
  [ "$STATUS" = "ERROR" ] && echo "Reel processing failed." && exit 0
  sleep 10
  ATTEMPTS=$((ATTEMPTS + 1))
done

# Step 2: Publish
PUBLISH=$(curl -s -X POST "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/media_publish" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "creation_id=${CONTAINER_ID}")
MEDIA_ID=$(echo "$PUBLISH" | jq -r '.id // empty')

if [ -n "$MEDIA_ID" ]; then
  echo "Reel published (Media ID: ${MEDIA_ID}). Reach and plays metrics available after 24-48h."
else
  echo "Reel publish failed: $(echo "$PUBLISH" | jq -r '.error.message // "unknown error"')"
fi
story

发布 Story(图片或视频,24 小时后过期)。

通过 AskUserQuestion 收集:

  1. 内容 URL(HTTPS 图片或视频)——自由文本
  2. 内容类型:[图片 Story,视频 Story]
if [ "$CONTENT_TYPE" = "Video story" ]; then
  MEDIA_TYPE="VIDEO"
  URL_FIELD="video_url"
else
  MEDIA_TYPE="IMAGE"
  URL_FIELD="image_url"
fi

CONTAINER=$(curl -s -X POST "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/media" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "media_type=STORIES" \
  -F "${URL_FIELD}=${CONTENT_URL}")
CONTAINER_ID=$(echo "$CONTAINER" | jq -r '.id // empty')

if [ -z "$CONTAINER_ID" ]; then
  echo "Failed to create Story container: $(echo "$CONTAINER" | jq -r '.error.message // "unknown error"')"
  exit 0
fi

PUBLISH=$(curl -s -X POST "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/media_publish" \
  -H "Authorization: Bearer ${META_TOKEN}" \
  -F "creation_id=${CONTAINER_ID}")
MEDIA_ID=$(echo "$PUBLISH" | jq -r '.id // empty')

if [ -n "$MEDIA_ID" ]; then
  echo "Story published (Media ID: ${MEDIA_ID}). Expires after 24 hours."
else
  echo "Story publish failed: $(echo "$PUBLISH" | jq -r '.error.message // "unknown error"')"
fi
insights <MEDIA_ID>

查看单条帖子的指标。注意:对于非 Reels 视频,覆盖人数、收藏数和分享数已弃用;Reels/视频仅提供播放次数。

METRICS="reach,saved,shares,comments_count,like_count,impressions"
# For Reels, add plays: detect via media_type field
MEDIA_TYPE=$(curl -s "https://graph.facebook.com/v21.0/${MEDIA_ID}?fields=media_type" \
  -H "Authorization: Bearer ${META_TOKEN}" | jq -r '.media_type // "IMAGE"')
[ "$MEDIA_TYPE" = "VIDEO" ] || [ "$MEDIA_TYPE" = "REEL" ] && METRICS="${METRICS},plays"

RESULT=$(curl -s "https://graph.facebook.com/v21.0/${MEDIA_ID}/insights?metric=${METRICS}&period=lifetime" \
  -H "Authorization: Bearer ${META_TOKEN}")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  INSTAGRAM POST INSIGHTS — ${MEDIA_ID}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "$RESULT" | jq -r '.data[]? | "  \(.name): \(.values[0].value)"' 2>/dev/null || \
  echo "No insights data — account must have >1,000 followers for insights access."
account-insights [days]

查看账号级覆盖人数和展示次数。默认:过去 7 天。

DAYS="${DAYS:-7}"
END_DATE=$(date +%Y-%m-%d)
START_DATE=$(date -v-${DAYS}d +%Y-%m-%d 2>/dev/null || date -d "${DAYS} days ago" +%Y-%m-%d)

RESULT=$(curl -s "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/insights?metric=reach,impressions,profile_views&period=day&since=${START_DATE}&until=${END_DATE}" \
  -H "Authorization: Bearer ${META_TOKEN}")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  INSTAGRAM ACCOUNT INSIGHTS — Last ${DAYS} Days"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  Note: Data reflects 24-48h reporting delay"
echo ""

REACH=$(echo "$RESULT" | jq '[.data[]? | select(.name == "reach") | .values[]?.value | tonumber] | add // 0')
IMPRESSIONS=$(echo "$RESULT" | jq '[.data[]? | select(.name == "impressions") | .values[]?.value | tonumber] | add // 0')
PROFILE_VIEWS=$(echo "$RESULT" | jq '[.data[]? | select(.name == "profile_views") | .values[]?.value | tonumber] | add // 0')

echo "  Reach:         ${REACH}"
echo "  Impressions:   ${IMPRESSIONS}"
echo "  Profile Views: ${PROFILE_VIEWS}"
demographics

按年龄/性别和热门地区查看受众细分情况。需要选择 lifetime 时段(延迟 48 小时)。

RESULT=$(curl -s "https://graph.facebook.com/v21.0/${IG_ACCOUNT_ID}/insights?metric=audience_gender_age,audience_city,audience_country&period=lifetime" \
  -H "Authorization: Bearer ${META_TOKEN}")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  INSTAGRAM DEMOGRAPHICS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  Note: Top 45 segments shown. Data has 48h delay."
echo ""

echo "AGE / GENDER BREAKDOWN:"
echo "$RESULT" | jq -r '.data[]? | select(.name == "audience_gender_age") | .values[0].value | to_entries[] | "  \(.key): \(.value)%"' 2>/dev/null | head -20

echo ""
echo "TOP CITIES:"
echo "$RESULT" | jq -r '.data[]? | select(.name == "audience_city") | .values[0].value | to_entries | sort_by(-.value) | .[0:10][] | "  \(.key): \(.value)%"' 2>/dev/null

echo ""
echo "TOP COUNTRIES:"
echo "$RESULT" | jq -r '.data[]? | select(.name == "audience_country") | .values[0].value | to_entries | sort_by(-.value) | .[0:10][] | "  \(.key): \(.value)%"' 2>/dev/null

google-ads

凭据检查:如果解析后 GADS_DEV_TOKEN 或 GADS_REFRESH_TOKEN 为空,则打印: Warning: Google Ads not configured. Run /ops:setup marketing to set up credentials. 并停止。

令牌刷新:每次调用 google-ads 时,都要在开始时运行上文“凭据解析”中的访问令牌刷新 curl。如果 GADS_ACCESS_TOKEN 为 null 或 "null",则打印: Warning: Google Ads token refresh failed. Check client_id/client_secret/refresh_token in /ops:setup. 并停止。

在 google-ads 部分根据 $ARGUMENTS 路由:

输入操作
(空)、dashboard、overview广告系列效果仪表板(过去 7 天)
search-terms、terms搜索词报告及否定关键词候选项(过去 30 天)
budget-recs、recommendations、recsGoogle 提供的预算优化建议
campaigns、manage广告系列管理——列出、创建、暂停、启用、调整预算
keywords、kw、keyword-plannerKeyword Planner——发现关键词及其搜索量和出价数据
ad-groups、ag广告组管理——列出、创建、添加/移除关键词、调整出价
仪表板(默认——无参数、dashboard、overview)
# Campaign performance — last 7 days
CAMPAIGNS=$(curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary '{
    "query": "SELECT campaign.id, campaign.name, campaign.status, campaign_budget.amount_micros, metrics.cost_micros, metrics.impressions, metrics.clicks, metrics.conversions, metrics.conversions_value FROM campaign WHERE segments.date DURING LAST_7_DAYS AND campaign.status != REMOVED ORDER BY metrics.cost_micros DESC LIMIT 20"
  }')

# Check for API error
GADS_ERROR=$(echo "$CAMPAIGNS" | jq -r '.[0].error.message // empty' 2>/dev/null)
if [ -n "$GADS_ERROR" ]; then
  echo "Google Ads API error: ${GADS_ERROR}"
  echo "Check credentials with /ops:marketing setup."
  exit 0
fi

# Check for empty results
CAMPAIGN_COUNT=$(echo "$CAMPAIGNS" | jq '[.[].results[]?] | length' 2>/dev/null || echo "0")
if [ "$CAMPAIGN_COUNT" -eq 0 ]; then
  echo "No active campaigns found in the last 7 days."
  exit 0
fi

# Compute totals
TOTAL_SPEND=$(echo "$CAMPAIGNS" | jq '[.[].results[]?.metrics.costMicros // "0" | tonumber] | add / 1000000' 2>/dev/null)
TOTAL_CONVERSIONS=$(echo "$CAMPAIGNS" | jq '[.[].results[]?.metrics.conversions // "0" | tonumber] | add' 2>/dev/null)
TOTAL_VALUE=$(echo "$CAMPAIGNS" | jq '[.[].results[]?.metrics.conversionsValue // "0" | tonumber] | add' 2>/dev/null)
OVERALL_ROAS=$(awk "BEGIN { if (${TOTAL_SPEND:-0} > 0) printf \"%.2f\", ${TOTAL_VALUE:-0} / ${TOTAL_SPEND:-1}; else print \"—\" }")
TOTAL_SPEND_FMT=$(awk "BEGIN { printf \"%.2f\", ${TOTAL_SPEND:-0} }")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  GOOGLE ADS — Last 7 Days"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Total Spend: \$${TOTAL_SPEND_FMT}"
echo "Total Conversions: ${TOTAL_CONVERSIONS}"
echo "Overall ROAS: ${OVERALL_ROAS}"
echo ""

# Print table header
printf "| %-30s | %-8s | %-10s | %-10s | %-8s | %-8s | %-6s | %-6s | %-6s |\n" \
  "Campaign" "Status" "Budget/day" "Spend" "Impr" "Clicks" "CTR" "Conv" "ROAS"
printf "|%s|%s|%s|%s|%s|%s|%s|%s|%s|\n" \
  "--------------------------------" "----------" "------------" "------------" "----------" "----------" "--------" "--------" "--------"

# Print each campaign row
echo "$CAMPAIGNS" | jq -r '.[].results[]? | [
  .campaign.name,
  .campaign.status,
  (.campaignBudget.amountMicros // "0" | tonumber / 1000000),
  (.metrics.costMicros // "0" | tonumber / 1000000),
  (.metrics.impressions // "0" | tonumber),
  (.metrics.clicks // "0" | tonumber),
  (.metrics.conversions // "0" | tonumber),
  (.metrics.conversionsValue // "0" | tonumber),
  (.metrics.costMicros // "0" | tonumber)
] | @tsv' 2>/dev/null | while IFS=$'\t' read -r name status budget_raw spend_raw impr_raw clicks_raw conv_raw value_raw cost_raw; do
  BUDGET=$(awk "BEGIN { printf \"%.2f\", ${budget_raw:-0} }")
  SPEND=$(awk "BEGIN { printf \"%.2f\", ${spend_raw:-0} }")
  CTR=$(awk "BEGIN { if (${impr_raw:-0} > 0) printf \"%.2f\", ${clicks_raw:-0} / ${impr_raw:-0} * 100; else print \"0.00\" }")
  ROAS=$(awk "BEGIN { if (${spend_raw:-0} > 0) printf \"%.2f\", ${value_raw:-0} / ${spend_raw:-1}; else print \"—\" }")
  printf "| %-30s | %-8s | \$%-9s | \$%-9s | %-8s | %-8s | %-5s%% | %-6s | %-6s |\n" \
    "${name:0:30}" "$status" "$BUDGET" "$SPEND" "$impr_raw" "$clicks_raw" "$CTR" "$conv_raw" "$ROAS"
done
搜索词(search-terms、terms)
SEARCH_TERMS=$(curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary '{
    "query": "SELECT search_term_view.search_term, search_term_view.status, campaign.name, ad_group.name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING LAST_30_DAYS AND metrics.impressions > 0 ORDER BY metrics.impressions DESC LIMIT 100"
  }')

# Check for API error
GADS_ST_ERROR=$(echo "$SEARCH_TERMS" | jq -r '.[0].error.message // empty' 2>/dev/null)
if [ -n "$GADS_ST_ERROR" ]; then
  echo "Google Ads API error: ${GADS_ST_ERROR}"
  echo "Check credentials with /ops:marketing setup."
  exit 0
fi

TERM_COUNT=$(echo "$SEARCH_TERMS" | jq '[.[].results[]?] | length' 2>/dev/null || echo "0")
if [ "$TERM_COUNT" -eq 0 ]; then
  echo "No search term data found for the last 30 days."
  exit 0
fi

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  SEARCH TERMS REPORT — Last 30 Days"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

printf "| %-30s | %-10s | %-20s | %-15s | %-6s | %-6s | %-7s | %-6s |\n" \
  "Search Term" "Status" "Campaign" "Ad Group" "Impr" "Clicks" "Cost" "Conv"
printf "|%s|%s|%s|%s|%s|%s|%s|%s|\n" \
  "--------------------------------" "------------" "----------------------" "-----------------" "--------" "--------" "---------" "--------"

# Status mapping and table rows
echo "$SEARCH_TERMS" | jq -r '.[].results[]? | [
  .searchTermView.searchTerm,
  .searchTermView.status,
  .campaign.name,
  .adGroup.name,
  (.metrics.impressions // "0" | tostring),
  (.metrics.clicks // "0" | tostring),
  (.metrics.costMicros // "0" | tonumber / 1000000 | tostring),
  (.metrics.conversions // "0" | tostring)
] | @tsv' 2>/dev/null | while IFS=$'\t' read -r term status campaign adgroup impr clicks cost_raw conv; do
  case "$status" in
    ADDED)    STATUS_LABEL="✓ Added"   ;;
    EXCLUDED) STATUS_LABEL="✗ Excluded" ;;
    *)        STATUS_LABEL="○ New"     ;;
  esac
  COST=$(awk "BEGIN { printf \"%.2f\", ${cost_raw:-0} }")
  printf "| %-30s | %-10s | %-20s | %-15s | %-6s | %-6s | \$%-6s | %-6s |\n" \
    "${term:0:30}" "$STATUS_LABEL" "${campaign:0:20}" "${adgroup:0:15}" "$impr" "$clicks" "$COST" "$conv"
done

echo ""
echo "Negative keyword candidates (high spend, zero conversions):"

echo "$SEARCH_TERMS" | jq -r '.[].results[]? | select(
  (.metrics.conversions // "0" | tonumber) == 0 and
  (.metrics.costMicros // "0" | tonumber) > 1000000
) | "  • \(.searchTermView.searchTerm) — $\(.metrics.costMicros | tonumber / 1000000 | tostring | split(".") | .[0] + "." + (.[1] // "00")[0:2])"' 2>/dev/null || echo "  (none found)"
预算建议(budget-recs、recommendations、recs)
RECS=$(curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary '{
    "query": "SELECT recommendation.resource_name, recommendation.type, recommendation.campaign, recommendation.impact, recommendation.campaign_budget_recommendation FROM recommendation WHERE recommendation.type IN (CAMPAIGN_BUDGET, MOVE_UNUSED_BUDGET, MARGINAL_ROI_CAMPAIGN_BUDGET, FORECASTING_CAMPAIGN_BUDGET)"
  }')

# Check for API error
GADS_REC_ERROR=$(echo "$RECS" | jq -r '.[0].error.message // empty' 2>/dev/null)
if [ -n "$GADS_REC_ERROR" ]; then
  echo "Google Ads API error: ${GADS_REC_ERROR}"
  echo "Check credentials with /ops:marketing setup."
  exit 0
fi

REC_COUNT=$(echo "$RECS" | jq '[.[].results[]?] | length' 2>/dev/null || echo "0")
if [ "$REC_COUNT" -eq 0 ]; then
  echo "No budget recommendations available. Google needs campaign data to generate recommendations."
  exit 0
fi

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  BUDGET RECOMMENDATIONS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

printf "| %-22s | %-25s | %-15s | %-13s | %-20s |\n" \
  "Type" "Campaign" "Current Budget" "Recommended" "Impact"
printf "|%s|%s|%s|%s|%s|\n" \
  "------------------------" "---------------------------" "-----------------" "---------------" "----------------------"

echo "$RECS" | jq -r '.[].results[]? | [
  .recommendation.type,
  .recommendation.campaign,
  (.recommendation.campaignBudgetRecommendation.currentBudgetAmountMicros // "0" | tonumber / 1000000 | tostring),
  (.recommendation.campaignBudgetRecommendation.recommendedBudgetAmountMicros // "0" | tonumber / 1000000 | tostring),
  (.recommendation.impact.baseMetrics.impressions // "0" | tonumber | tostring),
  (.recommendation.impact.potentialMetrics.impressions // "0" | tonumber | tostring)
] | @tsv' 2>/dev/null | while IFS=$'\t' read -r rec_type campaign current_raw recommended_raw base_impr_raw pot_impr_raw; do
  case "$rec_type" in
    CAMPAIGN_BUDGET)             TYPE_LABEL="Increase Budget"      ;;
    MOVE_UNUSED_BUDGET)          TYPE_LABEL="Move Unused Budget"   ;;
    MARGINAL_ROI_CAMPAIGN_BUDGET) TYPE_LABEL="Marginal ROI"        ;;
    FORECASTING_CAMPAIGN_BUDGET) TYPE_LABEL="Forecasting"          ;;
    *)                           TYPE_LABEL="$rec_type"            ;;
  esac
  CURRENT=$(awk "BEGIN { printf \"%.2f\", ${current_raw:-0} }")
  RECOMMENDED=$(awk "BEGIN { printf \"%.2f\", ${recommended_raw:-0} }")
  IMPACT=$(awk "BEGIN {
    base = ${base_impr_raw:-0}; pot = ${pot_impr_raw:-0}
    if (base > 0) printf \"+%.0f%% impressions\", (pot - base) / base * 100
    else print \"—\"
  }")
  printf "| %-22s | %-25s | \$%-14s | \$%-12s | %-20s |\n" \
    "$TYPE_LABEL" "${campaign:0:25}" "$CURRENT" "$RECOMMENDED" "$IMPACT"
done
广告系列管理(campaigns、manage)

列出广告系列:

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary '{
    "query": "SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign_budget.amount_micros FROM campaign WHERE campaign.status != REMOVED ORDER BY campaign.name LIMIT 50"
  }'

输出为表格:

| # | Campaign ID | Name | Status | Channel | Budget/day |

创建广告系列(campaigns create):

通过 AskUserQuestion 逐项向用户收集自由文本:

  1. 广告系列名称
  2. 每日预算(美元)(转换为微单位:BUDGET_MICROS=$(awk "BEGIN {printf \"%d\", $DOLLARS * 1000000}"))
  3. 渠道类型——使用以下选项调用 AskUserQuestion:[Search, Display, Shopping, Video] 映射为 API 值:SEARCH、DISPLAY、SHOPPING、VIDEO

分两步修改:

步骤 1 — 创建预算:

BUDGET_RESP=$(curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/campaignBudgets:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"create\": {
        \"name\": \"Budget for ${CAMPAIGN_NAME}\",
        \"deliveryMethod\": \"STANDARD\",
        \"amountMicros\": \"${BUDGET_MICROS}\"
      }
    }]
  }")
BUDGET_RESOURCE=$(echo "$BUDGET_RESP" | jq -r '.results[0].resourceName')

步骤 2 — 创建广告系列(为安全起见,状态始终设为 PAUSED):

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/campaigns:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"create\": {
        \"name\": \"${CAMPAIGN_NAME}\",
        \"campaignBudget\": \"${BUDGET_RESOURCE}\",
        \"advertisingChannelType\": \"${CHANNEL_TYPE}\",
        \"status\": \"PAUSED\",
        \"manualCpc\": {},
        \"networkSettings\": {
          \"targetGoogleSearch\": true,
          \"targetSearchNetwork\": true,
          \"targetContentNetwork\": false
        }
      }
    }]
  }"

输出:✓ 广告系列 "${CAMPAIGN_NAME}" 已创建(状态:PAUSED,预算:$XX.XX/天)。使用以下命令启用:/ops:marketing google-ads campaigns enable <ID>

如果出错,解析响应中的 error.message 并显示。

暂停广告系列(campaigns pause <ID>):

⚠ 规则 5 — 暂停前确认:通过 AskUserQuestion 询问:"Pause campaign <NAME> (ID: <ID>)?",选项为 [Pause, Cancel]。

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/campaigns:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"update\": {
        \"resourceName\": \"customers/${GADS_CUSTOMER_ID}/campaigns/${CAMPAIGN_ID}\",
        \"status\": \"PAUSED\"
      },
      \"updateMask\": \"status\"
    }]
  }"

输出:✓ 广告系列 <NAME> 已暂停。

启用广告系列(campaigns enable <ID>):

与暂停操作相同,但设置 "status": "ENABLED"。无需确认(启用操作不会造成破坏)。

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/campaigns:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"update\": {
        \"resourceName\": \"customers/${GADS_CUSTOMER_ID}/campaigns/${CAMPAIGN_ID}\",
        \"status\": \"ENABLED\"
      },
      \"updateMask\": \"status\"
    }]
  }"

输出:✓ 广告系列 <NAME> 已启用。

调整预算(campaigns budget <ID> <AMOUNT>):

首先,获取广告系列当前预算的资源名称:

CAMPAIGN_DATA=$(curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{\"query\": \"SELECT campaign.campaign_budget, campaign_budget.amount_micros FROM campaign WHERE campaign.id = ${CAMPAIGN_ID}\"}")
BUDGET_RESOURCE=$(echo "$CAMPAIGN_DATA" | jq -r '.[0].results[0].campaign.campaignBudget // empty')
CURRENT_BUDGET_MICROS=$(echo "$CAMPAIGN_DATA" | jq -r '.[0].results[0].campaignBudget.amountMicros // "0"')
CURRENT_BUDGET=$(awk "BEGIN { printf \"%.2f\", ${CURRENT_BUDGET_MICROS:-0} / 1000000 }")

通过 AskUserQuestion 确认:"Change budget from $<CURRENT> to $<NEW>/day?",选项为 [Confirm, Cancel]。

然后更新:

NEW_BUDGET_MICROS=$(awk "BEGIN {printf \"%d\", ${NEW_AMOUNT} * 1000000}")
curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/campaignBudgets:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"update\": {
        \"resourceName\": \"${BUDGET_RESOURCE}\",
        \"amountMicros\": \"${NEW_BUDGET_MICROS}\"
      },
      \"updateMask\": \"amountMicros\"
    }]
  }"

输出:✓ 预算已更新:$<OLD>/天 → $<NEW>/天

关键词规划工具(keywords、kw、keyword-planner)
# Collect seed keywords from user via AskUserQuestion (free text)
# "Enter seed keywords (comma-separated):"
# Split into JSON array for the request: KEYWORDS_JSON_ARRAY=$(echo "$SEED_KEYWORDS" | sed 's/,/","/g' | sed 's/^/"/;s/$/"/')

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}:generateKeywordIdeas" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"language\": \"languageConstants/1000\",
    \"geoTargetConstants\": [\"geoTargetConstants/2840\"],
    \"includeAdultKeywords\": false,
    \"keywordPlanNetwork\": \"GOOGLE_SEARCH\",
    \"keywordSeed\": {
      \"keywords\": [${KEYWORDS_JSON_ARRAY}]
    }
  }"

语言 1000 = 英语,地理位置 2840 = 美国。这些是默认值;如果用户在偏好设置中配置了区域设置,则使用该设置。其他常见值:英国=2826、加拿大=2124、澳大利亚=2036。

输出格式:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  KEYWORD IDEAS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Seeds: keyword1, keyword2

| Keyword | Avg Monthly Searches | Competition | Low Bid | High Bid |
|---------|---------------------|-------------|---------|----------|
  • avgMonthlySearches 按原值显示(整数)
  • competition 按原值显示(LOW、MEDIUM、HIGH)
  • lowTopOfPageBidMicros 和 highTopOfPageBidMicros 除以 1,000,000,并以 $X.XX 格式显示
  • 输出按 avgMonthlySearches 降序排列

如果没有结果,输出:未找到这些种子关键词的关键词创意。请尝试不同或更宽泛的关键词。

广告组管理(ad-groups、ag)

列出广告系列的广告组(ad-groups list <CAMPAIGN_ID>):

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{\"query\": \"SELECT ad_group.id, ad_group.name, ad_group.status, ad_group.cpc_bid_micros FROM ad_group WHERE campaign.id = ${CAMPAIGN_ID} AND ad_group.status != REMOVED ORDER BY ad_group.name\"}"

输出:

| # | Ad Group ID | Name | Status | CPC Bid |

创建广告组(ad-groups create <CAMPAIGN_ID>):

通过 AskUserQuestion 收集:

  1. 广告组名称(自由文本)
  2. 默认 CPC 出价(美元)(自由文本,转换为微单位)
BID_MICROS=$(awk "BEGIN {printf \"%d\", ${BID_DOLLARS} * 1000000}")
curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/adGroups:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"create\": {
        \"name\": \"${AD_GROUP_NAME}\",
        \"campaign\": \"customers/${GADS_CUSTOMER_ID}/campaigns/${CAMPAIGN_ID}\",
        \"status\": \"ENABLED\",
        \"type\": \"SEARCH_STANDARD\",
        \"cpcBidMicros\": \"${BID_MICROS}\"
      }
    }]
  }"

输出:✓ 已在广告系列 ${CAMPAIGN_ID} 中创建广告组 "${AD_GROUP_NAME}"(CPC 出价:$X.XX)

列出广告组中的关键词(ad-groups keywords <AD_GROUP_ID>):

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{\"query\": \"SELECT ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.cpc_bid_micros, ad_group_criterion.status FROM ad_group_criterion WHERE ad_group.id = ${AD_GROUP_ID} AND ad_group_criterion.type = KEYWORD AND ad_group_criterion.status != REMOVED\"}"

输出:

| # | Keyword | Match Type | CPC Bid | Status |

向广告组添加关键词(ad-groups add-keyword <AD_GROUP_ID>):

通过 AskUserQuestion 收集:

  1. 关键词文本(自由文本)
  2. 匹配类型——使用以下选项调用 AskUserQuestion:[Broad, Phrase, Exact] 映射为:BROAD、PHRASE、EXACT
  3. CPC 出价(美元)(自由文本,转换为微单位)——可选;未提供时使用广告组默认值
curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/adGroupCriteria:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"create\": {
        \"adGroup\": \"customers/${GADS_CUSTOMER_ID}/adGroups/${AD_GROUP_ID}\",
        \"status\": \"ENABLED\",
        \"keyword\": {
          \"text\": \"${KEYWORD_TEXT}\",
          \"matchType\": \"${MATCH_TYPE}\"
        }
        ${BID_MICROS:+,\"cpcBidMicros\": \"${BID_MICROS}\"}
      }
    }]
  }"

输出:✓ 已将关键词 "${KEYWORD_TEXT}"(${MATCH_TYPE})添加到广告组 ${AD_GROUP_ID}

注意:关键词创建后不可修改。若要更改匹配类型或文本,请先移除再重新创建。若只需更改出价,请使用 ad-groups update-bid。

移除关键词(ad-groups remove-keyword <AD_GROUP_ID> <CRITERION_ID>):

⚠ 规则 5 — 移除前确认:通过 AskUserQuestion 询问:"Remove keyword <TEXT> from ad group <NAME>?",选项为 [Remove, Cancel]。

curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/adGroupCriteria:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"remove\": \"customers/${GADS_CUSTOMER_ID}/adGroupCriteria/${AD_GROUP_ID}~${CRITERION_ID}\"
    }]
  }"

输出:✓ 已从广告组移除关键词。

更新关键词出价(ad-groups update-bid <AD_GROUP_ID> <CRITERION_ID> <BID>):

NEW_BID_MICROS=$(awk "BEGIN {printf \"%d\", ${NEW_BID} * 1000000}")
curl -s -X POST \
  "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/adGroupCriteria:mutate" \
  "${GADS_HEADERS[@]}" \
  --data-binary "{
    \"operations\": [{
      \"update\": {
        \"resourceName\": \"customers/${GADS_CUSTOMER_ID}/adGroupCriteria/${AD_GROUP_ID}~${CRITERION_ID}\",
        \"cpcBidMicros\": \"${NEW_BID_MICROS}\"
      },
      \"updateMask\": \"cpcBidMicros\"
    }]
  }"

输出:✓ 关键词出价已更新为 $X.XX


campaigns

跨渠道营销活动概览 — 统一查看所有已配置渠道(Klaviyo、Meta Ads、Google Ads)中的活跃营销活动。

并行运行:

  1. Klaviyo 活跃营销活动(状态:draft + scheduled + sending)
  2. Meta Ads 活跃营销活动(状态 ACTIVE)——复用 META_TOKEN / META_ACCOUNT
  3. Google Ads 活跃营销活动(状态 ENABLED)——如果已配置,则复用 GADS_* 凭据
  4. Google Ads:先刷新访问令牌(参见 Credential Resolution)
# Meta Ads campaigns
META_CAMPAIGNS=$(curl -s "https://graph.facebook.com/v20.0/${META_ACCOUNT}/campaigns?fields=name,status,daily_budget,lifetime_budget,objective&filtering=[{\"field\":\"effective_status\",\"operator\":\"IN\",\"value\":[\"ACTIVE\"]}]" \
  -H "Authorization: Bearer ${META_TOKEN}" 2>/dev/null)

# Klaviyo campaigns
KLAVIYO_CAMPAIGNS=$(curl -s "https://a.klaviyo.com/api/campaigns/?filter=equals(messages.channel,'email')&sort=-created_at&page[size]=10" \
  -H "Authorization: Klaviyo-API-Key ${KLAVIYO_KEY}" \
  -H "revision: 2024-10-15" 2>/dev/null)

# Google Ads campaigns (if configured)
GADS_CAMPAIGNS=""
if [ -n "$GADS_ACCESS_TOKEN" ] && [ "$GADS_ACCESS_TOKEN" != "null" ]; then
  GADS_CAMPAIGNS=$(curl -s -X POST \
    "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
    "${GADS_HEADERS[@]}" \
    --data-binary '{"query": "SELECT campaign.id, campaign.name, campaign.status, campaign_budget.amount_micros, metrics.cost_micros FROM campaign WHERE campaign.status = ENABLED ORDER BY metrics.cost_micros DESC LIMIT 10"}' 2>/dev/null)
fi
输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 CROSS-CHANNEL CAMPAIGNS — active
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 EMAIL (Klaviyo)
 [campaign name]  [status]  [send date or "scheduled for X"]

 PAID — META ADS
 [campaign name]  [status]  $[daily_budget]/day  [objective]

 PAID — GOOGLE ADS
 [campaign name]  [status]  $[budget]/day  $[spend 7d]

 FLOWS (Always-on automation)
 [flow name]  [trigger type]  [status: live/draft]

对于任何未配置的渠道,显示 [not configured — /ops:marketing setup]。


optimize

跨平台广告优化 Agent。读取 Meta + Google Ads 数据,计算综合 ROAS,并建议预算转移方向。

启动营销优化 Agent:

Agent(prompt="Run the marketing optimizer agent. Read ops-marketing-dash data for Meta Ads and Google Ads spend/conversions/ROAS. Compute blended ROAS across platforms. Identify the highest-ROAS platform. Recommend budget shifts with specific dollar amounts. List top 3 actions by expected impact. Use the marketing-optimizer.md agent instructions.", model="claude-sonnet-4-5")

如果可使用 Agent Teams(CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1):

TeamCreate("optimizer")
Agent(team_name="optimizer", name="marketing-optimizer", prompt="You are the marketing optimizer. Read ops-marketing-dash pre-gathered data. Compute blended ROAS for Meta + Google. Recommend budget reallocation. Show unified attribution table.")
输出格式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 AD OPTIMIZATION REPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Blended ROAS:    [X]x  (Meta: [X]x  Google: [X]x)
 Total Spend:     $[X]  Total Revenue: $[X]

 RECOMMENDATIONS
 1. [Action]  Expected impact: [+X% ROAS / +$X revenue]
 2. [Action]  ...
 3. [Action]  ...

 BUDGET REALLOCATION
 Move $[X]/day from [Platform A] → [Platform B]
 Rationale: [X]x ROAS vs [X]x ROAS

attribution

统一归因表,横向展示所有已配置平台的支出、转化次数、收入和 ROAS。

# Gather all platform data in parallel
# Meta Ads — last 7d
META_DATA=$(curl -s "https://graph.facebook.com/v20.0/${META_ACCOUNT}/insights?fields=spend,actions,action_values&date_preset=last_7d&level=account" \
  -H "Authorization: Bearer ${META_TOKEN}" 2>/dev/null)
META_SPEND=$(echo "$META_DATA" | jq -r '.data[0].spend // "0"')
META_CONV=$(echo "$META_DATA" | jq '[.data[0].actions[]? | select(.action_type == "purchase") | .value | tonumber] | add // 0' 2>/dev/null)
META_REV=$(echo "$META_DATA" | jq '[.data[0].action_values[]? | select(.action_type == "purchase") | .value | tonumber] | add // 0' 2>/dev/null)
META_ROAS=$(awk "BEGIN { if (${META_SPEND:-0} > 0) printf \"%.2f\", ${META_REV:-0} / ${META_SPEND:-1}; else print \"—\" }")

# Google Ads — last 7d
GADS_DATA=""
GADS_SPEND="0"; GADS_CONV="0"; GADS_REV="0"; GADS_ROAS="—"
if [ -n "$GADS_ACCESS_TOKEN" ] && [ "$GADS_ACCESS_TOKEN" != "null" ]; then
  GADS_DATA=$(curl -s -X POST \
    "https://googleads.googleapis.com/${GADS_API_VERSION}/customers/${GADS_CUSTOMER_ID}/googleAds:searchStream" \
    "${GADS_HEADERS[@]}" \
    --data-binary '{"query": "SELECT metrics.cost_micros, metrics.conversions, metrics.conversions_value FROM customer WHERE segments.date DURING LAST_7_DAYS"}' 2>/dev/null)
  GADS_SPEND=$(echo "$GADS_DATA" | jq '[.[].results[]?.metrics.costMicros // "0" | tonumber] | add / 1000000 // 0' 2>/dev/null || echo "0")
  GADS_CONV=$(echo "$GADS_DATA" | jq '[.[].results[]?.metrics.conversions // "0" | tonumber] | add // 0' 2>/dev/null || echo "0")
  GADS_REV=$(echo "$GADS_DATA" | jq '[.[].results[]?.metrics.conversionsValue // "0" | tonumber] | add // 0' 2>/dev/null || echo "0")
  GADS_ROAS=$(awk "BEGIN { if (${GADS_SPEND:-0} > 0) printf \"%.2f\", ${GADS_REV:-0} / ${GADS_SPEND:-1}; else print \"—\" }")
fi

# Klaviyo attributed revenue
KLAVIYO_REV="—"
# (Pull from metric aggregates if needed — complex, show as note)

# GA4 conversions + revenue
GA4_DATA=$(curl -s -X POST "https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReport" \
  -H "Authorization: Bearer ${GA4_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}], "metrics": [{"name": "conversions"}, {"name": "totalRevenue"}]}' 2>/dev/null)
GA4_CONV=$(echo "$GA4_DATA" | jq -r '.rows[0].metricValues[0].value // "—"')
GA4_REV=$(echo "$GA4_DATA" | jq -r '.rows[0].metricValues[1].value // "—"')

TOTAL_AD_SPEND=$(awk "BEGIN { printf \"%.2f\", ${META_SPEND:-0} + ${GADS_SPEND:-0} }")
TOTAL_AD_REV=$(awk "BEGIN { printf \"%.2f\", ${META_REV:-0} + ${GADS_REV:-0} }")
BLENDED_ROAS=$(awk "BEGIN { if (${TOTAL_AD_SPEND:-0} > 0) printf \"%.2f\", ${TOTAL_AD_REV:-0} / ${TOTAL_AD_SPEND:-1}; else print \"—\" }")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  UNIFIED ATTRIBUTION — Last 7 Days"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
printf "| %-14s | %-10s | %-12s | %-12s | %-8s |\n" "Platform" "Spend" "Conversions" "Revenue" "ROAS"
printf "|%s|%s|%s|%s|%s|\n" "----------------" "------------" "--------------" "--------------" "----------"
printf "| %-14s | \$%-9s | %-12s | \$%-11s | %-8s |\n" "Meta Ads" "$META_SPEND" "$META_CONV" "$META_REV" "${META_ROAS}x"
printf "| %-14s | \$%-9s | %-12s | \$%-11s | %-8s |\n" "Google Ads" "$(printf "%.2f" ${GADS_SPEND})" "$GADS_CONV" "$(printf "%.2f" ${GADS_REV})" "${GADS_ROAS}x"
printf "| %-14s | %-10s | %-12s | %-12s | %-8s |\n" "Klaviyo" "—" "—" "${KLAVIYO_REV}" "—"
printf "| %-14s | %-10s | %-12s | %-12s | %-8s |\n" "GA4 (organic)" "—" "$GA4_CONV" "\$${GA4_REV}" "—"
printf "|%s|%s|%s|%s|%s|\n" "----------------" "------------" "--------------" "--------------" "----------"
printf "| %-14s | \$%-9s | %-12s | \$%-11s | %-8s |\n" "TOTAL (ads)" "$TOTAL_AD_SPEND" "—" "$TOTAL_AD_REV" "${BLENDED_ROAS}x"

dashboard(默认 — 无参数)

并行运行所有部分,然后呈现统一仪表板。

# Run the pre-gathered data script
"${CLAUDE_PLUGIN_ROOT}/bin/ops-marketing-dash" 2>/dev/null

解析 JSON 输出并显示:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 MARKETING DASHBOARD — [date]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Email (Klaviyo)  [N] subs  |  [X]% open rate  |  $[X] attributed
 Paid (Meta)      $[X] spent  |  [X]x ROAS  |  [N] purchases
 Paid (Google)    $[X] spent  |  [X]x ROAS  |  [N] conversions
 Organic (GA4)    [N] sessions  |  [X]% CVR  |  $[X] revenue
 SEO (GSC)        [N] clicks  |  [N] impressions  |  [X] avg pos
 Instagram        [N] followers  |  [N] reach (7d)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Marketing Health Score: [N]/100  ([status: Healthy/Warning/Critical])
 Blended ROAS: [X]x  Top channel: [channel]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

营销健康度评分计算(0-100,显示在仪表板底部):

  • 综合 ROAS ≥ 3x:+30 分;1-3x:+15 分;< 1x:+0 分
  • 邮件打开率 ≥ 20%:+20 分;10-20%:+10 分;< 10%:+0 分
  • 渠道多样性(活跃平台 3 个以上):+20 分;2 个平台:+10 分;1 个:+0 分
  • GA4 CVR ≥ 2%:+20 分;1-2%:+10 分;< 1%:+0 分
  • 自然 SEO 点击量 > 1,000/月:+10 分;100-1000:+5 分;< 100:+0 分

状态阈值:≥70 = 健康,40-69 = 警告,< 40 = 严重。

对于缺少凭据的任何渠道,显示 [not configured — /ops:marketing setup]。


setup

在询问任何信息之前,自动扫描所有来源中是否已有凭据。在单个后台批次中运行:

# Env vars
printenv KLAVIYO_API_KEY KLAVIYO_PRIVATE_KEY META_ACCESS_TOKEN FACEBOOK_ACCESS_TOKEN META_AD_ACCOUNT_ID GA4_PROPERTY_ID GA_MEASUREMENT_ID 2>/dev/null
printenv GOOGLE_ADS_DEVELOPER_TOKEN GOOGLE_ADS_CLIENT_ID GOOGLE_ADS_CLIENT_SECRET GOOGLE_ADS_REFRESH_TOKEN GOOGLE_ADS_CUSTOMER_ID 2>/dev/null

# Shell profiles
grep -h 'KLAVIYO\|META_\|FACEBOOK\|GA4\|GA_MEASUREMENT\|GOOGLE_ADS' ~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#'

# Doppler — ALL projects, ALL configs
for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do
  for cfg in dev stg prd; do
    doppler secrets --project "$proj" --config "$cfg" --json 2>/dev/null | \
      jq -r --arg proj "$proj" --arg cfg "$cfg" 'to_entries[] | select(.key | test("KLAVIYO|META|FACEBOOK|GA4|GOOGLE|GOOGLE_ADS"; "i")) | "\(.key)=\(.value.computed) (doppler:\($proj)/\($cfg))"'
  done
done

# Dashlane — check for tokens in password entries
dcli password klaviyo --output json 2>/dev/null | jq -r '.[] | select(.password != null and .password != "") | "\(.title): token found"'
dcli password facebook --output json 2>/dev/null | jq -r '.[] | select(.password != null and .password != "") | "\(.title): token found"'
dcli password meta --output json 2>/dev/null | jq -r '.[] | select(.password != null and .password != "") | "\(.title): token found"'
dcli password "google ads" --output json 2>/dev/null | jq -r '.[] | select(.password != null and .password != "") | "\(.title): token found"'

# Keychain
security find-generic-password -s "klaviyo-api-key" -w 2>/dev/null
security find-generic-password -s "meta-ads-token" -w 2>/dev/null
security find-generic-password -s "google-ads-refresh-token" -w 2>/dev/null

# gcloud ADC (for GA4 + Search Console)
gcloud auth application-default print-access-token 2>/dev/null | head -c 10 && echo "...gcloud-ok"

# Chrome history — reveals account identity
sqlite3 ~/Library/Application\ Support/Google/Chrome/Default/History \
  "SELECT DISTINCT url FROM urls WHERE url LIKE '%klaviyo.com%' OR url LIKE '%analytics.google.com%' OR url LIKE '%business.facebook.com%' OR url LIKE '%search.google.com/search-console%' OR url LIKE '%ads.google.com%' ORDER BY last_visit_time DESC LIMIT 15" 2>/dev/null

# Existing prefs + userConfig
jq -r '.marketing // empty' "$PREFS_PATH" 2>/dev/null

在询问任何信息之前,先展示所有发现结果。仅提示提供任何来源中都未找到的值。使用 run_in_background: true 运行所有连通性测试。

**Klaviyo:**如果找到 KLAVIYO_PRIVATE_KEY 或包含 ck_* 密钥的 Dashlane 条目,则直接使用。注意:Klaviyo 私有密钥以 ck_(较旧)或 pk_(较新)开头。连通性测试:curl -s -H "Authorization: Klaviyo-API-Key $KEY" -H "revision: 2024-10-15" "https://a.klaviyo.com/api/lists?page[size]=1"。

**Meta Ads:**如果在 Doppler 中找到,则直接使用。需要同时提供 META_ACCESS_TOKEN 和 META_AD_ACCOUNT_ID。连通性测试:graph.facebook.com/v20.0/$AD_ACCOUNT_ID/campaigns?limit=1。

**GA4:**只需要 Property ID + gcloud ADC。如果尚未设置 gcloud ADC,则在后台运行 gcloud auth application-default login(会打开浏览器)。检查 Chrome 历史记录中的 GA4 属性网址,以自动检测属性 ID。

**Search Console:**只需要网站 URL + gcloud ADC。检查 Chrome 历史记录中的 search.google.com/search-console 网址,以自动检测网站。

**Google Ads:**其凭据配置比其他营销凭据更复杂,需要 3 项内容:(1) Google Ads MCC → Tools & Settings → API Center 中的开发者令牌,(2) Google Cloud Console 中的 OAuth2 客户端 ID + 密钥(Desktop app 类型,已启用 Google Ads API),(3) 通过浏览器 OAuth 流程获取的刷新令牌。配置流程:先收集开发者令牌和客户端凭据,然后打开浏览器授权 URL,由用户粘贴授权码,通过 curl 交换获取刷新令牌,接着列出可访问的客户账号并让用户选择。连通性测试:curl -s -X GET "https://googleads.googleapis.com/v23/customers:listAccessibleCustomers" -H "Authorization: Bearer $TOKEN" -H "developer-token: $DEV_TOKEN" ——预期返回包含 resourceNames 数组的 JSON。

通过 userConfig(首选)或 Doppler 保存。报告格式:[service] ✓ connected 或 [service] ✗ invalid key — [error]。

评论

支持 Markdown