2026-08-20 11:39:21 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 锐扬排产演示 · 一键重置 + 数据灌注(演示汇报用)
|
|
|
|
|
|
# 自动删除旧的「锐扬排产演示」项目 → 重建空项目 → 走完整对话流程:
|
|
|
|
|
|
# 解析文件夹 → 导入并试排 → MRP 分解 → 下达 → 柔性排产 → 发布 → 验证
|
|
|
|
|
|
# 对话过程实时写入会话消息(中间面板有完整自然语言交互记录)
|
|
|
|
|
|
# 目标后端:http://127.0.0.1:8000(当前运行中的演示应用)
|
|
|
|
|
|
# 用法:python demo-data\fill_ruiyang_demo.py
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
2026-09-08 00:07:26 +08:00
|
|
|
|
import os
|
2026-08-20 11:39:21 +08:00
|
|
|
|
import sys
|
|
|
|
|
|
import urllib.request
|
2026-09-08 00:07:26 +08:00
|
|
|
|
from pathlib import Path
|
2026-08-20 11:39:21 +08:00
|
|
|
|
|
2026-09-08 00:07:26 +08:00
|
|
|
|
BASE = (os.environ.get("APS_DEMO_API_URL") or "http://127.0.0.1:8000").rstrip("/")
|
2026-08-20 11:39:21 +08:00
|
|
|
|
PROJECT_NAME = "锐扬排产演示"
|
|
|
|
|
|
SCOPE_LABEL = "锐扬 APS 文件夹全流程演示"
|
2026-09-08 00:07:26 +08:00
|
|
|
|
WORKDIR = str(Path(
|
|
|
|
|
|
os.environ.get("RUIYANG_DEMO_DIR")
|
|
|
|
|
|
or (Path(__file__).resolve().parent / "锐扬APS演示数据")
|
|
|
|
|
|
).resolve())
|
2026-08-20 11:39:21 +08:00
|
|
|
|
PROJECT_ID = "" # 运行时由 reset_project() 确定
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def req(method: str, path: str, body: dict | None = None, timeout: int = 300) -> dict:
|
|
|
|
|
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
|
|
|
|
r = urllib.request.Request(
|
|
|
|
|
|
BASE + path, data=data, method=method,
|
|
|
|
|
|
headers={"Content-Type": "application/json"},
|
|
|
|
|
|
)
|
|
|
|
|
|
with urllib.request.urlopen(r, timeout=timeout) as resp:
|
|
|
|
|
|
return json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def chat(session_id: str, project_id: str, text: str) -> dict:
|
|
|
|
|
|
body = json.dumps({"sessionId": session_id, "projectId": project_id, "text": text, "history": []}).encode("utf-8")
|
|
|
|
|
|
r = urllib.request.Request(BASE + "/api/chat", data=body, method="POST",
|
|
|
|
|
|
headers={"Content-Type": "application/json"})
|
|
|
|
|
|
out = {"text": "", "intents": [], "blocks": [], "errors": []}
|
|
|
|
|
|
with urllib.request.urlopen(r, timeout=600) as resp:
|
|
|
|
|
|
for raw in resp:
|
|
|
|
|
|
line = raw.decode("utf-8", "ignore").strip()
|
|
|
|
|
|
if not line.startswith("data:"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
ev = json.loads(line[5:].strip())
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
continue
|
|
|
|
|
|
t = ev.get("type")
|
|
|
|
|
|
if t == "token":
|
|
|
|
|
|
out["text"] += ev.get("text") or ""
|
|
|
|
|
|
elif t == "intent":
|
|
|
|
|
|
out["intents"].append((ev.get("intent") or {}).get("intent"))
|
|
|
|
|
|
elif t == "block":
|
|
|
|
|
|
out["blocks"].append(ev.get("block") or {})
|
|
|
|
|
|
elif t == "error":
|
|
|
|
|
|
out["errors"].append(ev.get("message"))
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def confirm_all(session_id: str, blocks: list[dict]) -> list[str]:
|
|
|
|
|
|
"""批准所有 P2 确认卡,返回确认结果文案(用于写入会话记录)。"""
|
|
|
|
|
|
results = []
|
|
|
|
|
|
for b in blocks:
|
|
|
|
|
|
cid = (b.get("props") or {}).get("confirmId")
|
|
|
|
|
|
if cid:
|
|
|
|
|
|
res = req("POST", "/api/actions/confirm",
|
|
|
|
|
|
{"sessionId": session_id, "confirmId": cid, "approve": True})
|
|
|
|
|
|
print(f" 确认卡 {cid[:12]}… → {json.dumps(res, ensure_ascii=False)[:300]}")
|
|
|
|
|
|
if res.get("message"):
|
|
|
|
|
|
results.append(res["message"])
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Recorder:
|
|
|
|
|
|
"""把对话过程实时写入会话消息,让中间面板有自然语言交互记录。"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, session_id: str) -> None:
|
|
|
|
|
|
self.sid = session_id
|
|
|
|
|
|
self.messages: list[dict] = []
|
|
|
|
|
|
|
|
|
|
|
|
def turn(self, user_text: str, r: dict, confirms: list[str] | None = None) -> None:
|
|
|
|
|
|
self.messages.append({"role": "user", "text": user_text})
|
|
|
|
|
|
# 确认卡片块不落库(避免界面上出现已过期的确认按钮),确认结果以文本形式追加
|
|
|
|
|
|
blocks = [b for b in r["blocks"] if not (b.get("props") or {}).get("confirmId")]
|
|
|
|
|
|
text = r["text"]
|
|
|
|
|
|
for c in confirms or []:
|
|
|
|
|
|
text += f"\n\n✅ {c}"
|
|
|
|
|
|
agent: dict = {"role": "agent", "text": text, "streaming": False}
|
|
|
|
|
|
if r["intents"]:
|
|
|
|
|
|
agent["intent"] = {"intent": r["intents"][-1], "params": {}, "source": "RULE_FAST"}
|
|
|
|
|
|
if blocks:
|
|
|
|
|
|
agent["blocks"] = blocks
|
|
|
|
|
|
self.messages.append(agent)
|
|
|
|
|
|
self.flush()
|
|
|
|
|
|
|
|
|
|
|
|
def flush(self) -> None:
|
|
|
|
|
|
req("PUT", f"/api/sessions/{self.sid}/messages", {"messages": self.messages})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def step(title: str, r: dict, n: int = 700) -> None:
|
|
|
|
|
|
print(f"\n=== {title} intents={r['intents']} ===")
|
|
|
|
|
|
print(r["text"][:n])
|
|
|
|
|
|
if r["errors"]:
|
|
|
|
|
|
print(" !! ERRORS:", r["errors"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reset_project() -> str:
|
|
|
|
|
|
"""删除全部同名旧项目并重建空项目,返回新项目 ID。"""
|
|
|
|
|
|
global PROJECT_ID
|
|
|
|
|
|
ws = req("GET", "/api/workspace")
|
|
|
|
|
|
olds = [p for p in (ws.get("projects") or []) if p.get("name") == PROJECT_NAME]
|
|
|
|
|
|
for p in olds:
|
|
|
|
|
|
res = req("DELETE", f"/api/projects/{p['id']}")
|
|
|
|
|
|
print(f"[0] 删除旧项目 {p['id']}:{res.get('message')}")
|
|
|
|
|
|
proj = req("POST", "/api/projects", {
|
|
|
|
|
|
"name": PROJECT_NAME, "scopeLabel": SCOPE_LABEL, "workDir": WORKDIR,
|
|
|
|
|
|
})
|
|
|
|
|
|
PROJECT_ID = proj["project"]["id"]
|
|
|
|
|
|
print(f"[0] 新项目 {PROJECT_ID} workDir={WORKDIR}")
|
|
|
|
|
|
return PROJECT_ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
|
# 1) 重置项目(删旧建新)→ 新会话 → 激活工作区
|
|
|
|
|
|
reset_project()
|
|
|
|
|
|
sess = req("POST", "/api/sessions", {"projectId": PROJECT_ID, "title": "锐扬全流程演示"})
|
|
|
|
|
|
sid = sess["session"]["id"]
|
|
|
|
|
|
ws = req("PUT", "/api/workspace", {"activeProjectId": PROJECT_ID, "activeSessionId": sid, "messages": {}})
|
|
|
|
|
|
print(f"[1] 会话 {sid} 已激活 worldKey={ws.get('worldKey')}")
|
|
|
|
|
|
rec = Recorder(sid)
|
|
|
|
|
|
|
|
|
|
|
|
# 2) 解析文件夹
|
|
|
|
|
|
r = chat(sid, PROJECT_ID, "分析一下数据文件")
|
|
|
|
|
|
step("解析文件夹", r, 1000)
|
|
|
|
|
|
rec.turn("分析一下数据文件", r)
|
|
|
|
|
|
|
|
|
|
|
|
# 3) 导入并试排(P2 确认卡)
|
|
|
|
|
|
r = chat(sid, PROJECT_ID, "根据这个文件夹的数据导入并试排")
|
|
|
|
|
|
step("导入并试排", r)
|
|
|
|
|
|
rec.turn("根据这个文件夹的数据导入并试排", r, confirm_all(sid, r["blocks"]))
|
|
|
|
|
|
|
|
|
|
|
|
# 4) MRP 分解
|
|
|
|
|
|
r = chat(sid, PROJECT_ID, "执行MRP分解")
|
|
|
|
|
|
step("MRP分解", r)
|
|
|
|
|
|
rec.turn("执行MRP分解", r)
|
|
|
|
|
|
mrp = req("GET", "/api/mrp")
|
|
|
|
|
|
print(f" /api/mrp: make={len(mrp.get('make') or [])} purchase={len(mrp.get('purchaseOrders') or [])} outsource={len(mrp.get('outsourceOrders') or [])}")
|
|
|
|
|
|
|
|
|
|
|
|
# 5) 下达 MRP 建议单(P2 确认卡)
|
|
|
|
|
|
r = chat(sid, PROJECT_ID, "下达MRP建议单")
|
|
|
|
|
|
step("下达MRP建议单", r)
|
|
|
|
|
|
rec.turn("下达MRP建议单", r, confirm_all(sid, r["blocks"]))
|
|
|
|
|
|
|
|
|
|
|
|
# 6) 柔性排产
|
|
|
|
|
|
r = chat(sid, PROJECT_ID, "跑一版柔性排产")
|
|
|
|
|
|
step("柔性排产", r)
|
|
|
|
|
|
rec.turn("跑一版柔性排产", r, confirm_all(sid, r["blocks"]))
|
|
|
|
|
|
|
|
|
|
|
|
# 7) 发布排产版本(P2 确认卡)
|
|
|
|
|
|
r = chat(sid, PROJECT_ID, "发布当前排产版本")
|
|
|
|
|
|
step("发布排产版本", r)
|
|
|
|
|
|
rec.turn("发布当前排产版本", r, confirm_all(sid, r["blocks"]))
|
|
|
|
|
|
|
|
|
|
|
|
# 7) 验证
|
|
|
|
|
|
od = req("GET", "/api/orders")
|
|
|
|
|
|
print("\n=== 验证 /api/orders ===")
|
|
|
|
|
|
for k in ["orders", "productionOrders", "purchaseOrders", "outsourceOrders", "make"]:
|
|
|
|
|
|
v = od.get(k)
|
|
|
|
|
|
if isinstance(v, list):
|
|
|
|
|
|
print(f" {k}: {len(v)} 条")
|
|
|
|
|
|
print(f" hasSchedule: {od.get('hasSchedule')}")
|
|
|
|
|
|
|
|
|
|
|
|
s = req("GET", "/api/world/summary")
|
|
|
|
|
|
print(f"\n/api/world/summary: {json.dumps(s, ensure_ascii=False)[:400]}")
|
|
|
|
|
|
|
|
|
|
|
|
g = req("GET", "/api/flex/gantt")
|
|
|
|
|
|
rows = g.get("rows") or g.get("lines") or []
|
|
|
|
|
|
print(f"/api/flex/gantt: keys={list(g.keys())[:8]} 行数={len(rows)}")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\nDONE project={PROJECT_ID} session={sid}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(main())
|