|
@@ -14,7 +14,7 @@ from zoneinfo import ZoneInfo
|
|
|
from storage import initialize_schema, load_managed_accounts
|
|
from storage import initialize_schema, load_managed_accounts
|
|
|
from tencent_client import ACTIVE_STATUS, TencentClient
|
|
from tencent_client import ACTIVE_STATUS, TencentClient
|
|
|
|
|
|
|
|
-from .agency_delivery import publish_agency_reports
|
|
|
|
|
|
|
+from .agency_delivery import agency_route_key, publish_agency_reports
|
|
|
from .config import AgencyWebhookConfig, RoiConfig
|
|
from .config import AgencyWebhookConfig, RoiConfig
|
|
|
from .data_source import (
|
|
from .data_source import (
|
|
|
ODPSClient,
|
|
ODPSClient,
|
|
@@ -68,6 +68,44 @@ def _redact_agency_webhooks(
|
|
|
return message
|
|
return message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _internal_source_revision(config: RoiConfig, now: datetime) -> str:
|
|
|
|
|
+ if config.internal_test_dedup_enabled:
|
|
|
|
|
+ return "internal_test"
|
|
|
|
|
+ return f"it_{now.strftime('%H%M%S%f')}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _build_internal_reports(
|
|
|
|
|
+ *,
|
|
|
|
|
+ agency_reports: list[dict[str, object]],
|
|
|
|
|
+) -> list[dict[str, object]]:
|
|
|
|
|
+ reports = [
|
|
|
|
|
+ {
|
|
|
|
|
+ **report,
|
|
|
|
|
+ "title": f"内部测试_{Path(str(report['report'])).stem}",
|
|
|
|
|
+ }
|
|
|
|
|
+ for report in agency_reports
|
|
|
|
|
+ if agency_route_key(str(report["agency_name"])) == "自动化投放"
|
|
|
|
|
+ ]
|
|
|
|
|
+ if len(reports) != 1:
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ "Internal ROI test requires exactly one 自动化投放 agency report"
|
|
|
|
|
+ )
|
|
|
|
|
+ return reports
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _internal_webhook_config(
|
|
|
|
|
+ reports: list[dict[str, object]],
|
|
|
|
|
+ webhook_url: str,
|
|
|
|
|
+) -> AgencyWebhookConfig:
|
|
|
|
|
+ return AgencyWebhookConfig(
|
|
|
|
|
+ enabled=True,
|
|
|
|
|
+ webhooks={
|
|
|
|
|
+ agency_route_key(str(report["agency_name"])): webhook_url
|
|
|
|
|
+ for report in reports
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _annotate_current_creative_status(
|
|
def _annotate_current_creative_status(
|
|
|
rows,
|
|
rows,
|
|
|
tencent: TencentClient | None = None,
|
|
tencent: TencentClient | None = None,
|
|
@@ -179,11 +217,33 @@ def run_daily_roi(
|
|
|
send_feishu: bool,
|
|
send_feishu: bool,
|
|
|
now: datetime | None = None,
|
|
now: datetime | None = None,
|
|
|
source_revision: str | None = None,
|
|
source_revision: str | None = None,
|
|
|
|
|
+ internal_test: bool = False,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
"""Compute, snapshot, report, and optionally publish one ROI batch."""
|
|
"""Compute, snapshot, report, and optionally publish one ROI batch."""
|
|
|
|
|
|
|
|
config = RoiConfig.from_env()
|
|
config = RoiConfig.from_env()
|
|
|
agency_webhook_config = AgencyWebhookConfig.from_env()
|
|
agency_webhook_config = AgencyWebhookConfig.from_env()
|
|
|
|
|
+ if internal_test and send_feishu:
|
|
|
|
|
+ raise ValueError("internal_test cannot publish formal Feishu notifications")
|
|
|
|
|
+ if internal_test and not (agency_webhook_config.webhooks or {}).get("内部"):
|
|
|
|
|
+ raise ValueError("Internal ROI test requires agency webhook route: 内部")
|
|
|
|
|
+ effective_now = now or datetime.now(SHANGHAI)
|
|
|
|
|
+ if effective_now.tzinfo is None:
|
|
|
|
|
+ effective_now = effective_now.replace(tzinfo=SHANGHAI)
|
|
|
|
|
+ if internal_test:
|
|
|
|
|
+ if source_revision:
|
|
|
|
|
+ raise ValueError("internal_test manages source_revision automatically")
|
|
|
|
|
+ source_revision = _internal_source_revision(config, effective_now)
|
|
|
|
|
+
|
|
|
|
|
+ client = ODPSClient(project=os.getenv("ODPS_PROJECT", "loghubods"))
|
|
|
|
|
+ end_date = resolve_end_date(
|
|
|
|
|
+ client,
|
|
|
|
|
+ requested_end_date,
|
|
|
|
|
+ now=effective_now,
|
|
|
|
|
+ )
|
|
|
|
|
+ start_date, end_date = date_window(end_date)
|
|
|
|
|
+ expected_dates = _dates(start_date)
|
|
|
|
|
+
|
|
|
initialize_schema()
|
|
initialize_schema()
|
|
|
fission_version = os.getenv(
|
|
fission_version = os.getenv(
|
|
|
"ROI_FISSION_PARAMETER_VERSION",
|
|
"ROI_FISSION_PARAMETER_VERSION",
|
|
@@ -196,12 +256,9 @@ def run_daily_roi(
|
|
|
run_config["fission_multiplier"] = fission_parameters.snapshot()
|
|
run_config["fission_multiplier"] = fission_parameters.snapshot()
|
|
|
run_config["report_version"] = REPORT_VERSION
|
|
run_config["report_version"] = REPORT_VERSION
|
|
|
run_config["agency_webhook"] = agency_webhook_config.snapshot()
|
|
run_config["agency_webhook"] = agency_webhook_config.snapshot()
|
|
|
|
|
+ run_config["internal_test"] = internal_test
|
|
|
if source_revision:
|
|
if source_revision:
|
|
|
run_config["source_revision"] = source_revision
|
|
run_config["source_revision"] = source_revision
|
|
|
- client = ODPSClient(project=os.getenv("ODPS_PROJECT", "loghubods"))
|
|
|
|
|
- end_date = resolve_end_date(client, requested_end_date)
|
|
|
|
|
- start_date, end_date = date_window(end_date)
|
|
|
|
|
- expected_dates = _dates(start_date)
|
|
|
|
|
run_id, run_key = _run_identity(
|
|
run_id, run_key = _run_identity(
|
|
|
end_date,
|
|
end_date,
|
|
|
fission_parameters,
|
|
fission_parameters,
|
|
@@ -223,8 +280,9 @@ def run_daily_roi(
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
reusable_statuses = FINAL_STATUSES | {"PENDING_APPROVAL", "EXECUTING"}
|
|
reusable_statuses = FINAL_STATUSES | {"PENDING_APPROVAL", "EXECUTING"}
|
|
|
|
|
+ publish_requested = send_feishu or internal_test
|
|
|
if run.get("status") in reusable_statuses or (
|
|
if run.get("status") in reusable_statuses or (
|
|
|
- run.get("status") == "COMPUTED" and not send_feishu
|
|
|
|
|
|
|
+ run.get("status") == "COMPUTED" and not publish_requested
|
|
|
):
|
|
):
|
|
|
logger.info("Reuse ROI run=%s status=%s", run_id, run.get("status"))
|
|
logger.info("Reuse ROI run=%s status=%s", run_id, run.get("status"))
|
|
|
reused_result: dict[str, Any] = {
|
|
reused_result: dict[str, Any] = {
|
|
@@ -284,10 +342,14 @@ def run_daily_roi(
|
|
|
_rule_config(config),
|
|
_rule_config(config),
|
|
|
fission_parameters=fission_parameters,
|
|
fission_parameters=fission_parameters,
|
|
|
)
|
|
)
|
|
|
- managed_ids = {
|
|
|
|
|
- int(row["account_id"])
|
|
|
|
|
- for row in load_managed_accounts()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ managed_ids = (
|
|
|
|
|
+ set()
|
|
|
|
|
+ if internal_test
|
|
|
|
|
+ else {
|
|
|
|
|
+ int(row["account_id"])
|
|
|
|
|
+ for row in load_managed_accounts()
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
annotated, snapshots, actions = annotate_execution(
|
|
annotated, snapshots, actions = annotate_execution(
|
|
|
summary,
|
|
summary,
|
|
|
managed_ids,
|
|
managed_ids,
|
|
@@ -348,19 +410,31 @@ def run_daily_roi(
|
|
|
if source_revision:
|
|
if source_revision:
|
|
|
batch_name = f"{batch_name}_{source_revision}"
|
|
batch_name = f"{batch_name}_{source_revision}"
|
|
|
output_path = output_dir / f"{batch_name}.xlsx"
|
|
output_path = output_dir / f"{batch_name}.xlsx"
|
|
|
- write_workbook(
|
|
|
|
|
- report_rows,
|
|
|
|
|
- thresholds,
|
|
|
|
|
- expected_dates,
|
|
|
|
|
- output_path,
|
|
|
|
|
- run_config,
|
|
|
|
|
- fission_match_summary,
|
|
|
|
|
- )
|
|
|
|
|
- agency_report_date = (now or datetime.now(SHANGHAI)).strftime("%Y%m%d")
|
|
|
|
|
|
|
+ if not internal_test:
|
|
|
|
|
+ write_workbook(
|
|
|
|
|
+ report_rows,
|
|
|
|
|
+ thresholds,
|
|
|
|
|
+ expected_dates,
|
|
|
|
|
+ output_path,
|
|
|
|
|
+ run_config,
|
|
|
|
|
+ fission_match_summary,
|
|
|
|
|
+ )
|
|
|
|
|
+ agency_report_date = effective_now.strftime("%Y%m%d")
|
|
|
agency_reports = write_agency_workbooks(
|
|
agency_reports = write_agency_workbooks(
|
|
|
report_rows,
|
|
report_rows,
|
|
|
output_dir / f"{agency_report_date}_调控建议",
|
|
output_dir / f"{agency_report_date}_调控建议",
|
|
|
agency_report_date,
|
|
agency_report_date,
|
|
|
|
|
+ agency_names={"自动化投放"} if internal_test else None,
|
|
|
|
|
+ )
|
|
|
|
|
+ internal_reports = (
|
|
|
|
|
+ _build_internal_reports(agency_reports=agency_reports)
|
|
|
|
|
+ if internal_test
|
|
|
|
|
+ else []
|
|
|
|
|
+ )
|
|
|
|
|
+ result_report = (
|
|
|
|
|
+ str(internal_reports[0]["report"])
|
|
|
|
|
+ if internal_test
|
|
|
|
|
+ else str(output_path)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
result: dict[str, Any] = {
|
|
result: dict[str, Any] = {
|
|
@@ -378,10 +452,52 @@ def run_daily_roi(
|
|
|
"fission_multiplier_matches": fission_match_summary.to_dict(
|
|
"fission_multiplier_matches": fission_match_summary.to_dict(
|
|
|
"records"
|
|
"records"
|
|
|
),
|
|
),
|
|
|
- "report": str(output_path),
|
|
|
|
|
|
|
+ "report": result_report,
|
|
|
"agency_reports": agency_reports,
|
|
"agency_reports": agency_reports,
|
|
|
}
|
|
}
|
|
|
- if not send_feishu:
|
|
|
|
|
|
|
+ if not publish_requested:
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ if internal_test:
|
|
|
|
|
+ internal_url = (agency_webhook_config.webhooks or {})["内部"]
|
|
|
|
|
+ internal_config = _internal_webhook_config(
|
|
|
|
|
+ internal_reports,
|
|
|
|
|
+ internal_url,
|
|
|
|
|
+ )
|
|
|
|
|
+ publisher = RoiFeishuPublisher()
|
|
|
|
|
+ try:
|
|
|
|
|
+ deliveries = publish_agency_reports(
|
|
|
|
|
+ run_id=run_id,
|
|
|
|
|
+ reports=internal_reports,
|
|
|
|
|
+ config=internal_config,
|
|
|
|
|
+ publisher=publisher,
|
|
|
|
|
+ now=effective_now,
|
|
|
|
|
+ )
|
|
|
|
|
+ finally:
|
|
|
|
|
+ publisher.close()
|
|
|
|
|
+ result["internal_deliveries"] = deliveries
|
|
|
|
|
+ failed = [row for row in deliveries if row.get("status") != "SENT"]
|
|
|
|
|
+ if failed:
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ f"Internal ROI test delivery failed for {len(failed)} report(s)"
|
|
|
|
|
+ )
|
|
|
|
|
+ main_delivery = next(
|
|
|
|
|
+ row for row in deliveries if row["agency_name"] == "自动化投放"
|
|
|
|
|
+ )
|
|
|
|
|
+ mark_published(
|
|
|
|
|
+ run_id,
|
|
|
|
|
+ sheet_token=str(main_delivery["sheet_token"]),
|
|
|
|
|
+ sheet_url=str(main_delivery["sheet_url"]),
|
|
|
|
|
+ message_id="internal-webhook",
|
|
|
|
|
+ expires_at=None,
|
|
|
|
|
+ requires_approval=False,
|
|
|
|
|
+ )
|
|
|
|
|
+ result.update(
|
|
|
|
|
+ {
|
|
|
|
|
+ "status": "COMPLETED",
|
|
|
|
|
+ "sheet_url": main_delivery["sheet_url"],
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
return result
|
|
return result
|
|
|
|
|
|
|
|
counts = recommendation_rows.groupby("动作").size().to_dict()
|
|
counts = recommendation_rows.groupby("动作").size().to_dict()
|
|
@@ -402,7 +518,7 @@ def run_daily_roi(
|
|
|
requires_approval=bool(actions),
|
|
requires_approval=bool(actions),
|
|
|
)
|
|
)
|
|
|
expires_at = (
|
|
expires_at = (
|
|
|
- (now or datetime.now(SHANGHAI))
|
|
|
|
|
|
|
+ effective_now
|
|
|
+ timedelta(minutes=config.approval_ttl_minutes)
|
|
+ timedelta(minutes=config.approval_ttl_minutes)
|
|
|
if actions
|
|
if actions
|
|
|
else None
|
|
else None
|