aps-agent/poc/pi-fallback/smoke/smoke_e2e_diag.py

212 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
# P1 冒烟 · 诊断轮(Agent-G):与 smoke_e2e.py 完全同链,仅两点差异并如实标注:
# 1) 预播种租户世界(桌面 mock 身份下聊天读写的是租户世界,首轮为空导致快照空);
# 种子 = server/data/world.json 演示数据的副本,全部落在临时 APS_HOME 内。
# 2) server 进程内对 _GUARD_TS_TEMPLATE 做内存级单行修正(注释行的 { block:... } 转义),
# 用于回答「该 bug 是否为唯一拦路点」。产品代码文件零改动;本轮结论不作为产品路径真相,
# 仅作修复验证的预判证据。shim 见 shim_server.py。
import hashlib
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.request
from http.cookiejar import CookieJar
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[2]
SITE = HERE / "site2"
APS_HOME = SITE / "aps-home"
DATA = APS_HOME / "data"
PORT = 8392
BASE = f"http://127.0.0.1:{PORT}"
DEVICE_ID = "smoke-g-device-0001"
MOCK_TENANT = "demo0000000000000000000000000001"
QUERY = ("琢磨个冷门的角度:把每张待交付订单的交货日期换算成一周里的第几天,"
"交货压力集中在一周的开头还是末尾?再数数物料种类,凭数据给个备料松紧的"
"判断,别改任何东西,只给结论。")
R: dict = {"query": QUERY, "mode": "diagnostic-shim", "startedAt": time.strftime("%Y-%m-%d %H:%M:%S")}
def _device_user_id(device_id: str) -> int:
return int(hashlib.sha256(device_id.encode()).hexdigest()[:15], 16)
def http(opener, method, path, body=None, headers=None, timeout=30):
req = urllib.request.Request(BASE + path, method=method)
for k, v in (headers or {}).items():
req.add_header(k, v)
data = None
if body is not None:
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
req.add_header("Content-Type", "application/json")
return opener.open(req, data=data, timeout=timeout)
def desktop_headers():
return {"x-aps-client": "desktop", "x-aps-device-id": DEVICE_ID}
def main() -> int:
if SITE.exists():
shutil.rmtree(SITE)
DATA.mkdir(parents=True)
# 预播种:平台默认世界 + 桌面租户个人项目世界(均为演示数据副本,限临时目录)
demo_world = (REPO / "server" / "data" / "world.json").read_bytes()
(DATA / "world.json").write_bytes(demo_world)
tenant_world = (DATA / "tenants" / MOCK_TENANT / "projects"
/ f"personal-{_device_user_id(DEVICE_ID)}" / "world.json")
tenant_world.parent.mkdir(parents=True, exist_ok=True)
tenant_world.write_bytes(demo_world)
features = SITE / "features.json"
features.write_text(
json.dumps({"version": 1, "features": {"fallback": True}}, ensure_ascii=False),
encoding="utf-8")
R["site"] = str(SITE)
env = dict(os.environ)
env.update({
"APS_HOME": str(APS_HOME),
"APS_FEATURES_PATH": str(features),
"APS_WORLD_PATH": str(DATA / "world.json"),
"APS_DB_PATH": str(DATA / "master.db"),
"APS_LICENSE_PROVIDER": "mock",
"APS_PORT": str(PORT),
"APS_FALLBACK_MODEL": "aps-fallback/kimi-k2.6",
# 本机 PATH 中 node 优先解析为 node.CMD 垫片,Popen 带参起 .cmd 必炸;
# 用既有配置项显式指向真实 node.exe(不改产品代码)
"APS_FALLBACK_NODE": r"D:\AppData\Kimi\resources\resources\runtime\node.exe",
"PYTHONIOENCODING": "utf-8",
})
env.pop("APS_FALLBACK_DIR", None)
server_log = open(HERE / "server2.log", "w", encoding="utf-8")
proc = subprocess.Popen(
[sys.executable, str(HERE / "shim_server.py")],
cwd=str(REPO), env=env, stdout=server_log, stderr=subprocess.STDOUT)
R["serverPid"] = proc.pid
try:
health = None
for _ in range(90):
if proc.poll() is not None:
break
try:
with urllib.request.urlopen(BASE + "/api/health", timeout=3) as r:
health = json.loads(r.read().decode("utf-8"))
break
except Exception:
time.sleep(1)
R["health"] = health
if not health:
R["fatal"] = "server 90s 内未就绪(见 server2.log)"
return 1
cj = CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
resp = http(opener, "POST", "/api/auth/license/activate",
{"code": "APS-DAY-DEMO", "deviceId": DEVICE_ID},
headers=desktop_headers())
act = json.loads(resp.read().decode("utf-8"))
R["licenseActivate"] = {"status": resp.status, "provider": act.get("provider"),
"username": (act.get("user") or {}).get("username")}
resp = http(opener, "GET", "/api/features", headers=desktop_headers())
feats = json.loads(resp.read().decode("utf-8"))
R["features"] = {"fallback": feats["features"].get("fallback"),
"defaultOff": feats.get("defaultOff"), "source": feats.get("source")}
t0 = time.monotonic()
resp = http(opener, "POST", "/api/chat", {"text": QUERY},
headers=desktop_headers(), timeout=280)
raw = resp.read().decode("utf-8", errors="replace")
R["chatElapsedSec"] = round(time.monotonic() - t0, 1)
(HERE / "chat2.sse").write_text(raw, encoding="utf-8")
events = []
for line in raw.splitlines():
line = line.strip()
if line.startswith("data:"):
try:
events.append(json.loads(line[5:].strip()))
except json.JSONDecodeError:
pass
R["sseEventTypes"] = {}
for e in events:
R["sseEventTypes"][e.get("type", "?")] = R["sseEventTypes"].get(e.get("type", "?"), 0) + 1
R["intentEvent"] = next((e for e in events if e.get("type") == "intent"), None)
reply_text = "".join(e.get("text", "") for e in events if e.get("type") == "token")
R["replyLen"] = len(reply_text)
R["replyContainsFallbackMarker"] = "[智能兜底 · 草稿]" in reply_text
(HERE / "reply2.txt").write_text(reply_text, encoding="utf-8")
finally:
if proc.poll() is None:
subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"],
capture_output=True, timeout=30)
try:
proc.wait(timeout=20)
except Exception:
pass
server_log.close()
# 离线校验
fb_root = DATA / "fallback"
runs = sorted([p for p in fb_root.glob("fb-*") if p.is_dir()]) if fb_root.is_dir() else []
R["runDirs"] = [str(p) for p in runs]
if runs:
run = runs[-1]
R["runTree"] = sorted(str(p.relative_to(run)) for p in run.rglob("*"))
rj = run / "result.json"
if rj.is_file():
outcome = json.loads(rj.read_text(encoding="utf-8"))
R["resultJson"] = {k: outcome.get(k) for k in (
"run_id", "ok", "stop_reason", "error_message", "steps",
"output_bytes", "elapsed_sec", "citation_check")}
calls = run / "calls.jsonl"
if calls.is_file():
recs = [json.loads(x) for x in calls.read_text(encoding="utf-8").splitlines() if x.strip()]
R["callsJsonl"] = {"count": len(recs),
"tools": sorted({r.get("tool", "?") for r in recs}),
"statuses": sorted({r.get("status", "?") for r in recs})}
R["reportMdExists"] = (run / "outbox" / "report.md").is_file()
guard = next(run.glob("guard-*.ts"), None)
if guard:
g = guard.read_text(encoding="utf-8")
R["guardSanity"] = {"hasReturnBlock": "return { block: true, reason };" in g}
log = run / "orchestrator.log"
if log.is_file():
R["orchestratorLogTail"] = log.read_text(encoding="utf-8").splitlines()[-8:]
snap = run / "inbox" / "snapshot.md"
if snap.is_file():
R["snapshotMd"] = snap.read_text(encoding="utf-8")
ocsv = run / "inbox" / "orders.csv"
if ocsv.is_file():
R["ordersCsv"] = ocsv.read_text(encoding="utf-8")
# 审计:聊天身份的世界(租户个人项目)
audits = []
for w in DATA.glob("tenants/*/projects/*/world.json"):
for a in (json.loads(w.read_text(encoding="utf-8")).get("auditEvents") or []):
if a.get("action") == "agent.fallback.propose":
audits.append({"worldFile": str(w), "actor": a.get("actor"),
"result": a.get("result"), "power": a.get("power"),
"rationale": a.get("rationale")})
R["fallbackAuditEvents"] = audits
(HERE / "results2.json").write_text(
json.dumps(R, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
print(json.dumps({k: R[k] for k in (
"health", "licenseActivate", "features", "chatElapsedSec", "sseEventTypes",
"intentEvent", "replyLen", "replyContainsFallbackMarker", "runDirs", "resultJson",
"callsJsonl", "reportMdExists", "guardSanity", "fallbackAuditEvents") if k in R},
ensure_ascii=False, indent=2, default=str))
return 0
if __name__ == "__main__":
sys.exit(main())