feat: 安全应用采购规格解析结果 (#257)

This commit is contained in:
chengma
2026-08-17 17:43:25 +08:00
parent 484ce4e8ec
commit afc1cdf719
19 changed files with 1669 additions and 24 deletions
+145
View File
@@ -2,6 +2,7 @@
import hashlib
import json
import re
import socket
from http.client import RemoteDisconnected
from typing import Any, Callable, Mapping, Optional
@@ -17,6 +18,8 @@ from .admin_gateway import (
ClaimCapabilities,
ClientInfo,
RegistrationReceipt,
SpecResolutionMatch,
SpecResolutionReceipt,
SubmissionReceipt,
)
from .task_models import TaskType
@@ -254,6 +257,148 @@ class HttpAdminGateway(AdminGateway):
return self._submit(task_id, idempotency_key, failure, "failure")
def resolve_purchase_spec(
self,
task_id: str,
idempotency_key: str,
observation: Mapping[str, Any],
) -> SpecResolutionReceipt:
"""发送一次已持久化的规格解析命令并严格校验响应。"""
if not task_id.strip() or not idempotency_key.strip():
raise ValueError("规格解析的 task_id 和 Idempotency-Key 不能为空")
if not self._client_id:
raise AdminGatewayError(
"CLIENT_ID_MISSING", "规格解析前必须配置 Client 设备号", False
)
request_id = str(uuid4())
headers = {
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json",
"Idempotency-Key": idempotency_key.strip(),
"X-Request-Id": request_id,
"X-Client-Id": self._client_id,
}
if self._token:
headers["Authorization"] = f"Bearer {self._token}"
request = Request(
f"{self._base_url}/api/v1/client/tasks/{task_id.strip()}/spec-resolution",
data=json.dumps(observation, ensure_ascii=False).encode("utf-8"),
headers=headers,
method="POST",
)
try:
with self._opener(request, timeout=self._timeout_seconds) as response:
status = getattr(response, "status", None) or response.getcode()
body = response.read()
except HTTPError as exc:
self._raise_http_error(exc, request_id, "解析采购规格")
except (
URLError,
RemoteDisconnected,
ConnectionError,
socket.timeout,
TimeoutError,
) as exc:
reason = getattr(exc, "reason", exc)
code = (
"ADMIN_TIMEOUT"
if isinstance(reason, (socket.timeout, TimeoutError))
else "ADMIN_UNAVAILABLE"
)
raise AdminGatewayError(
code,
"Admin 规格解析超时" if code == "ADMIN_TIMEOUT" else "无法连接 Admin 规格解析服务",
True,
request_id,
) from exc
if status != 200:
raise AdminGatewayError(
"ADMIN_UNEXPECTED_RESPONSE",
f"Admin 规格解析返回了未预期的状态码 {status}",
status >= 500,
request_id,
)
return self._parse_spec_resolution_response(body, request_id)
@classmethod
def _parse_spec_resolution_response(
cls, body: bytes, request_id: str
) -> SpecResolutionReceipt:
data = cls._decode_json(body, request_id)
schema_version = data.get("schema_version")
resolution_id = data.get("resolution_id")
outcome = data.get("outcome")
source = data.get("source")
snapshot_hash = data.get("candidate_snapshot_hash")
confidence = data.get("confidence_bps")
reason = data.get("reason")
resolved_at = data.get("resolved_at")
match_data = data.get("match")
valid = (
schema_version == 1
and isinstance(resolution_id, str)
and bool(resolution_id.strip())
and outcome in {"matched", "uncertain", "rejected", "failed"}
and (source is None or source in {"rule", "ai", "reused"})
and isinstance(snapshot_hash, str)
and re.fullmatch(r"[0-9a-f]{64}", snapshot_hash) is not None
and (confidence is None or (
isinstance(confidence, int)
and not isinstance(confidence, bool)
and 0 <= confidence <= 10000
))
and isinstance(reason, str)
and len(reason) <= 500
and isinstance(resolved_at, str)
and bool(resolved_at.strip())
)
match = None
if outcome == "matched" and isinstance(match_data, Mapping):
candidate_id = match_data.get("candidate_id")
raw_text = match_data.get("raw_text")
options = match_data.get("options")
if (
isinstance(candidate_id, str)
and bool(candidate_id.strip())
and isinstance(raw_text, str)
and bool(raw_text)
and isinstance(options, Mapping)
and set(options) == {"color", "size"}
and all(
isinstance(value, str) and bool(value)
for value in options.values()
)
and options.get("size") == raw_text
):
match = SpecResolutionMatch(
candidate_id,
raw_text,
{str(key): str(value) for key, value in options.items()},
)
if (
not valid
or (outcome == "matched") != (match is not None)
or (outcome != "matched" and match_data is not None)
):
raise AdminGatewayError(
"ADMIN_INVALID_RESPONSE",
"Admin 规格解析响应字段不完整或不合法",
False,
request_id,
)
return SpecResolutionReceipt(
1,
resolution_id,
outcome,
source,
snapshot_hash,
match,
confidence,
reason,
resolved_at,
)
def _submit(
self,
task_id: str,