99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
"""智能体侧:解密胶囊并按契约请求平台(前端不持有明文契约)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
|
|
|
PREFIX = "AJZ1"
|
|
|
|
|
|
def decrypt_capsule(agent_key_b64: str, capsule: str) -> dict[str, Any]:
|
|
parts = capsule.split(".")
|
|
if len(parts) != 3 or parts[0] != PREFIX:
|
|
raise ValueError("invalid capsule")
|
|
key = base64.urlsafe_b64decode(pad(agent_key_b64))
|
|
nonce = base64.urlsafe_b64decode(pad(parts[1]))
|
|
data = base64.urlsafe_b64decode(pad(parts[2]))
|
|
# Go RawURLEncoding has no padding; Python needs pad
|
|
aes = AESGCM(key)
|
|
plain = aes.decrypt(nonce, data, None)
|
|
return json.loads(plain)
|
|
|
|
|
|
def pad(s: str) -> str:
|
|
return s + "=" * (-len(s) % 4)
|
|
|
|
|
|
class AgentClient:
|
|
def __init__(self, access_token: str, agent_key: str, capsule: str):
|
|
self.access_token = access_token
|
|
self.desc = decrypt_capsule(agent_key, capsule)
|
|
self.http = httpx.Client(
|
|
base_url=self.desc["base_url"],
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
timeout=30.0,
|
|
)
|
|
|
|
def resources(self) -> list[dict[str, Any]]:
|
|
return self.desc.get("resources", [])
|
|
|
|
def list(self, resource_name: str, **filters: str) -> dict[str, Any]:
|
|
res = self._find(resource_name)
|
|
params = {f"filter.{k}": v for k, v in filters.items()}
|
|
r = self.http.get(res["path"], params=params)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def create(self, resource_name: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
res = self._find(resource_name)
|
|
r = self.http.post(res["path"], json=body)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def get(self, resource_name: str, row_id: str) -> dict[str, Any]:
|
|
res = self._find(resource_name)
|
|
r = self.http.get(f"{res['path'].rstrip('/')}/{row_id}")
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def update(self, resource_name: str, row_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
res = self._find(resource_name)
|
|
r = self.http.put(f"{res['path'].rstrip('/')}/{row_id}", json=body)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def delete(self, resource_name: str, row_id: str) -> None:
|
|
res = self._find(resource_name)
|
|
r = self.http.delete(f"{res['path'].rstrip('/')}/{row_id}")
|
|
if r.status_code not in (200, 204):
|
|
r.raise_for_status()
|
|
|
|
def _find(self, name: str) -> dict[str, Any]:
|
|
for r in self.resources():
|
|
if r["name"] == name:
|
|
return r
|
|
raise KeyError(f"resource not in capsule: {name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import os
|
|
import sys
|
|
|
|
token = os.environ.get("AJZ_TOKEN", "")
|
|
key = os.environ.get("AJZ_AGENT_KEY", "")
|
|
capsule = os.environ.get("AJZ_CAPSULE", "")
|
|
if not (token and key and capsule):
|
|
print("Set AJZ_TOKEN / AJZ_AGENT_KEY / AJZ_CAPSULE", file=sys.stderr)
|
|
sys.exit(1)
|
|
client = AgentClient(token, key, capsule)
|
|
print(json.dumps({"app": client.desc["app_slug"], "resources": [r["name"] for r in client.resources()]}, ensure_ascii=False))
|
|
if client.resources():
|
|
name = client.resources()[0]["name"]
|
|
print(json.dumps(client.list(name), ensure_ascii=False, indent=2))
|