chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
229
scripts/e2e_flow.py
Normal file
229
scripts/e2e_flow.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user