落地并存测服全端:智能体 Graph/路由、后台切 8180、主账与部署脚本齐备,便于线上重发与验收。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-13 21:15:58 +08:00
co-authored by Cursor
parent 0eb1adecc8
commit 8103b3b651
143 changed files with 8968 additions and 277 deletions
+134 -7
View File
@@ -2,25 +2,152 @@
主账 HTTP 客户端:只打主账 API,不直连 TMS / MySQL。
正式写操作必须带幂等键;Graph 成功 ≠ 主账成功。
骨架:仅占位客户端。
本文件提供可联调的 ping / JSON 请求骨架;不做六态业务分支。
"""
from __future__ import annotations
from dataclasses import dataclass
import logging
from dataclasses import dataclass, field
from typing import Any, Optional
import httpx
from agent.config import Settings, get_settings
logger = logging.getLogger(__name__)
@dataclass
class LedgerResponse:
"""主账一次 HTTP 调用的结果摘要(不含密钥)。"""
ok: bool
status_code: int
data: Any = None
error: str = ""
@dataclass
class LedgerClient:
"""
主账客户端骨架。
主账客户端。
参数:base_url 来自 BIZ_API_BASE_URL。
禁止:在本类拼 TMS 签名或写工单 SQL。
base_url:BIZ_API_BASE_URL(新系统多为 http://127.0.0.1:8180/jeecgboot)。
超时:默认 10s,禁止无限阻塞 Worker 槽。
禁止:在本类拼 TMS HMAC 或写工单 SQL。
"""
base_url: str
timeout_seconds: float = 10.0
service_token: str = ""
_client: Optional[httpx.Client] = field(default=None, repr=False)
@classmethod
def from_settings(cls, settings: Optional[Settings] = None) -> "LedgerClient":
"""从环境组装客户端。"""
cfg = settings or get_settings()
return cls(
base_url=(cfg.biz_api_base_url or "").rstrip("/"),
timeout_seconds=float(getattr(cfg, "biz_api_timeout_seconds", 10) or 10),
service_token=(getattr(cfg, "biz_api_token", None) or ""),
)
def _http(self) -> httpx.Client:
if self._client is None:
headers = {"Accept": "application/json"}
if self.service_token:
headers["X-Access-Token"] = self.service_token
self._client = httpx.Client(
base_url=self.base_url,
timeout=self.timeout_seconds,
headers=headers,
)
return self._client
def close(self) -> None:
"""关闭连接池。"""
if self._client is not None:
self._client.close()
self._client = None
def health_url(self) -> str:
"""返回主账健康检查 URL,供后续联调;骨架不发请求。"""
return f"{self.base_url.rstrip('/')}/health"
"""健康检查相对路径候选的完整 URL(日志用)。"""
return f"{self.base_url}/"
def ping(self) -> LedgerResponse:
"""
探测主账是否可达。
尝试 GET / 或常见健康路径;4xx/5xx 记入 error,不抛给回调线程。
副作用:出站 HTTP。
"""
if not self.base_url:
return LedgerResponse(ok=False, status_code=0, error="BIZ_API_BASE_URL 未配置")
paths = ["/", "/sys/randomImage/ping-check"]
last: LedgerResponse = LedgerResponse(ok=False, status_code=0, error="no_attempt")
for path in paths:
try:
resp = self._http().get(path)
last = LedgerResponse(
ok=resp.status_code < 500,
status_code=resp.status_code,
data={"path": path},
)
if resp.status_code < 500:
logger.info(
"ledger.ping ok status=%s path=%s",
resp.status_code,
path,
)
return last
except httpx.HTTPError as exc:
last = LedgerResponse(ok=False, status_code=0, error=str(exc))
logger.warning("ledger.ping 失败 path=%s err=%s", path, exc)
return last
def request_json(
self,
method: str,
path: str,
*,
json_body: Optional[dict[str, Any]] = None,
idempotency_key: str = "",
) -> LedgerResponse:
"""
通用 JSON 请求骨架。
写操作应传入 idempotency_key(写入头 X-Idempotency-Key)。
不做业务语义解析;调用方根据主账合同处理 result。
"""
headers: dict[str, str] = {}
if idempotency_key:
headers["X-Idempotency-Key"] = idempotency_key
try:
resp = self._http().request(
method.upper(),
path,
json=json_body,
headers=headers,
)
data: Any
try:
data = resp.json()
except ValueError:
data = {"text": resp.text[:500]}
ok = 200 <= resp.status_code < 300
if not ok:
logger.warning(
"ledger.request 非成功 status=%s path=%s",
resp.status_code,
path,
)
return LedgerResponse(ok=ok, status_code=resp.status_code, data=data)
except httpx.HTTPError as exc:
logger.warning("ledger.request 异常 path=%s err=%s", path, exc)
return LedgerResponse(ok=False, status_code=0, error=str(exc))
from agent.ledger.api_shell import LedgerApiShell # noqa: E402
__all__ = ["LedgerClient", "LedgerResponse", "LedgerApiShell"]
+111
View File
@@ -0,0 +1,111 @@
"""
主账 API 方法签名壳。
本文件职责:列出智能体将调用的主账端点形状;默认返回 not_implemented,不假装成功。
禁止:直连 TMS / MySQL;Graph 成功不能冒充本文件成功。
"""
from __future__ import annotations
import logging
from typing import Any, Optional
from agent.ledger import LedgerClient, LedgerResponse
logger = logging.getLogger(__name__)
class LedgerApiShell:
"""
主账业务 API 空壳(签名齐、实现空)。
正式:各方法改走 request_json 打真实路径;路径以主账 OpenAPI 为准。
"""
def __init__(self, client: Optional[LedgerClient] = None) -> None:
self.client = client or LedgerClient.from_settings()
def _nyi(self, name: str) -> LedgerResponse:
logger.info("ledger.api_shell.%s not_implemented", name)
return LedgerResponse(
ok=False,
status_code=501,
error=f"not_implemented:{name}",
data={"method": name},
)
def create_ticket(
self,
*,
payload: dict[str, Any],
idempotency_key: str,
) -> LedgerResponse:
"""建单壳。"""
_ = (payload, idempotency_key)
return self._nyi("create_ticket")
def transition_ticket(
self,
*,
inquiry_no: str,
to_status: str,
idempotency_key: str,
reason: str = "",
) -> LedgerResponse:
"""六态迁移壳。"""
_ = (inquiry_no, to_status, idempotency_key, reason)
return self._nyi("transition_ticket")
def get_ticket(self, *, inquiry_no: str) -> LedgerResponse:
"""查工单壳。"""
_ = inquiry_no
return self._nyi("get_ticket")
def upsert_quote_version(
self,
*,
inquiry_no: str,
quote_version: int,
payload: dict[str, Any],
idempotency_key: str,
) -> LedgerResponse:
"""报价版本壳。"""
_ = (inquiry_no, quote_version, payload, idempotency_key)
return self._nyi("upsert_quote_version")
def register_attachment(
self,
*,
inquiry_no: str,
object_key: str,
meta: dict[str, Any],
idempotency_key: str,
) -> LedgerResponse:
"""附件元数据登记壳。"""
_ = (inquiry_no, object_key, meta, idempotency_key)
return self._nyi("register_attachment")
def get_staff_by_wecom_id(self, *, wecom_id: str) -> LedgerResponse:
"""人员角色壳(按已有 wecom_id)。"""
_ = wecom_id
return self._nyi("get_staff_by_wecom_id")
def list_quote_templates(self, *, enabled_only: bool = True) -> LedgerResponse:
"""
报价模板列表壳。
真相:后台上传/启用;智能体只读已启用项,再经对象存储取字节。
"""
_ = enabled_only
return self._nyi("list_quote_templates")
def wake_agent_ack(
self,
*,
inquiry_no: str,
quote_version: int,
wait_version: int,
) -> LedgerResponse:
"""主账侧唤醒回执壳(智能体被唤醒后可选回写)。"""
_ = (inquiry_no, quote_version, wait_version)
return self._nyi("wake_agent_ack")