230 lines
8.3 KiB
Python
230 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
||
"""端到端:AI 生成 → publish → capsule → agent 解密列表 → import + 新能力冒烟"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
import httpx
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT / "agent_sdk"))
|
||
from client import AgentClient, decrypt_capsule # noqa: E402
|
||
|
||
PLATFORM = "http://127.0.0.1:8180"
|
||
AI = "http://127.0.0.1:8180/ai"
|
||
|
||
|
||
def main() -> None:
|
||
excel = ROOT / "ai-service" / "sample_inventory.xlsx"
|
||
if not excel.exists():
|
||
import subprocess
|
||
|
||
subprocess.check_call([sys.executable, str(ROOT / "ai-service" / "make_sample_excel.py")])
|
||
|
||
with httpx.Client(timeout=60.0) as http:
|
||
# 网关无 token 应 401
|
||
denied = http.get(f"{PLATFORM}/api/v1/audit/logs")
|
||
assert denied.status_code == 401, denied.text
|
||
print("gateway jwt deny ok")
|
||
|
||
tok = http.post(
|
||
f"{PLATFORM}/api/v1/auth/login",
|
||
json={"username": "demo", "password": "demo123"},
|
||
)
|
||
tok.raise_for_status()
|
||
auth = tok.json()
|
||
token, agent_key = auth["access_token"], auth["agent_key"]
|
||
assert auth.get("role") in ("owner", "editor", "viewer"), auth
|
||
print("login ok", auth.get("username"), "role", auth.get("role"), "tenant", auth.get("tenant_id"))
|
||
|
||
headers = {"Authorization": f"Bearer {token}"}
|
||
|
||
# 对象存储
|
||
up = http.post(
|
||
f"{PLATFORM}/api/v1/storage",
|
||
headers=headers,
|
||
files={"file": ("note.txt", b"hello-storage", "text/plain")},
|
||
)
|
||
up.raise_for_status()
|
||
upj = up.json()
|
||
assert upj.get("key") and upj.get("url"), upj
|
||
print("storage upload", upj["key"])
|
||
|
||
# 多模态(无 Key 时启发式)
|
||
png = (
|
||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||
b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00"
|
||
b"\x00\x01\x01\x00\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82"
|
||
)
|
||
files = {
|
||
"excel": (
|
||
"sample_inventory.xlsx",
|
||
excel.read_bytes(),
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
),
|
||
"images": ("ui_list.png", png, "image/png"),
|
||
}
|
||
data = {"prompt": "根据库存表和截图生成可筛选库存列表", "storage_mode": "schema_per_app"}
|
||
gen = http.post(f"{AI}/api/v1/apps/generate", data=data, files=files)
|
||
gen.raise_for_status()
|
||
gj = gen.json()
|
||
draft = gj["draft"]
|
||
warns = " ".join(gj.get("warnings") or [])
|
||
vision_ok = (
|
||
"看图" in warns
|
||
or "截图" in warns
|
||
or bool(draft.get("meta", {}).get("source", {}).get("vision_summary"))
|
||
)
|
||
assert vision_ok, warns
|
||
slug = draft["meta"]["slug"]
|
||
resource = draft["apis"]["resources"][0]["path"].lstrip("/")
|
||
print("generate", slug, resource, "confidence", gj["confidence"])
|
||
|
||
# 唯一 slug,避免重复发布
|
||
slug = f"{slug}_{int(time.time())}"
|
||
draft["meta"]["slug"] = slug
|
||
draft["apis"]["base_path"] = f"/api/v1/apps/{slug}"
|
||
print("slug", slug)
|
||
|
||
pub = http.post(
|
||
f"{PLATFORM}/api/v1/apps/{slug}/publish",
|
||
headers=headers,
|
||
json={"blueprint": draft},
|
||
)
|
||
if pub.status_code >= 400:
|
||
print(pub.text)
|
||
pub.raise_for_status()
|
||
print("published", pub.json()["schema_name"], "memory_mode", pub.json()["memory_mode"])
|
||
|
||
# 审计
|
||
audit = http.get(f"{PLATFORM}/api/v1/audit/logs", headers=headers, params={"page": 1})
|
||
audit.raise_for_status()
|
||
actions = [x.get("action") for x in audit.json().get("items") or []]
|
||
assert any(a.startswith("publish.") for a in actions) or any(
|
||
a == "storage.upload" for a in actions
|
||
), actions
|
||
print("audit actions sample", actions[:5])
|
||
|
||
cap = http.get(f"{PLATFORM}/api/v1/apps/{slug}/agent-capsule", headers=headers)
|
||
cap.raise_for_status()
|
||
capsule = cap.json()["capsule"]
|
||
assert capsule.startswith("AJZ1."), capsule
|
||
desc = decrypt_capsule(agent_key, capsule)
|
||
assert "resources" in desc and desc["app_slug"] == slug
|
||
print("capsule decrypted resources:", [r["name"] for r in desc["resources"]])
|
||
assert "/api/v1/apps/" not in capsule
|
||
|
||
client = AgentClient(token, agent_key, capsule)
|
||
created = client.create(
|
||
resource,
|
||
{
|
||
"sku": "E2E-1",
|
||
"product_name": "测试商品",
|
||
"warehouse": "华东仓",
|
||
"qty": 3,
|
||
"unit_price": 9.9,
|
||
"status": "在售",
|
||
},
|
||
)
|
||
print("agent create id", created.get("id"))
|
||
listed = client.list(resource, warehouse="华东仓")
|
||
print("agent list total", listed.get("total"))
|
||
|
||
csv_body = "sku,product_name,warehouse,qty,unit_price,status\nE2E-2,导入商品,华南仓,5,19.9,在售\n"
|
||
try:
|
||
from openpyxl import Workbook
|
||
import io
|
||
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.append(["sku", "product_name", "warehouse", "qty", "unit_price", "status"])
|
||
ws.append(["E2E-2", "导入商品", "华南仓", 5, 19.9, "在售"])
|
||
buf = io.BytesIO()
|
||
wb.save(buf)
|
||
files = {
|
||
"file": (
|
||
"imp.xlsx",
|
||
buf.getvalue(),
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
)
|
||
}
|
||
except Exception:
|
||
files = {"file": ("imp.csv", csv_body.encode("utf-8"), "text/csv")}
|
||
imp = http.post(
|
||
f"{PLATFORM}/api/v1/apps/{slug}/{resource}/import",
|
||
headers=headers,
|
||
files=files,
|
||
)
|
||
imp.raise_for_status()
|
||
print("import", imp.json())
|
||
|
||
ag = http.get(
|
||
f"{PLATFORM}/api/v1/apps/{slug}/{resource}/aggregate",
|
||
headers=headers,
|
||
params={"group_by": "warehouse", "sum": "qty"},
|
||
)
|
||
ag.raise_for_status()
|
||
print("aggregate", ag.json())
|
||
|
||
# database_per_app 冒烟
|
||
data2 = {"prompt": "独立库库存台账", "storage_mode": "database_per_app"}
|
||
gen2 = http.post(
|
||
f"{AI}/api/v1/apps/generate",
|
||
data=data2,
|
||
files={
|
||
"excel": (
|
||
"sample_inventory.xlsx",
|
||
excel.read_bytes(),
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
)
|
||
},
|
||
)
|
||
gen2.raise_for_status()
|
||
draft2 = gen2.json()["draft"]
|
||
assert draft2["storage"]["mode"] == "database_per_app"
|
||
slug2 = f"dbapp_{int(time.time())}"
|
||
draft2["meta"]["slug"] = slug2
|
||
draft2["apis"]["base_path"] = f"/api/v1/apps/{slug2}"
|
||
pub2 = http.post(
|
||
f"{PLATFORM}/api/v1/apps/{slug2}/publish",
|
||
headers=headers,
|
||
json={"blueprint": draft2},
|
||
)
|
||
if pub2.status_code >= 400:
|
||
print("database_per_app publish:", pub2.text)
|
||
pub2.raise_for_status()
|
||
pj2 = pub2.json()
|
||
assert pj2.get("database_name"), pj2
|
||
print("database_per_app", pj2["database_name"], "schema", pj2["schema_name"])
|
||
|
||
# viewer token 不应能 publish
|
||
issued = http.post(
|
||
f"{PLATFORM}/api/v1/auth/token",
|
||
json={
|
||
"tenant_id": auth["tenant_id"],
|
||
"user_id": auth["user_id"],
|
||
"role": "viewer",
|
||
"secret": "dev-only-change-me",
|
||
},
|
||
)
|
||
if issued.status_code == 200:
|
||
vtok = issued.json()["access_token"]
|
||
forbid = http.post(
|
||
f"{PLATFORM}/api/v1/apps/{slug}/publish",
|
||
headers={"Authorization": f"Bearer {vtok}"},
|
||
json={"blueprint": draft},
|
||
)
|
||
assert forbid.status_code == 403, forbid.text
|
||
print("rbac viewer deny publish ok")
|
||
else:
|
||
print("skip rbac token issue:", issued.status_code, issued.text[:120])
|
||
|
||
print("E2E OK")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|