feat: prepare ARR for controlled public deployment

This commit is contained in:
Wyndham ARR
2026-07-29 16:38:05 +08:00
commit a701de9f0e
271 changed files with 48472 additions and 0 deletions

273
arr_web/services.py Normal file
View File

@@ -0,0 +1,273 @@
"""Mutation seams for uploads and deterministic monthly report generation."""
from __future__ import annotations
import os
import tempfile
import threading
import uuid
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Any, Dict, Protocol
from arr_ingestion.contracts import IngestionError
from arr_ingestion.direct_contracts import SubmissionGrant
from arr_ingestion.repository import JobRegistration
from arr_processing.callbacks import AgentResultWriteback
from arr_processing.contracts import ProcessingRequest
from arr_processing.errors import ProcessingError
from arr_processing.runner import processing_dispatch_idempotency_key
from arr_storage.store import ManagedObjectStore
from arr_web.contracts import (
PortalError,
validate_upload_filename,
validate_xml_payload,
)
from monthly_reports.service import MonthlyReportService, RunRequest
class UploadCoordinator(Protocol):
def submit(self, original_filename: str, payload: bytes) -> Dict[str, Any]:
...
class MonthlyCoordinator(Protocol):
def generate(self, month_key: str, as_of_date: date) -> Dict[str, Any]:
...
class AgentResultCoordinator(Protocol):
def accept(self, raw_signed_result: bytes) -> Dict[str, Any]:
...
class ProcessingStarter(Protocol):
def start(self, request: ProcessingRequest) -> Any:
...
class DirectJobRepository(Protocol):
def register_job(self, registration: JobRegistration) -> None:
...
def issue_grant(
self,
job_id: str,
attempt_no: int,
*,
ttl_seconds: int,
) -> SubmissionGrant:
...
class UnavailableUploadCoordinator:
"""Production-safe default until private OSS and the remote file API are wired."""
def submit(self, original_filename: str, payload: bytes) -> Dict[str, Any]:
raise PortalError(
"PROCESSING_GATEWAY_UNAVAILABLE",
"文件接收服务尚未完成生产接线",
503,
)
class UnavailableMonthlyCoordinator:
def generate(self, month_key: str, as_of_date: date) -> Dict[str, Any]:
raise PortalError(
"MONTHLY_GENERATOR_UNAVAILABLE",
"月报生成服务暂不可用",
503,
)
class UnavailableAgentResultCoordinator:
def accept(self, raw_signed_result: bytes) -> Dict[str, Any]:
raise PortalError(
"AGENT_WRITEBACK_UNAVAILABLE",
"Agent 结果回写服务尚未启用",
503,
)
@dataclass
class ProcessingAgentResultCoordinator:
"""Web-safe adapter around the authenticated ARR result pipeline."""
writeback: AgentResultWriteback
def accept(self, raw_signed_result: bytes) -> Dict[str, Any]:
try:
return self.writeback.accept(raw_signed_result).to_dict()
except (IngestionError, ProcessingError) as error:
code = error.code
if code in {
"PROCESSING_SIGNATURE_INVALID",
"PROCESSING_SIGNATURE_EXPIRED",
}:
status = 401
message = "Agent 结果签名无效或已过期"
elif code.endswith("_NOT_FOUND") or code in {
"JOB_NOT_FOUND",
"PROCESSING_JOB_NOT_FOUND",
}:
status = 404
message = "Agent 结果对应的业务任务不存在"
elif "CONFLICT" in code or code in {
"JOB_TERMINAL",
"PROCESSING_RESULT_MISMATCH",
}:
status = 409
message = "Agent 结果与现有业务任务冲突"
elif getattr(error, "retryable", False) or code.startswith("DATABASE_"):
status = 503
message = "Agent 结果暂时无法写入,请使用相同回调重试"
else:
status = 422
message = "Agent 结果未通过业务验收"
raise PortalError(code, message, status) from None
@dataclass
class ObjectStoreUploadCoordinator:
"""ARR upload boundary: private object -> DB registration -> remote dispatch."""
object_store: ManagedObjectStore
ingestion_repository: DirectJobRepository
processing_starter: ProcessingStarter
processor_version: str
rule_set_sha256: str
submission_grant_ttl_seconds: int = 900
def __post_init__(self) -> None:
if (
not isinstance(self.submission_grant_ttl_seconds, int)
or isinstance(self.submission_grant_ttl_seconds, bool)
or not 30 <= self.submission_grant_ttl_seconds <= 1800
):
raise ValueError("submission grant lifetime is invalid")
def submit(self, original_filename: str, payload: bytes) -> Dict[str, Any]:
validate_upload_filename(original_filename)
validate_xml_payload(payload)
job_id = "arrjob-" + uuid.uuid4().hex
attempt_no = 1
try:
with tempfile.TemporaryDirectory(prefix="arr-web-upload-") as temporary:
source_path = Path(temporary) / "source.xml"
descriptor = os.open(
source_path,
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
0o600,
)
try:
with os.fdopen(descriptor, "wb") as target:
descriptor = -1
target.write(payload)
target.flush()
os.fsync(target.fileno())
finally:
if descriptor >= 0:
os.close(descriptor)
stored = self.object_store.upload_committed(
job_id=job_id,
attempt_no=attempt_no,
role="source_xml",
source=source_path,
original_filename="source.xml",
)
source_file_id = "arr_file_" + uuid.uuid4().hex
request_without_grant = ProcessingRequest(
job_id=job_id,
attempt_no=attempt_no,
source_file_id=source_file_id,
processor_version=self.processor_version,
rule_set_sha256=self.rule_set_sha256,
)
self.ingestion_repository.register_job(
JobRegistration(
job_id=job_id,
source=stored.to_artifact_ref(),
processor_version=self.processor_version,
rule_set_sha256=self.rule_set_sha256,
attempt_no=attempt_no,
idempotency_key=processing_dispatch_idempotency_key(
request_without_grant
),
)
)
grant = self.ingestion_repository.issue_grant(
job_id,
attempt_no,
ttl_seconds=self.submission_grant_ttl_seconds,
)
if grant.job_id != job_id or grant.attempt_no != attempt_no:
raise IngestionError(
"SUBMISSION_GRANT_UNAVAILABLE",
"direct result submission grant identity is invalid",
retryable=True,
)
request = ProcessingRequest(
job_id=job_id,
attempt_no=attempt_no,
source_file_id=source_file_id,
processor_version=self.processor_version,
rule_set_sha256=self.rule_set_sha256,
submission_grant=grant.submission_grant,
)
record = self.processing_starter.start(request)
except (IngestionError, ProcessingError) as error:
status = (
503
if getattr(error, "retryable", False)
or error.code.startswith("DATABASE_")
or error.code in {
"OBJECT_STORE_UNAVAILABLE",
}
else 422
)
raise PortalError(error.code, error.safe_message, status) from None
except Exception:
raise PortalError(
"PROCESSING_SUBMISSION_FAILED",
"文件已安全接收,但处理任务未能启动",
503,
) from None
remote_status = str(getattr(record, "remote_status", "queued"))
return {
"job_id": job_id,
"status": "queued" if remote_status in {"reserved", "pending"} else remote_status,
"attempt_no": attempt_no,
"source_sha256": stored.sha256,
"source_byte_size": stored.byte_size,
}
@dataclass
class ProgramMonthlyCoordinator:
"""Runs the ordinary database program; it never invokes an Agent."""
service: MonthlyReportService
def __post_init__(self) -> None:
self._lock = threading.Lock()
def generate(self, month_key: str, as_of_date: date) -> Dict[str, Any]:
year, month = (int(part) for part in month_key.split("-"))
request = RunRequest(year, month, as_of_date)
try:
request.validate()
except Exception:
raise PortalError("MONTHLY_REQUEST_INVALID", "月报日期范围无效") from None
if not self._lock.acquire(blocking=False):
raise PortalError("MONTHLY_ALREADY_RUNNING", "该服务正在生成月报", 409)
try:
result = self.service.run(request)
finally:
self._lock.release()
payload = result.to_dict()
if result.status != "success":
error = payload.get("errors", [{}])[0]
code = str(error.get("code") or "MONTHLY_GENERATION_FAILED")
raise PortalError(code, "月报生成未完成", 422)
return payload