关于 ops-marketing
ops-marketing 是用于查看营销表现并管理营销活动的 Claude skill,覆盖邮件、广告、分析、SEO 和社交渠道。它提供分渠道报告,并将请求导向相应的数据面板或管理流程。
主要功能
- 01Klaviyo 邮件指标
- 02Meta Ads 表现与管理
- 03Google Ads 面板和广告活动
- 04GA4 分析与归因
- 05Search Console 与社交报告
适用场景
- 01查看订阅者、邮件活动和流程情况
- 02检查广告支出与活动表现
- 03分析网站会话、转化和搜索指标
- 04按渠道处理营销工作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ davepoon/ buildwithclaude --skill ops-marketing安装后在 Claude Code 中输入/ops-marketing确认已加载。
详细介绍
OPS ► 营销指挥中心
运行时上下文
执行前,加载可用上下文:
-
偏好设置:读取
${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.jsontimezone— 正确显示所有时间戳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 凭据
-
守护进程健康状态:读取
${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json- 如果
action_needed不为 null → 在运行任何渠道查询前展示该内容
- 如果
-
密钥:通过 userConfig → 环境变量 → Doppler MCP(
mcp__doppler__*)→ Doppler CLI 回退方案解析 API 密钥(参见下文“凭据解析”部分)
CLI/API 参考
Klaviyo REST API
| 端点 | 方法 | 描述 |
|---|---|---|
https://a.klaviyo.com/api/lists/?fields[list]=name,id,profile_count | GET | 所有列表 + 订阅者数量 |
https://a.klaviyo.com/api/campaigns/?filter=equals(messages.channel,'email')&sort=-created_at | GET | 最近的营销活动 |
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_7d | GET | 账户级广告支出 |
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_account | GET | 已关联的 Instagram 账户 |
身份验证标头:Authorization: Bearer ${META_TOKEN}
Google Analytics 4(Data API)
| 端点 | 方法 | 描述 |
|---|---|---|
https://analyticsdata.googleapis.com/v1beta/properties/${GA4_PROPERTY}:runReport | POST | 运行自定义报告 |
身份验证: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/query | POST | 搜索效果数据 |
身份验证:与 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,klaviyo | Klaviyo 邮件指标 |
| ads,meta | Meta Ads 表现(只读概览) |
| meta-manage,meta create-campaign,meta target,meta creative,meta rules,meta audiences,meta advantage | Meta Ads 广告系列管理(参见 ## meta-manage 部分) |
| google-ads,gads | Google Ads 仪表板 + 广告系列管理(参见 ## google-ads 部分) |
| analytics,ga4 | GA4 会话 + 转化 |
| ga4 realtime,ga4 funnel,ga4 cohort,ga4 audience,ga4 pivot | GA4 高级分析(参见 ## ga4-advanced 部分) |
| seo,gsc | Search Console 指标 |
| social | 社交媒体聚合器 |
| instagram,instagram post,instagram reel,instagram story,instagram insights,instagram demographics | Instagram 发布 + 洞察(参见 ## 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 个选项):
- 广告系列目标 —
[OUTCOME_TRAFFIC, OUTCOME_SALES, OUTCOME_LEADS, OUTCOME_AWARENESS] - 每日预算(美元,自由文本)
- 广告系列名称(自由文本)
然后通过 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 收集信息:
- 目标国家/地区(以逗号分隔的 ISO 代码,例如
US,CA,GB)— 自由文本 - 年龄范围:
[18-34, 25-54, 35-65, 18-65] - 性别:
[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 收集信息:
- 图片文件路径或 URL(自由文本)
- 要关联该广告的广告组 ID(自由文本)
- 正文(广告文案,自由文本 — 建议最多 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 提示):
- 规则类型:
[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 提示:
- 受众类型:
[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 收集:
- 每日预算(美元,自由输入)
- 广告系列名称(自由输入)
然后确认:"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 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 收集:
- 图片 URL(可公开访问的 HTTPS URL)——自由文本
- 说明文字——自由文本
# 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 收集:
- 视频 URL(HTTPS)——自由文本
- 说明文字——自由文本
# 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 收集:
- 内容 URL(HTTPS 图片或视频)——自由文本
- 内容类型:
[图片 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、recs | Google 提供的预算优化建议 |
| campaigns、manage | 广告系列管理——列出、创建、暂停、启用、调整预算 |
| keywords、kw、keyword-planner | Keyword 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 逐项向用户收集自由文本:
- 广告系列名称
- 每日预算(美元)(转换为微单位:
BUDGET_MICROS=$(awk "BEGIN {printf \"%d\", $DOLLARS * 1000000}")) - 渠道类型——使用以下选项调用 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 收集:
- 广告组名称(自由文本)
- 默认 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 收集:
- 关键词文本(自由文本)
- 匹配类型——使用以下选项调用 AskUserQuestion:
[Broad, Phrase, Exact]映射为:BROAD、PHRASE、EXACT - 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)中的活跃营销活动。
并行运行:
- Klaviyo 活跃营销活动(状态:draft + scheduled + sending)
- Meta Ads 活跃营销活动(状态 ACTIVE)——复用
META_TOKEN/META_ACCOUNT - Google Ads 活跃营销活动(状态 ENABLED)——如果已配置,则复用
GADS_*凭据 - 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]。