Initial commit: moreminimore-service content pipeline

- 9 mm* skills (orchestrator, article, social, publish, analytics)
- 6 dependency skills (content-writer, geo-optimizer, etc.)
- 3 analytics scripts (GSC, Google Ads, Meta Ads)
- Config template + setup guide
- SOUL-MM.md persona extension
- OrbitOS integration reference
This commit is contained in:
Kunthawat Greethong
2026-07-02 10:01:53 +07:00
commit 7b5af16f6d
36 changed files with 7803 additions and 0 deletions

87
scripts/README.md Normal file
View File

@@ -0,0 +1,87 @@
# Platform API Scripts — คู่มือการติดตั้ง
scripts/ เหล่านี้เป็น bridge สำหรับเชื่อมต่อกับ platform APIs
ณ ตอนนี้เป็น **placeholder** — ต้องติดตั้ง dependencies ก่อนใช้งาน
## Prerequisites (ทั่วไป)
```bash
# Hermes ต้องมี tools เหล่านี้
# web_search สำหรับค้นหาข้อมูล
# terminal สำหรับรัน scripts
```
## Google Ads API
### Dependencies
```bash
pip install google-ads
```
### Authentication
1. ไปที่ https://console.cloud.google.com/ → สร้าง Project
2. Enable Google Ads API
3. สร้าง OAuth 2.0 credentials → ดาวน์โหลด JSON
4. ไปที่ https://ads.google.com/ → Tools → API Center
- ขอ Developer Token (ต้องผ่าน basic approval)
5. ตั้งค่า environment:
```bash
export GOOGLE_ADS_DEVELOPER_TOKEN="your-token"
export GOOGLE_ADS_CLIENT_ID="your-client-id"
export GOOGLE_ADS_CLIENT_SECRET="your-client-secret"
export GOOGLE_ADS_REFRESH_TOKEN="your-refresh-token"
export GOOGLE_ADS_LOGIN_CUSTOMER_ID="manager-account-id"
```
### ทางเลือก: Google Ads MCP Server
https://github.com/googleads/google-ads-mcp
ใช้กับ Hermes/Claude โดยตรง ไม่ต้องเขียน script แยก
## Meta Ads API
### Dependencies
```bash
pip install facebook-business
```
### Authentication
1. ไปที่ https://developers.facebook.com/ → สร้าง App
2. เพิ่ม Marketing API product
3. ต้องผ่าน business verification (ใช้บัตรประชาชน/เอกบริษัท)
4. Generate access token:
- permissions: `ads_read`, `ads_management`, `business_management`
5. ตั้งค่า environment:
```bash
export META_ACCESS_TOKEN="your-token"
export META_AD_ACCOUNT_ID="act_xxxxxxxx"
export META_APP_ID="your-app-id"
export META_APP_SECRET="your-app-secret"
```
## Google Search Console API
### Dependencies
```bash
pip install google-auth google-auth-oauthlib google-auth-httplib2
```
### Authentication
ใช้ gcloud application-default credentials:
```bash
gcloud auth application-default login \
--scopes=https://www.googleapis.com/auth/webmasters.readonly
```
### หมายสำคัญ
- **seo-analysis skill** มี scripts GSC ครบถ้วนที่ `~/.hermes/skills/seo-analysis/scripts/`
- สามารถ adapt มาใช้กับ multi-client ได้
- แต่ละ client ต้องเพิ่ม service account email ใน GSC property → Settings → Users
## แนวทางการสร้าง scripts จริง
เมื่อพร้อม implement scripts จริง:
1. **Copy pattern** จาก seo-analysis skill scripts — มีโครงสร้างครบ (analyze_gsc.py, url_inspection.py, ฯลฯ)
2. **แยก config** ต่อ client — ใช้ .env หรือ config.json ต่อ client
3. **Output format** — JSON ที่ orchestrator อ่านได้ (state.json friendly)
4. **Error handling** — ถ้า token หมดอายุ หรือ API quota หมด ให้แจ้ง orchestrator

37
scripts/google-ads.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Google Ads API Bridge Script
# =============================
# Placeholder — implement when Google Ads API access is configured
#
# Required setup:
# 1. Google Ads API developer token
# 2. OAuth 2.0 client credentials
# 3. Google Ads MCP server (recommended) or direct API client
# https://github.com/googleads/google-ads-mcp
#
# Usage:
# ./google-ads.sh <action> <customer-id> [options]
#
# Actions:
# audit Run full Google Ads audit (uses ads-google skill data)
# campaigns List active campaign structure
# keywords Get keyword performance data
# create Create new campaign from campaign-brief.md
# stats Get performance stats for a date range
#
# This script is called by the moreminimore-orchestrator when
# campaign execution is requested.
echo "⚠️ Google Ads API script not yet implemented."
echo ""
echo "Required before use:"
echo " 1. Create OAuth 2.0 credentials in Google Cloud Console"
echo " 2. Apply for Google Ads API developer token"
echo " 3. Install google-ads Python client: pip install google-ads"
echo " 4. Set up authentication: export GOOGLE_ADS_JSON_KEY=/path/to/key.json"
echo ""
echo "Alternative: Use Google Ads MCP server for automated data collection"
echo " https://github.com/googleads/google-ads-mcp"
echo ""
echo "See: /Users/kunthawat/Gitea/moreminimore-service-system/scripts/README.md"
exit 1

32
scripts/meta-ads.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Meta Ads API Bridge Script
# ===========================
# Placeholder — implement when Meta Ads API access is configured
#
# Required setup:
# 1. Facebook App with Marketing API permission
# 2. Access token with ads_read + ads_manage scope
# 3. Ad account ID
#
# Usage:
# ./meta-ads.sh <action> <ad-account-id> [options]
#
# Actions:
# audit Run full Meta Ads audit (uses ads-meta skill data)
# campaigns List active campaign structure
# create Create new campaign from campaign-brief.md
# stats Get performance stats for a date range
#
# This script is called by the moreminimore-orchestrator when
# campaign execution is requested.
echo "⚠️ Meta Ads API script not yet implemented."
echo ""
echo "Required before use:"
echo " 1. Create Facebook App at https://developers.facebook.com/"
echo " 2. Get Marketing API access (needs business verification)"
echo " 3. Generate long-lived access token"
echo " 4. Install facebook-business Python SDK: pip install facebook-business"
echo ""
echo "See: /Users/kunthawat/Gitea/moreminimore-service-system/scripts/README.md"
exit 1

419
scripts/mm_google_ads.py Normal file
View File

@@ -0,0 +1,419 @@
#!/usr/bin/env python3
"""
Google Ads API for moreminimore-service.
Reads clients-config.json, pulls campaign/keyword/ad data.
Usage:
python3 mm_google_ads.py --client moreminimore campaigns
python3 mm_google_ads.py --client moreminimore keywords
python3 mm_google_ads.py --client moreminimore stats --days 30
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
def load_config():
"""Load clients-config.json"""
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
with open(config_path) as f:
return json.load(f)
def find_client(config, client_id):
"""Find client by ID"""
for client in config.get("clients", []):
if client["id"] == client_id:
return client
return None
def get_google_ads_config(config, client):
"""Get Google Ads credentials from config"""
google = config.get("global", {}).get("google", {})
developer_token = google.get("developer_token", "")
manager_account_id = google.get("manager_account_id", "")
customer_id = client.get("analytics", {}).get("google_ads_customer_id", "")
if not developer_token:
print("ERROR: No developer_token in global.google config", file=sys.stderr)
sys.exit(1)
if not customer_id:
print(f"ERROR: No google_ads_customer_id for client '{client['id']}'", file=sys.stderr)
sys.exit(1)
# Remove hyphens from customer_id
customer_id = customer_id.replace("-", "")
return {
"developer_token": developer_token,
"manager_account_id": manager_account_id.replace("-", ""),
"customer_id": customer_id,
}
def get_access_token(key_path):
"""Get access token from service account key using openssl for JWT signing"""
import subprocess
import base64
import time
if not os.path.exists(key_path):
print(f"ERROR: Service account key not found: {key_path}", file=sys.stderr)
sys.exit(1)
try:
with open(key_path) as f:
key_data = json.load(f)
# JWT header
header = {"alg": "RS256", "typ": "JWT"}
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
# JWT payload
now = int(time.time())
payload = {
"iss": key_data["client_email"],
"scope": "https://www.googleapis.com/auth/adwords",
"aud": "https://oauth2.googleapis.com/token",
"iat": now,
"exp": now + 3600,
}
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
# Sign with openssl
signing_input = f"{header_b64}.{payload_b64}"
# Write private key to temp file
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as kf:
kf.write(key_data["private_key"])
key_file = kf.name
try:
# Sign with openssl
result = subprocess.run(
["openssl", "dgst", "-sha256", "-sign", key_file],
input=signing_input.encode(),
capture_output=True,
timeout=10,
)
if result.returncode != 0:
print(f"ERROR: openssl signing failed: {result.stderr.decode()}", file=sys.stderr)
sys.exit(1)
signature_b64 = base64.urlsafe_b64encode(result.stdout).decode().rstrip('=')
signed_jwt = f"{signing_input}.{signature_b64}"
finally:
os.unlink(key_file)
# Exchange JWT for access token
token_url = "https://oauth2.googleapis.com/token"
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt,
}).encode()
req = urllib.request.Request(token_url, data=data, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
with urllib.request.urlopen(req, timeout=15) as resp:
token_data = json.loads(resp.read().decode())
return token_data["access_token"]
except Exception as e:
print(f"ERROR: Failed to get access token: {e}", file=sys.stderr)
print(f" Key file: {key_path}", file=sys.stderr)
sys.exit(1)
def google_ads_query(customer_id, query, developer_token, access_token, manager_account_id=None):
"""Execute a GAQL query against Google Ads API"""
url = f"https://googleads.googleapis.com/v24/customers/{customer_id}/googleAds:searchStream"
headers = {
"Authorization": f"Bearer {access_token}",
"developer-token": developer_token,
"Content-Type": "application/json",
}
if manager_account_id:
headers["login-customer-id"] = manager_account_id
body = json.dumps({"query": query}).encode()
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
print(f"ERROR: Google Ads API returned {e.code}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(json.dumps(error_json, indent=2), file=sys.stderr)
except:
print(error_body[:500], file=sys.stderr)
sys.exit(1)
def list_campaigns(ads_config, access_token):
"""List active campaigns"""
query = """
SELECT
campaign.id,
campaign.name,
campaign.status,
campaign.advertising_channel_type,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions
FROM campaign
WHERE campaign.status = 'ENABLED'
ORDER BY campaign.name
"""
result = google_ads_query(
ads_config["customer_id"],
query,
ads_config["developer_token"],
access_token,
ads_config.get("manager_account_id"),
)
return result
def list_keywords(ads_config, access_token):
"""List active keywords with performance"""
query = """
SELECT
ad_group.name,
ad_group_criterion.keyword.text,
ad_group_criterion.keyword.match_type,
ad_group_criterion.quality_info.quality_score,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions,
metrics.ctr,
metrics.average_cpc
FROM keyword_view
WHERE ad_group_criterion.status = 'ENABLED'
ORDER BY metrics.impressions DESC
LIMIT 100
"""
result = google_ads_query(
ads_config["customer_id"],
query,
ads_config["developer_token"],
access_token,
ads_config.get("manager_account_id"),
)
return result
def get_stats(ads_config, access_token, days=30):
"""Get campaign performance stats"""
query = f"""
SELECT
campaign.name,
segments.date,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions,
metrics.ctr,
metrics.average_cpc,
metrics.cost_per_conversion
FROM campaign
WHERE campaign.status = 'ENABLED'
AND segments.date DURING LAST_{days}_DAYS
ORDER BY segments.date DESC
"""
result = google_ads_query(
ads_config["customer_id"],
query,
ads_config["developer_token"],
access_token,
ads_config.get("manager_account_id"),
)
return result
def format_currency(micros):
"""Format micros to currency"""
if not micros:
return "฿0"
return f"฿{int(micros) / 1_000_000:,.2f}"
def format_number(n):
"""Format number with commas"""
if not n:
return "0"
return f"{int(n):,}"
def display_campaigns(data):
"""Display campaign data in a table"""
if not data or not data[0].get("results"):
print("ไม่พบ campaign ที่ active")
return
results = data[0]["results"]
print(f"\n📋 Active Campaigns ({len(results)})")
print("-" * 100)
print(f"{'Name':<30} {'Type':<15} {'Impressions':>12} {'Clicks':>10} {'Cost':>12} {'Conv':>8}")
print("-" * 100)
for row in results:
campaign = row.get("campaign", {})
metrics = row.get("metrics", {})
name = campaign.get("name", "")[:28]
channel = campaign.get("advertisingChannelType", "")
print(f"{name:<30} {channel:<15} {format_number(metrics.get('impressions', 0)):>12} "
f"{format_number(metrics.get('clicks', 0)):>10} "
f"{format_currency(metrics.get('costMicros', 0)):>12} "
f"{format_number(metrics.get('conversions', 0)):>8}")
def display_keywords(data):
"""Display keyword data in a table"""
if not data or not data[0].get("results"):
print("ไม่พบ keyword ที่ active")
return
results = data[0]["results"]
print(f"\n🔑 Keywords ({len(results)})")
print("-" * 110)
print(f"{'Keyword':<35} {'Match':<10} {'QS':>4} {'Impressions':>12} {'Clicks':>10} {'CTR':>8} {'CPC':>10}")
print("-" * 110)
for row in results:
criterion = row.get("adGroupCriterion", {})
keyword = criterion.get("keyword", {})
metrics = row.get("metrics", {})
qs = criterion.get("qualityInfo", {}).get("qualityScore", "")
kw_text = keyword.get("text", "")[:33]
match_type = keyword.get("matchType", "")
ctr = float(metrics.get("ctr", 0)) * 100
avg_cpc = int(metrics.get("averageCpc", 0)) / 1_000_000
print(f"{kw_text:<35} {match_type:<10} {qs:>4} "
f"{format_number(metrics.get('impressions', 0)):>12} "
f"{format_number(metrics.get('clicks', 0)):>10} "
f"{ctr:>7.2f}% "
f"฿{avg_cpc:>8.2f}")
def display_stats(data):
"""Display stats in a table"""
if not data or not data[0].get("results"):
print("ไม่พบข้อมูลสถิติ")
return
results = data[0]["results"]
# Aggregate by campaign
campaigns = {}
for row in results:
campaign_name = row.get("campaign", {}).get("name", "")
metrics = row.get("metrics", {})
if campaign_name not in campaigns:
campaigns[campaign_name] = {
"impressions": 0, "clicks": 0, "cost": 0, "conversions": 0
}
campaigns[campaign_name]["impressions"] += int(metrics.get("impressions", 0))
campaigns[campaign_name]["clicks"] += int(metrics.get("clicks", 0))
campaigns[campaign_name]["cost"] += int(metrics.get("costMicros", 0))
campaigns[campaign_name]["conversions"] += float(metrics.get("conversions", 0))
print(f"\n📊 Campaign Stats")
print("-" * 100)
print(f"{'Campaign':<30} {'Impressions':>12} {'Clicks':>10} {'Cost':>12} {'Conv':>8} {'CTR':>8} {'CPA':>12}")
print("-" * 100)
for name, m in campaigns.items():
ctr = (m["clicks"] / m["impressions"] * 100) if m["impressions"] > 0 else 0
cpa = (m["cost"] / m["conversions"]) if m["conversions"] > 0 else 0
print(f"{name[:28]:<30} {format_number(m['impressions']):>12} "
f"{format_number(m['clicks']):>10} "
f"{format_currency(m['cost']):>12} "
f"{int(m['conversions']):>8} "
f"{ctr:>7.2f}% "
f"{format_currency(cpa):>12}")
def main():
parser = argparse.ArgumentParser(description="Google Ads API for moreminimore-service")
parser.add_argument("--client", required=True, help="Client ID from clients-config.json")
parser.add_argument("action", choices=["campaigns", "keywords", "stats"], help="Action to perform")
parser.add_argument("--days", type=int, default=30, help="Number of days for stats (default: 30)")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
args = parser.parse_args()
# Load config
config = load_config()
# Find client
client = find_client(config, args.client)
if not client:
print(f"ERROR: Client '{args.client}' not found", file=sys.stderr)
sys.exit(1)
# Get Google Ads config
ads_config = get_google_ads_config(config, client)
print(f"📊 Google Ads — {client['display_name']}")
print(f" Customer ID: {ads_config['customer_id']}")
print()
# Get access token from service account key
key_path = config.get("global", {}).get("google", {}).get("service_account_key", "")
if not key_path:
print("ERROR: No service_account_key in global.google config", file=sys.stderr)
sys.exit(1)
access_token = get_access_token(os.path.expanduser(key_path))
# Execute action
if args.action == "campaigns":
data = list_campaigns(ads_config, access_token)
if args.json:
print(json.dumps(data, indent=2))
else:
display_campaigns(data)
elif args.action == "keywords":
data = list_keywords(ads_config, access_token)
if args.json:
print(json.dumps(data, indent=2))
else:
display_keywords(data)
elif args.action == "stats":
data = get_stats(ads_config, access_token, args.days)
if args.json:
print(json.dumps(data, indent=2))
else:
display_stats(data)
if __name__ == "__main__":
main()

319
scripts/mm_gsc.py Normal file
View File

@@ -0,0 +1,319 @@
#!/usr/bin/env python3
"""
GSC Analytics for moreminimore-service.
Reads clients-config.json, pulls GSC data using service account key.
Usage:
python3 mm_gsc.py --client moreminimore --days 90
python3 mm_gsc.py --client moreminimore --days 28
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from datetime import date, timedelta
def load_config():
"""Load clients-config.json"""
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
if not os.path.exists(config_path):
print(f"ERROR: Config not found at {config_path}", file=sys.stderr)
sys.exit(1)
with open(config_path) as f:
return json.load(f)
def find_client(config, client_id):
"""Find client by ID"""
for client in config.get("clients", []):
if client["id"] == client_id:
return client
return None
def get_gsc_property(client):
"""Get GSC property from client config"""
analytics = client.get("analytics", {})
gsc_property = analytics.get("gsc_property", "")
if not gsc_property:
print(f"ERROR: No gsc_property set for client '{client['id']}'", file=sys.stderr)
sys.exit(1)
return gsc_property
def get_access_token(key_path):
"""Get access token from service account key using openssl for JWT signing"""
import subprocess
import base64
import time
if not os.path.exists(key_path):
print(f"ERROR: Service account key not found: {key_path}", file=sys.stderr)
sys.exit(1)
try:
with open(key_path) as f:
key_data = json.load(f)
# JWT header
header = {"alg": "RS256", "typ": "JWT"}
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
# JWT payload
now = int(time.time())
payload = {
"iss": key_data["client_email"],
"scope": "https://www.googleapis.com/auth/webmasters.readonly",
"aud": "https://oauth2.googleapis.com/token",
"iat": now,
"exp": now + 3600,
}
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
# Sign with openssl
signing_input = f"{header_b64}.{payload_b64}"
# Write private key to temp file
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as kf:
kf.write(key_data["private_key"])
key_file = kf.name
try:
# Sign with openssl
result = subprocess.run(
["openssl", "dgst", "-sha256", "-sign", key_file],
input=signing_input.encode(),
capture_output=True,
timeout=10,
)
if result.returncode != 0:
print(f"ERROR: openssl signing failed: {result.stderr.decode()}", file=sys.stderr)
sys.exit(1)
signature_b64 = base64.urlsafe_b64encode(result.stdout).decode().rstrip('=')
signed_jwt = f"{signing_input}.{signature_b64}"
finally:
os.unlink(key_file)
# Exchange JWT for access token
token_url = "https://oauth2.googleapis.com/token"
data = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt,
}).encode()
req = urllib.request.Request(token_url, data=data, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
with urllib.request.urlopen(req, timeout=15) as resp:
token_data = json.loads(resp.read().decode())
return token_data["access_token"]
except Exception as e:
print(f"ERROR: Failed to get access token: {e}", file=sys.stderr)
print(f" Key file: {key_path}", file=sys.stderr)
sys.exit(1)
def gsc_query(site_property, access_token, request_body):
"""Execute a GSC API query"""
# URL encode the site property
import urllib.parse
site_encoded = urllib.parse.quote(site_property, safe='')
url = f"https://searchconsole.googleapis.com/webmasters/v3/sites/{site_encoded}/searchAnalytics/query"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
body = json.dumps(request_body).encode()
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
print(f"ERROR: GSC API returned {e.code}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(json.dumps(error_json, indent=2), file=sys.stderr)
except:
print(error_body[:500], file=sys.stderr)
sys.exit(1)
def get_top_queries(site_property, access_token, days=28):
"""Get top queries"""
end_date = date.today() - timedelta(days=3) # GSC data lags ~3 days
start_date = end_date - timedelta(days=days)
result = gsc_query(site_property, access_token, {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"dimensions": ["query"],
"rowLimit": 50,
"type": "web",
})
return result.get("rows", [])
def get_top_pages(site_property, access_token, days=28):
"""Get top pages"""
end_date = date.today() - timedelta(days=3)
start_date = end_date - timedelta(days=days)
result = gsc_query(site_property, access_token, {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"dimensions": ["page"],
"rowLimit": 50,
"type": "web",
})
return result.get("rows", [])
def get_summary(site_property, access_token, days=28):
"""Get overall summary"""
end_date = date.today() - timedelta(days=3)
start_date = end_date - timedelta(days=days)
result = gsc_query(site_property, access_token, {
"startDate": start_date.isoformat(),
"endDate": end_date.isoformat(),
"dimensions": ["date"],
"type": "web",
})
rows = result.get("rows", [])
total_clicks = sum(r.get("clicks", 0) for r in rows)
total_impressions = sum(r.get("impressions", 0) for r in rows)
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
avg_position = sum(r.get("position", 0) * r.get("impressions", 0) for r in rows) / total_impressions if total_impressions > 0 else 0
return {
"clicks": total_clicks,
"impressions": total_impressions,
"ctr": avg_ctr,
"position": avg_position,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
}
def display_summary(summary):
"""Display summary"""
print(f"📊 Summary ({summary['start_date']} to {summary['end_date']})")
print("-" * 60)
print(f" Clicks: {summary['clicks']:,}")
print(f" Impressions: {summary['impressions']:,}")
print(f" CTR: {summary['ctr']:.2f}%")
print(f" Avg Position: {summary['position']:.1f}")
def display_queries(rows):
"""Display top queries"""
if not rows:
print("\nไม่พบ queries")
return
print(f"\n🔑 Top Queries ({len(rows)})")
print("-" * 90)
print(f"{'Query':<45} {'Clicks':>10} {'Impressions':>12} {'CTR':>8} {'Position':>10}")
print("-" * 90)
for row in rows[:30]:
keys = row.get("keys", [])
query = keys[0] if keys else ""
ctr = row.get("ctr", 0) * 100
print(f"{query[:43]:<45} {row.get('clicks', 0):>10,} "
f"{row.get('impressions', 0):>12,} "
f"{ctr:>7.2f}% "
f"{row.get('position', 0):>10.1f}")
def display_pages(rows):
"""Display top pages"""
if not rows:
print("\nไม่พบ pages")
return
print(f"\n📄 Top Pages ({len(rows)})")
print("-" * 90)
print(f"{'Page':<55} {'Clicks':>10} {'Impressions':>12} {'CTR':>8}")
print("-" * 90)
for row in rows[:20]:
keys = row.get("keys", [])
page = keys[0] if keys else ""
ctr = row.get("ctr", 0) * 100
print(f"{page[:53]:<55} {row.get('clicks', 0):>10,} "
f"{row.get('impressions', 0):>12,} "
f"{ctr:>7.2f}%")
def main():
parser = argparse.ArgumentParser(description="GSC Analytics for moreminimore-service")
parser.add_argument("--client", required=True, help="Client ID from clients-config.json")
parser.add_argument("--days", type=int, default=90, help="Number of days to analyze (default: 90)")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
args = parser.parse_args()
# Load config
config = load_config()
# Find client
client = find_client(config, args.client)
if not client:
print(f"ERROR: Client '{args.client}' not found in config", file=sys.stderr)
print(f"Available clients: {[c['id'] for c in config.get('clients', [])]}", file=sys.stderr)
sys.exit(1)
# Get GSC property
gsc_property = get_gsc_property(client)
# Get service account key
key_path = config.get("global", {}).get("google", {}).get("service_account_key", "")
if not key_path:
print("ERROR: No service_account_key in global.google config", file=sys.stderr)
sys.exit(1)
print(f"📊 GSC Analytics for: {client['display_name']}")
print(f" Property: {gsc_property}")
print(f" Period: {args.days} days")
print()
# Get access token
access_token = get_access_token(os.path.expanduser(key_path))
# Get data
summary = get_summary(gsc_property, access_token, args.days)
queries = get_top_queries(gsc_property, access_token, args.days)
pages = get_top_pages(gsc_property, access_token, args.days)
if args.json:
result = {
"summary": summary,
"queries": queries,
"pages": pages,
}
print(json.dumps(result, indent=2))
else:
display_summary(summary)
display_queries(queries)
display_pages(pages)
if __name__ == "__main__":
main()

303
scripts/mm_meta_ads.py Normal file
View File

@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""
Meta Ads API for moreminimore-service.
Reads clients-config.json, pulls campaign/ad set/ad data.
Usage:
python3 mm_meta_ads.py --client moreminimore campaigns
python3 mm_meta_ads.py --client moreminimore adsets
python3 mm_meta_ads.py --client moreminimore stats --days 30
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
import urllib.parse
from datetime import datetime, timedelta
def load_config():
"""Load clients-config.json"""
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
with open(config_path) as f:
return json.load(f)
def find_client(config, client_id):
"""Find client by ID"""
for client in config.get("clients", []):
if client["id"] == client_id:
return client
return None
def get_meta_config(config, client):
"""Get Meta Ads credentials from config"""
meta = config.get("global", {}).get("meta", {})
ad_account_id = client.get("analytics", {}).get("meta_ad_account_id", "")
page_id = client.get("social", {}).get("facebook", {}).get("page_id", "")
page_token = client.get("social", {}).get("facebook", {}).get("page_token", "")
system_user_token = meta.get("system_user_token", "")
# Use page_token for page operations, system_user_token for ad operations
token = page_token or system_user_token
if not token:
print("ERROR: No page_token or system_user_token in config", file=sys.stderr)
sys.exit(1)
if not ad_account_id:
print(f"ERROR: No meta_ad_account_id for client '{client['id']}'", file=sys.stderr)
sys.exit(1)
return {
"token": token,
"ad_account_id": ad_account_id,
"page_id": page_id,
}
def meta_api_get(endpoint, params=None):
"""Make a GET request to Meta Graph API"""
base_url = "https://graph.facebook.com/v21.0"
url = f"{base_url}/{endpoint}"
if params:
url += "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
print(f"ERROR: Meta API returned {e.code}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(json.dumps(error_json, indent=2), file=sys.stderr)
except:
print(error_body[:500], file=sys.stderr)
sys.exit(1)
def meta_api_post(endpoint, data):
"""Make a POST request to Meta Graph API"""
base_url = "https://graph.facebook.com/v21.0"
url = f"{base_url}/{endpoint}"
body = urllib.parse.urlencode(data).encode()
req = urllib.request.Request(url, data=body, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ""
print(f"ERROR: Meta API returned {e.code}", file=sys.stderr)
if error_body:
try:
error_json = json.loads(error_body)
print(json.dumps(error_json, indent=2), file=sys.stderr)
except:
print(error_body[:500], file=sys.stderr)
sys.exit(1)
def list_campaigns(meta_config):
"""List active campaigns"""
result = meta_api_get(
f"act_{meta_config['ad_account_id']}/campaigns",
{
"fields": "id,name,status,objective,daily_budget,lifetime_budget,created_time",
"filtering": json.dumps([{"field": "effective_status", "operator": "IN", "value": ["ACTIVE"]}]),
"access_token": meta_config["token"],
}
)
return result
def list_adsets(meta_config):
"""List active ad sets"""
result = meta_api_get(
f"act_{meta_config['ad_account_id']}/adsets",
{
"fields": "id,name,status,targeting,daily_budget,lifetime_budget,optimization_goal,billing_event,created_time",
"filtering": json.dumps([{"field": "effective_status", "operator": "IN", "value": ["ACTIVE"]}]),
"access_token": meta_config["token"],
}
)
return result
def get_insights(meta_config, days=30, level="campaign"):
"""Get performance insights"""
date_start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
date_end = datetime.now().strftime("%Y-%m-%d")
result = meta_api_get(
f"act_{meta_config['ad_account_id']}/insights",
{
"level": level,
"fields": "campaign_name,adset_name,impressions,clicks,spend,actions,cpm,cpc,ctr,reach,frequency",
"time_range": json.dumps({"since": date_start, "until": date_end}),
"time_increment": "1",
"access_token": meta_config["token"],
}
)
return result
def format_currency(amount):
"""Format currency"""
if not amount:
return "฿0"
return f"฿{float(amount):,.2f}"
def format_number(n):
"""Format number with commas"""
if not n:
return "0"
return f"{int(n):,}"
def display_campaigns(data):
"""Display campaign data"""
if not data or not data.get("data"):
print("ไม่พบ campaign ที่ active")
return
campaigns = data["data"]
print(f"\n📋 Active Campaigns ({len(campaigns)})")
print("-" * 90)
print(f"{'Name':<35} {'Objective':<20} {'Daily Budget':>15} {'Status':<10}")
print("-" * 90)
for c in campaigns:
name = c.get("name", "")[:33]
objective = c.get("objective", "")
daily_budget = c.get("daily_budget", "")
status = c.get("status", "")
budget_str = f"฿{int(daily_budget)/100:,.2f}" if daily_budget else "-"
print(f"{name:<35} {objective:<20} {budget_str:>15} {status:<10}")
def display_adsets(data):
"""Display ad set data"""
if not data or not data.get("data"):
print("ไม่พบ ad set ที่ active")
return
adsets = data["data"]
print(f"\n📋 Active Ad Sets ({len(adsets)})")
print("-" * 100)
print(f"{'Name':<30} {'Optimization':<20} {'Billing':<15} {'Daily Budget':>15}")
print("-" * 100)
for a in adsets:
name = a.get("name", "")[:28]
opt_goal = a.get("optimization_goal", "")
billing = a.get("billing_event", "")
daily_budget = a.get("daily_budget", "")
budget_str = f"฿{int(daily_budget)/100:,.2f}" if daily_budget else "-"
print(f"{name:<30} {opt_goal:<20} {billing:<15} {budget_str:>15}")
def display_stats(data, level="campaign"):
"""Display performance stats"""
if not data or not data.get("data"):
print("ไม่พบข้อมูลสถิติ")
return
rows = data["data"]
# Aggregate by campaign or adset
aggregated = {}
for row in rows:
key = row.get("campaign_name", "") if level == "campaign" else row.get("adset_name", "")
if key not in aggregated:
aggregated[key] = {
"impressions": 0, "clicks": 0, "spend": 0, "reach": 0
}
aggregated[key]["impressions"] += int(row.get("impressions", 0))
aggregated[key]["clicks"] += int(row.get("clicks", 0))
aggregated[key]["spend"] += float(row.get("spend", 0))
aggregated[key]["reach"] += int(row.get("reach", 0))
label = "Campaign" if level == "campaign" else "Ad Set"
print(f"\n📊 {label} Stats")
print("-" * 100)
print(f"{label:<30} {'Impressions':>12} {'Clicks':>10} {'Spend':>12} {'Reach':>12} {'CTR':>8} {'CPC':>10}")
print("-" * 100)
for name, m in aggregated.items():
ctr = (m["clicks"] / m["impressions"] * 100) if m["impressions"] > 0 else 0
cpc = (m["spend"] / m["clicks"]) if m["clicks"] > 0 else 0
print(f"{name[:28]:<30} {format_number(m['impressions']):>12} "
f"{format_number(m['clicks']):>10} "
f"{format_currency(m['spend']):>12} "
f"{format_number(m['reach']):>12} "
f"{ctr:>7.2f}% "
f"{format_currency(cpc):>10}")
def main():
parser = argparse.ArgumentParser(description="Meta Ads API for moreminimore-service")
parser.add_argument("--client", required=True, help="Client ID from clients-config.json")
parser.add_argument("action", choices=["campaigns", "adsets", "stats"], help="Action to perform")
parser.add_argument("--days", type=int, default=30, help="Number of days for stats (default: 30)")
parser.add_argument("--level", choices=["campaign", "adset"], default="campaign", help="Stats level")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
args = parser.parse_args()
# Load config
config = load_config()
# Find client
client = find_client(config, args.client)
if not client:
print(f"ERROR: Client '{args.client}' not found", file=sys.stderr)
sys.exit(1)
# Get Meta config
meta_config = get_meta_config(config, client)
print(f"📊 Meta Ads — {client['display_name']}")
print(f" Ad Account: {meta_config['ad_account_id']}")
print()
# Execute action
if args.action == "campaigns":
data = list_campaigns(meta_config)
if args.json:
print(json.dumps(data, indent=2))
else:
display_campaigns(data)
elif args.action == "adsets":
data = list_adsets(meta_config)
if args.json:
print(json.dumps(data, indent=2))
else:
display_adsets(data)
elif args.action == "stats":
data = get_insights(meta_config, args.days, args.level)
if args.json:
print(json.dumps(data, indent=2))
else:
display_stats(data, args.level)
if __name__ == "__main__":
main()

38
scripts/search-console.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Google Search Console Bridge Script
# ====================================
# Placeholder — implement when GSC API access is configured
#
# Required setup:
# 1. Google Cloud Project with Search Console API enabled
# 2. GCP service account (or OAuth) with access to client's GSC property
# 3. Client must add service account email as GSC property user
#
# Usage:
# ./search-console.sh <action> <site-url> [options]
#
# Actions:
# top-queries Get top queries by clicks/impressions
# top-pages Get top pages by clicks/impressions
# trends Get traffic trends over time
# content-gaps Find queries ranking 11-30 with no dedicated page
# ctr-gaps Find high-impression, low-CTR opportunities
#
# This script is called by the moreminimore-orchestrator when
# analytics monitoring is active (Phase 3 — Analytics Loop).
echo "⚠️ Google Search Console API script not yet implemented."
echo ""
echo "Required before use:"
echo " 1. Create Google Cloud Project"
echo " 2. Enable Search Console API: gcloud services enable searchconsole.googleapis.com"
echo " 3. Set up service account or application-default credentials"
echo " 4. Client adds service account email to GSC property as user"
echo ""
echo "The seo-analysis skill has full GSC scripts at:"
echo " ~/.hermes/skills/seo-analysis/scripts/"
echo ""
echo "Consider adapting those scripts for multi-client use."
echo ""
echo "See: /Users/kunthawat/Gitea/moreminimore-service-system/scripts/README.md"
exit 1