feat: add TaurusX API performance data

This commit is contained in:
王源
2026-10-15 17:24:31 +08:00
parent 73f5fdf66e
commit 6e8238e5cf
4 changed files with 4260 additions and 1 deletions
+2
View File
@@ -1,3 +1,5 @@
__pycache__/ __pycache__/
*.pyc *.pyc
.DS_Store .DS_Store
.env
.env.*
+3928
View File
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Fetch recent TaurusX performance data without persisting API credentials."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import tempfile
import time
import urllib.parse
import urllib.request
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any
API_URL = "https://scaler.taurusx.com/openapi/performance_data"
TIMEZONE = "+8"
def build_token(secret_key: str, timestamp: str) -> str:
timestamp_hash = hashlib.md5(timestamp.encode()).hexdigest()
return hashlib.md5((secret_key + timestamp_hash).encode()).hexdigest()
def request_performance(
access_key: str,
secret_key: str,
start_date: date,
end_date: date,
) -> list[dict[str, Any]]:
timestamp = str(int(time.time()))
query = urllib.parse.urlencode(
{
"timezone": TIMEZONE,
"report_type": "event",
"start_time": start_date.isoformat(),
"end_time": end_date.isoformat(),
"dimension_option": "Campaign,Country",
}
)
request = urllib.request.Request(
f"{API_URL}?{query}",
headers={
"access-key": access_key,
"token": build_token(secret_key, timestamp),
"timestamp": timestamp,
"Accept": "application/json",
"User-Agent": "AgentStage-TaurusX-Report/1.0",
},
)
with urllib.request.urlopen(request, timeout=60) as response:
payload = json.loads(response.read())
if payload.get("code") != 200:
raise RuntimeError(f"TaurusX API returned code {payload.get('code')}: {payload.get('msg', 'unknown error')}")
rows = (payload.get("data") or {}).get("data_list")
if not isinstance(rows, list):
raise RuntimeError("TaurusX API response is missing data.data_list")
return rows
def match_pkg(campaign_name: str, packages: list[str]) -> str:
lowered = campaign_name.casefold()
for package in packages:
if package.casefold() in lowered:
return package
return ""
def build_dataset(
api_rows: list[dict[str, Any]],
report_payload: dict[str, Any],
start_date: date,
end_date: date,
) -> dict[str, Any]:
coverage_records = report_payload.get("records") or []
packages = sorted({str(row["pkg"]) for row in coverage_records}, key=len, reverse=True)
coverage_by_key = {
(str(row["pkg"]).casefold(), str(row["country"]).upper()): row
for row in coverage_records
}
records: list[dict[str, Any]] = []
for api_row in api_rows:
campaign_name = str(api_row.get("campaign_name") or "").strip()
pkg = match_pkg(campaign_name, packages)
country = str(api_row.get("country") or "").strip().upper()
coverage = coverage_by_key.get((pkg.casefold(), country)) if pkg and country else None
records.append(
{
"date": str(api_row.get("date") or ""),
"campaign_id": str(api_row.get("campaign_id") or ""),
"campaign_name": campaign_name,
"pkg": pkg,
"country": country,
"impressions": int(float(api_row.get("impressions") or 0)),
"clicks": int(float(api_row.get("clicks") or 0)),
"conversions": int(float(api_row.get("conversions") or 0)),
"cost": round(float(api_row.get("cost") or 0), 4),
"order_match": coverage is not None,
"adpingids": list(coverage.get("adpingids") or []) if coverage else [],
"order_statuses": list(coverage.get("order_statuses") or []) if coverage else [],
"listed_in_taurusx_orders": bool(coverage and coverage.get("target_present")),
}
)
records.sort(key=lambda row: (row["date"], row["cost"], row["clicks"]), reverse=True)
fetched_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
return {
"meta": {
"source": API_URL,
"timezone": TIMEZONE,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
"fetched_at": fetched_at,
"rows": len(records),
"campaigns": len({row["campaign_id"] for row in records}),
"pkg_country_count": len({(row["pkg"].casefold(), row["country"]) for row in records if row["pkg"]}),
"countries": len({row["country"] for row in records if row["country"]}),
"matched_order_rows": sum(row["order_match"] for row in records),
"unmatched_order_rows": sum(not row["order_match"] for row in records),
"impressions": sum(row["impressions"] for row in records),
"clicks": sum(row["clicks"] for row in records),
"conversions": sum(row["conversions"] for row in records),
"cost": round(sum(row["cost"] for row in records), 4),
},
"records": records,
}
def atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write("\n")
temporary_path = Path(handle.name)
temporary_path.replace(path)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--data-file", type=Path, default=Path(__file__).with_name("data.json"))
parser.add_argument("--days", type=int, default=7, choices=range(1, 8), metavar="1-7")
parser.add_argument("--end-date", type=date.fromisoformat, help="Last report date; defaults to yesterday in UTC+8")
args = parser.parse_args()
access_key = os.environ.get("TAURUSX_ACCESS_KEY", "").strip()
secret_key = os.environ.get("TAURUSX_SECRET_KEY", "").strip()
if not access_key or not secret_key:
raise SystemExit("TAURUSX_ACCESS_KEY and TAURUSX_SECRET_KEY environment variables are required")
if not args.data_file.is_file():
raise SystemExit(f"Report data file not found: {args.data_file}")
utc8_today = datetime.now(timezone(timedelta(hours=8))).date()
end_date = args.end_date or (utc8_today - timedelta(days=1))
start_date = end_date - timedelta(days=args.days - 1)
report_payload = json.loads(args.data_file.read_text(encoding="utf-8"))
api_rows = request_performance(access_key, secret_key, start_date, end_date)
report_payload["taurusx_perf"] = build_dataset(api_rows, report_payload, start_date, end_date)
atomic_write_json(args.data_file, report_payload)
meta = report_payload["taurusx_perf"]["meta"]
print(
f"Updated {args.data_file}: {meta['rows']} TaurusX rows, "
f"{meta['matched_order_rows']} matched orders, {meta['start_date']}..{meta['end_date']}"
)
if __name__ == "__main__":
main()
+160
View File
@@ -697,6 +697,165 @@
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url); document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
} }
const txState = { filtered: [], page: 1 };
function renderTaurusxPerf() {
const data = state.data.taurusx_perf;
if (!data || !Array.isArray(data.records)) return;
const panel = el('section', 'panel workspace');
panel.id = 'taurusxPerfPanel';
const header = el('div', 'workspace-head');
const head = el('div', 'panel-head');
const title = el('div');
title.append(
el('h2', '', 'TaurusX 近 7 日投放表现'),
el('div', 'panel-desc', `${data.meta.start_date} 至 ${data.meta.end_date} · UTC${data.meta.timezone} · 拉取于 ${formatTime(data.meta.fetched_at)}`)
);
head.append(title);
const summary = el('section', 'kpis');
summary.append(
kpi('Impressions', nf.format(data.meta.impressions), `${nf.format(data.meta.campaigns)} 个 Campaign`),
kpi('Clicks', nf.format(data.meta.clicks), `${nf.format(data.meta.pkg_country_count)} 个 Pkg_国家`),
kpi('Conversions', nf.format(data.meta.conversions), `${nf.format(data.meta.matched_order_rows)} 条已匹配订单`),
kpi('Cost', money(data.meta.cost), `${nf.format(data.meta.unmatched_order_rows)} 条订单未登记`)
);
summary.style.marginBottom = '16px';
const filters = el('div', 'filters');
filters.style.gridTemplateColumns = 'minmax(210px, 1.2fr) repeat(4, minmax(125px, .55fr)) auto';
const search = el('input', 'control');
search.id = 'txSearch'; search.type = 'search'; search.placeholder = '搜索 Campaign、Pkg、Adping ID'; search.autocomplete = 'off';
const reportDate = selectControl('txDate', '全部日期');
[...new Set(data.records.map(row => row.date))].sort().reverse().forEach(value => reportDate.append(new Option(value, value)));
const country = selectControl('txCountry', '全部国家');
[...new Set(data.records.map(row => row.country))].sort().forEach(value => country.append(new Option(value, value)));
const orderMatch = selectControl('txOrderMatch', '');
[['all','全部订单匹配'], ['matched','订单已登记'], ['unmatched','API有数据·订单未登记']]
.forEach(([value,label]) => orderMatch.append(new Option(label, value)));
const sort = selectControl('txSort', '');
[
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'],
['conversions-desc','Conversions:高到低'], ['pkg-asc','Pkg:A-Z']
].forEach(([value,label]) => sort.append(new Option(`排序 · ${label}`, value)));
const exportButton = el('button', 'button', '导出 TaurusX 数据');
exportButton.type = 'button';
filters.append(search, reportDate, country, orderMatch, sort, exportButton);
header.append(head, summary, filters);
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
document.getElementById('app').insertBefore(panel, document.querySelector('footer'));
search.addEventListener('input', () => { txState.page = 1; applyTxFilters(); });
[reportDate, country, orderMatch, sort].forEach(control => control.addEventListener('change', () => { txState.page = 1; applyTxFilters(); }));
exportButton.addEventListener('click', exportTxCsv);
applyTxFilters();
}
function applyTxFilters() {
const query = document.getElementById('txSearch').value.trim().toLocaleLowerCase();
const reportDate = document.getElementById('txDate').value;
const country = document.getElementById('txCountry').value;
const orderMatch = document.getElementById('txOrderMatch').value;
const sort = document.getElementById('txSort').value;
txState.filtered = state.data.taurusx_perf.records.filter(row => {
const searchable = `${row.campaign_id} ${row.campaign_name} ${row.pkg} ${row.adpingids.join(' ')}`.toLocaleLowerCase();
return (!query || searchable.includes(query))
&& (reportDate === 'all' || row.date === reportDate)
&& (country === 'all' || row.country === country)
&& (orderMatch === 'all' || (orderMatch === 'matched' ? row.order_match : !row.order_match));
});
const [field, direction] = sort.split('-');
const multiplier = direction === 'asc' ? 1 : -1;
txState.filtered.sort((left, right) => {
const a = left[field]; const b = right[field];
if (typeof a === 'string' || typeof b === 'string') {
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }) * multiplier
|| left.campaign_id.localeCompare(right.campaign_id, undefined, { numeric: true });
}
return (Number(a) - Number(b)) * multiplier
|| left.campaign_id.localeCompare(right.campaign_id, undefined, { numeric: true });
});
renderTxResults();
}
function renderTxResults() {
const panel = document.getElementById('taurusxPerfPanel');
const totalPages = Math.max(1, Math.ceil(txState.filtered.length / PAGE_SIZE));
txState.page = Math.min(txState.page, totalPages);
const start = (txState.page - 1) * PAGE_SIZE;
const rows = txState.filtered.slice(start, start + PAGE_SIZE);
const impressions = txState.filtered.reduce((sum, row) => sum + row.impressions, 0);
const clicks = txState.filtered.reduce((sum, row) => sum + row.clicks, 0);
const conversions = txState.filtered.reduce((sum, row) => sum + row.conversions, 0);
const cost = txState.filtered.reduce((sum, row) => sum + row.cost, 0);
const cards = panel.querySelectorAll('.kpi');
const setCard = (index, value, sub) => {
cards[index].querySelector('.value').textContent = value;
cards[index].querySelector('.sub').textContent = sub;
};
setCard(0, nf.format(impressions), `${nf.format(txState.filtered.length)} 条筛选明细`);
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
setCard(3, money(cost), `${nf.format(txState.filtered.filter(row => !row.order_match).length)} 条订单未登记`);
const meta = panel.querySelector('.result-meta');
meta.textContent = '';
meta.append(el('span', '', `筛选结果 ${nf.format(txState.filtered.length)} 条`), el('span', '', `第 ${txState.page} / ${totalPages} 页`));
const wrap = panel.querySelector('.table-wrap');
wrap.textContent = '';
const table = el('table');
const thead = el('thead'); const header = el('tr');
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Adpingid', '订单状态', 'Impressions', 'Clicks', 'Conversions', 'Cost', '订单关联']
.forEach(label => header.append(el('th', '', label)));
thead.append(header); table.append(thead);
const tbody = el('tbody');
if (!rows.length) {
const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 12;
const row = el('tr'); row.append(cell); tbody.append(row);
}
rows.forEach(record => {
const row = el('tr');
const country = el('td'); country.append(el('span', 'country-pill', record.country || '-'));
const status = el('td');
const statusPills = el('div', 'platforms');
(record.order_statuses.length ? record.order_statuses : ['未登记']).forEach(value => statusPills.append(el('span', 'platform-pill', value)));
status.append(statusPills);
const match = el('td');
match.append(el('span', `badge ${record.order_match ? 'covered' : 'target-only'}`, record.order_match ? '订单已登记' : 'API有数据·订单未登记'));
row.append(
el('td', '', record.date), el('td', 'pkg', record.campaign_id), el('td', 'product', record.campaign_name),
el('td', 'pkg', record.pkg || '-'), country, el('td', 'pkg', record.adpingids.join(' / ') || '-'), status,
el('td', '', nf.format(record.impressions)), el('td', '', nf.format(record.clicks)),
el('td', '', nf.format(record.conversions)), el('td', '', money(record.cost)), match
);
tbody.append(row);
});
table.append(tbody); wrap.append(table);
const pager = panel.querySelector('.pagination');
pager.textContent = '';
const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = txState.page <= 1;
const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = txState.page >= totalPages;
prev.addEventListener('click', () => { txState.page -= 1; renderTxResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
next.addEventListener('click', () => { txState.page += 1; renderTxResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
pager.append(prev, el('span', 'panel-desc', `${txState.page} / ${totalPages}`), next);
}
function exportTxCsv() {
const headers = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Adpingid', '订单状态', 'Impressions', 'Clicks', 'Conversions', 'Cost', '订单关联'];
const rows = [headers, ...txState.filtered.map(record => [
record.date, record.campaign_id, record.campaign_name, record.pkg, record.country,
record.adpingids.join(' / '), record.order_statuses.join(' / '), record.impressions,
record.clicks, record.conversions, record.cost, record.order_match ? '订单已登记' : 'API有数据·订单未登记'
])];
const csv = '\ufeff' + rows.map(row => row.map(value => `"${String(value).replaceAll('"', '""')}"`).join(',')).join('\n');
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
const link = document.createElement('a');
link.href = url; link.download = `TaurusX近7日_${new Date().toISOString().slice(0,10)}.csv`;
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
}
fetch(`data.json?v=${Date.now()}`, { cache: 'no-store' }) fetch(`data.json?v=${Date.now()}`, { cache: 'no-store' })
.then(response => { .then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -707,6 +866,7 @@
state.data = data; state.data = data;
renderShell(); renderShell();
renderMtg1013(); renderMtg1013();
renderTaurusxPerf();
}) })
.catch(error => { .catch(error => {
const app = document.getElementById('app'); const app = document.getElementById('app');