#!/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()