aps-agent/server/gateway/app.py

1421 lines
69 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.

# ============================================================
# FastAPI 应用与路由(moduleId: gateway-app, 可重生 ✅)
# 端点清单:
# POST /api/chat 对话(SSE 流:meta/intent/token/command/block/done)
# GET /api/world/summary KPI 摘要
# GET /api/world/gantt 甘特视图数据
# GET /api/world/load 负荷热力数据
# GET /api/world/due 交期承诺看板数据
# GET /api/orders 订单管理页数据(订单 + 成品选项)
# POST /api/orders/stage 订单写入动作暂存确认卡(order.upsert/cancel/complete)
# GET /api/master 主数据管理页数据(资源树/物料BOM/日历维保)
# POST /api/master/stage 主数据写入动作暂存确认卡(master.*.upsert)
# GET /api/mrp MRP 建议单(采购/委外,订单分解产物)
# POST /api/mrp/decompose 订单分解(P1:产出 DRAFT 建议)
# GET /api/timeline 时间线导轨(检查点 + 版本,§4.4)
# POST /api/actions/confirm 确认卡回传(批准/驳回 P2 动作)
# POST /api/actions/scenario/apply 采用某个沙盒方案(P1 正式排产)
# GET /api/gov/pending 门禁管理台·待审批队列(§6.10.2)
# GET /api/gov/audit 门禁管理台·审计链 + 完整性校验(§3.6)
# GET /api/gov/modules 重生中心·可重生模块注册表(§6.10.3)
# GET /api/gov/policy 门禁管理台·权力矩阵投影(M2.5,docs/architecture/harness.md 的 API 化)
# GET /api/gov/tests 重生中心·黄金测试看板(M2.5,读缓存 / ?run=true 重跑)
# GET /api/settings/llm 设置中心·LLM Provider 状态(M2.5)
# GET /api/knowledge/assets 知识资产清单(M3 §8.1)
# GET /api/knowledge/assets/{id} 单个知识资产全文(M3)
# GET /api/reports/{type} 报告导出(Markdown 下载,M3 §9.10)
# ⚠ 文档同步铁律(plan.md §12.9):新增端点必同步 docs/architecture/harness.md 只读端点备案
# ============================================================
from __future__ import annotations # 前向类型引用
import asyncio # 流式分片的微延时
import json # SSE 载荷序列化
import uuid # 会话 ID 生成
from typing import Any, AsyncIterator # 类型标注
from fastapi import FastAPI, File, Form, UploadFile # Web 框架 + 文件上传
from fastapi.middleware.cors import CORSMiddleware # 跨域(开发期前端直连用)
from fastapi.responses import StreamingResponse # SSE 响应
from pydantic import BaseModel, Field # 请求体契约
from server.agent_core import harness # 门禁(待审批队列投影)
from server.agent_core.intent import recognize # 意图两级管线
from server.agent_core.registry import scan_modules, verify_audit_chain # 注册表与审计校验
from server.aps_domain.views import due_view, gantt_view, load_view, world_summary # 世界视图
from server.aps_domain.workflow import execute_confirmed, handle_intent # 工作流编排
from server.aps_domain.orders import (
confirmation_for_order_action, list_orders, list_products, pool_summary, ORDER_ACTIONS,
)
from server.aps_domain.masterdata import MASTER_ACTIONS, confirmation_for_master_action, master_overview
from server.aps_domain.mrp import decompose_orders, list_mrp, summarize_decomposition
from server.contracts import IntentResult # 方案采用时构造意图
from server.state.checkpoints import get_checkpoints # 成对快照仓(时间线)
from server.state.store import get_store # 世界状态单例
# ---------------- 请求体契约 ----------------
class ChatRequest(BaseModel):
"""对话请求:会话 ID(可空=新会话)+ 用户文本。"""
sessionId: str | None = None # 会话 ID(M1 仅作标识与审计维度)
text: str = Field(min_length=1, max_length=500) # 用户口令(限长防滥用)
class ConfirmRequest(BaseModel):
"""确认卡回传:确认 ID + 批准与否。"""
sessionId: str | None = None # 会话 ID(审计维度)
confirmId: str # 待确认动作的一次性令牌
approve: bool # True=批准执行 False=驳回
class ScenarioApplyRequest(BaseModel):
"""采用沙盒方案:策略 + 引擎(引擎确定性 → 重跑即得卡片同款结果)。"""
sessionId: str | None = None # 会话 ID(审计维度)
strategy: str # 选中方案的策略模板
engine: str = "RULE" # 引擎类型
class OrderStageRequest(BaseModel):
"""订单管理写入动作:只暂存确认卡,不直接改世界。"""
sessionId: str | None = None # 发起会话
action: str # order.upsert / order.cancel / order.complete / order.delete
payload: dict[str, Any] = Field(default_factory=dict) # 订单载荷
class MasterStageRequest(BaseModel):
"""主数据维护写入动作:只暂存确认卡,不直接改世界(MD-01/02/03)。"""
sessionId: str | None = None # 发起会话
action: str # master.line/material/maintenance/bom/routing.upsert
payload: dict[str, Any] = Field(default_factory=dict) # 主数据载荷
class ParamsStageRequest(BaseModel):
"""排产参数更新(OR-02):只暂存确认卡。"""
sessionId: str | None = None
payload: dict[str, Any] = Field(default_factory=dict)
class ConstraintStageRequest(BaseModel):
"""约束剖面更新(SC-04):只暂存确认卡。"""
sessionId: str | None = None
payload: dict[str, Any] = Field(default_factory=dict)
class SopStageRequest(BaseModel):
"""SOP 规则包应用(IND-02):只暂存确认卡。"""
sessionId: str | None = None
query: str = "换线"
pack: dict[str, Any] | None = None
class MrpDecomposeRequest(BaseModel):
"""订单分解请求(P1:产出 DRAFT 采购/委外建议)。"""
sessionId: str | None = None # 发起会话
orderNo: str | None = None # 指定订单号;空=全部可排产订单
class MrpReleaseRequest(BaseModel):
"""MRP 建议单下达请求(P2:只暂存确认卡,不直接改世界)。"""
sessionId: str | None = None # 发起会话
orderNo: str | None = None # 指定订单号;空=全部
kind: str = "all" # all / purchase / outsource
class FlexScheduleRequest(BaseModel):
"""柔性排产请求(P1:能力池+虚拟产线,写 flex* 草稿表)。"""
sessionId: str | None = None
sortMode: str | None = None # ASC / DESC / BOTTLENECK
orderIds: list[int] = Field(default_factory=list)
window: str | None = None # short/mid/long/full(SC-12)
enforceTeams: bool | None = None # SC-11 班组约束
class FlexSiteLoadRequest(BaseModel):
"""现场完整生产路线加载(P2:替换 flex* 演示种子)。"""
sessionId: str | None = None
routePath: str | None = None
dataDir: str | None = None
includeSiblings: bool = False
stationCount: int = 4
applyNow: bool = False # True=脚本同款直写(开发用);False=确认卡
class FlexRescheduleRequest(BaseModel):
"""分级重排请求(DY-01 L2/L3/L4 → P2 确认卡)。"""
sessionId: str | None = None
level: str = "L2" # L2 短窗 / L3 日窗 / L4 全局
sortMode: str | None = None
class FlexSwapRequest(BaseModel):
"""L1 局部换机(P1):池内备机接手。"""
sessionId: str | None = None
equipmentCode: str | None = None
woId: int | None = None
targetCode: str | None = None
markFault: bool = True
class FlexSimulateRequest(BaseModel):
"""交期承诺模拟请求(PL-08,只读沙盒,不改主干)。"""
sessionId: str | None = None
productCode: str
quantity: int = Field(gt=0)
sortMode: str | None = None
class FlexRushRequest(BaseModel):
sessionId: str | None = None
productCode: str
quantity: int = Field(gt=0)
dueDate: str | None = None
priority: int = 1
class RushEvaluateRequest(BaseModel):
"""OR-04 固定轨插单快评(P1 沙盒)。"""
sessionId: str | None = None
orderNo: str | None = None
customerName: str | None = None
customerLevel: str | None = None
productId: int | None = None
productCode: str | None = None
quantity: int | None = None
deliveryDate: str | None = None
priority: int | None = None
strategy: str | None = None
class RushApplyRequest(BaseModel):
"""OR-04 采用插单(P2 确认卡)。"""
sessionId: str | None = None
payload: dict[str, Any] = Field(default_factory=dict)
strategy: str | None = None
evalId: str | None = None
class WorkspaceReplaceRequest(BaseModel):
"""AG-08 工作区整包同步。"""
projects: list[dict[str, Any]] = Field(default_factory=list)
sessions: list[dict[str, Any]] = Field(default_factory=list)
files: list[dict[str, Any]] = Field(default_factory=list)
messages: dict[str, list[Any]] | None = None
activeProjectId: str | None = None
activeSessionId: str | None = None
worldKey: str | None = "default"
class ProjectCreateRequest(BaseModel):
name: str = "未命名项目"
scopeLabel: str | None = None
class SessionCreateRequest(BaseModel):
projectId: str
title: str | None = "新话题"
class MessagesReplaceRequest(BaseModel):
messages: list[Any] = Field(default_factory=list)
class MessageAppendRequest(BaseModel):
message: dict[str, Any] = Field(default_factory=dict)
class ForecastStageRequest(BaseModel):
"""OR-05 预测订单 P2 暂存。"""
sessionId: str | None = None
action: str
payload: dict[str, Any] = Field(default_factory=dict)
class FlexFaultRequest(BaseModel):
sessionId: str | None = None
equipmentCode: str
status: str = "MAINTENANCE"
reschedule: bool = True
class FlexResourcePatchRequest(BaseModel):
sessionId: str | None = None
kind: str # equipment | mold
code: str
patch: dict[str, Any] = Field(default_factory=dict)
class ConflictResolveRequest(BaseModel):
sessionId: str | None = None
conflictId: int
action: str
params: dict[str, Any] = Field(default_factory=dict)
class FlexAdjustRequest(BaseModel):
sessionId: str | None = None
woId: int
newStart: str # YYYY-MM-DD HH:MM
class ScheduleAdjustRequest(BaseModel):
sessionId: str | None = None
woId: int
newStart: str
class SapSyncRequest(BaseModel):
sessionId: str | None = None
class MesDispatchRequest(BaseModel):
sessionId: str | None = None
track: str = "flex" # flex | fixed
class MesReportRequest(BaseModel):
sessionId: str | None = None
woId: int
track: str = "flex"
progressPct: int | None = None
finish: bool = False
# ---------------- SSE 工具 ----------------
def _sse(payload: dict[str, Any]) -> str:
"""把一个事件对象编码为 SSE 帧(data: {json}\\n\\n)。"""
return "data: " + json.dumps(payload, ensure_ascii=False) + "\n\n" # 标准 SSE 数据帧
def create_app() -> FastAPI:
"""应用工厂:注册中间件与全部路由。"""
app = FastAPI(title="APS Planning Agent", version="0.1.0") # 应用实例
app.add_middleware( # 开发期放开跨域(生产由网关收紧)
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# ---------------- 对话:SSE 会话流 ----------------
@app.post("/api/chat")
async def chat(req: ChatRequest) -> StreamingResponse:
"""一轮对话(权力等级:随意图而定;本端点只编排不执行 P2)。"""
store = get_store() # 世界状态
session_id = req.sessionId or uuid.uuid4().hex[:12] # 会话 ID(缺省新建)
async def stream() -> AsyncIterator[str]: # SSE 事件生成器
yield _sse({"type": "meta", "sessionId": session_id}) # ① 会话元信息
try:
intent = await recognize(req.text, store.data) # ② 意图识别(两级管线)
yield _sse({"type": "intent", "intent": intent.model_dump()}) # ③ 意图作为协议证据下发
reply = await handle_intent(store, session_id, intent) # ④ 工作流执行(P0/P1 直通,P2 出卡)
for i in range(0, len(reply.text), 24): # ⑤ 正文分片(打字机流式感)
yield _sse({"type": "token", "text": reply.text[i:i + 24]}) # 24 字符一片
await asyncio.sleep(0.02) # 微延时(不阻塞事件循环)
for cmd in reply.commands: # ⑥ 视口命令逐条下发
yield _sse({"type": "command", "command": cmd.model_dump()})
for block in reply.blocks: # ⑦ UI 块(确认卡等)逐条下发
yield _sse({"type": "block", "block": block.model_dump()})
except Exception as exc: # 兜底:任何异常转为 error 事件(连接不中断)
yield _sse({"type": "error", "message": f"处理失败:{exc}"})
yield _sse({"type": "done"}) # ⑧ 结束标记
return StreamingResponse(stream(), media_type="text/event-stream", # SSE 响应
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
# ---------------- 世界视图(P0 只读) ----------------
@app.get("/api/world/summary")
async def summary() -> dict:
"""KPI 摘要(视口工具栏)。"""
return world_summary(get_store().data) # 直接投影
@app.get("/api/world/gantt")
async def gantt() -> dict:
"""甘特视图数据(最新版本工单 + 骨架 + 标记)。"""
return gantt_view(get_store().data) # 直接投影
@app.get("/api/world/load")
async def load(days: int = 14) -> dict:
"""负荷热力数据(产线 × 未来 N 天)。"""
return load_view(get_store().data, days=days) # 直接投影
@app.get("/api/world/due")
async def due() -> dict:
"""交期承诺看板数据。"""
return {"rows": due_view(get_store().data)} # 包一层便于扩展
# ---------------- 订单管理(P0 读;P2 写经确认卡) ----------------
@app.get("/api/orders")
async def orders() -> dict:
"""订单管理页数据:销售订单列表 + 成品下拉选项。"""
world = get_store().data
return {
"orders": list_orders(world), "products": list_products(world),
"statuses": ["DRAFT", "SUBMITTED", "APPROVED", "REJECTED", "CHANGED", "CANCELLED", "COMPLETED"],
"levels": ["VIP", "A", "B", "C"],
"pool": pool_summary(world),
}
@app.get("/api/forecast")
async def forecast_list() -> dict:
"""OR-05 预测订单台账(P0)。"""
from server.aps_domain.forecast import ensure_forecast_table, forecast_summary, list_forecasts
from server.aps_domain.orders import list_products
world = get_store().data
ensure_forecast_table(world)
return {
"forecasts": list_forecasts(world),
"summary": forecast_summary(world),
"products": list_products(world),
"statuses": ["DRAFT", "ACTIVE", "CONSUMED", "CANCELLED"],
"buckets": ["DAY", "WEEK", "MONTH"],
}
@app.get("/api/plan/buckets")
async def plan_buckets(
mode: str = "HYBRID",
horizonDays: int = 90,
includeForecast: bool = True,
startDate: str | None = None,
capacityMode: str = "FINITE",
) -> dict:
"""PL-01/PL-02 时间分桶 + 有限/无限粗能力(P0)。"""
from server.aps_domain.planning import build_plan_buckets
try:
return build_plan_buckets(
get_store().data,
mode=mode,
start_date=startDate,
horizon_days=horizonDays,
include_forecast=includeForecast,
capacity_mode=capacityMode,
)
except ValueError as exc:
return {"error": str(exc)}
@app.get("/api/plan/rccp")
async def plan_rccp(
mode: str = "HYBRID",
horizonDays: int = 90,
includeForecast: bool = True,
startDate: str | None = None,
) -> dict:
"""PL-02 有限/无限粗能力对照(P0)。"""
from server.aps_domain.planning import build_rccp_compare
try:
return build_rccp_compare(
get_store().data,
mode=mode,
start_date=startDate,
horizon_days=horizonDays,
include_forecast=includeForecast,
)
except ValueError as exc:
return {"error": str(exc)}
@app.get("/api/plan/feasibility")
async def plan_feasibility(
horizonDays: int = 90,
includeForecast: bool = True,
startDate: str | None = None,
) -> dict:
"""PL-03 交期可行性(P0,前置于排产)。"""
from server.aps_domain.planning import build_feasibility
try:
return build_feasibility(
get_store().data,
start_date=startDate,
horizon_days=horizonDays,
include_forecast=includeForecast,
)
except ValueError as exc:
return {"error": str(exc)}
@app.get("/api/plan/inventory")
async def plan_inventory(
horizonDays: int = 30,
includeForecast: bool = True,
startDate: str | None = None,
bucket: str = "DAY",
materialCode: str | None = None,
materialType: str | None = None,
) -> dict:
"""PL-04 库存投影(P0)。"""
from server.aps_domain.inventory import build_inventory_projection
try:
return build_inventory_projection(
get_store().data,
material_code=materialCode,
material_type=materialType,
horizon_days=horizonDays,
bucket=bucket,
include_forecast=includeForecast,
start_date=startDate,
)
except ValueError as exc:
return {"error": str(exc)}
@app.get("/api/plan/leveling")
async def plan_leveling(
mode: str = "WEEK",
horizonDays: int = 90,
includeForecast: bool = True,
startDate: str | None = None,
targetLoad: float = 0.85,
) -> dict:
"""PL-05 产能平衡/削峰建议(P0)。"""
from server.aps_domain.planning import build_leveling
try:
return build_leveling(
get_store().data,
mode=mode,
start_date=startDate,
horizon_days=horizonDays,
include_forecast=includeForecast,
target_load=targetLoad,
)
except ValueError as exc:
return {"error": str(exc)}
@app.get("/api/plan/supply")
async def plan_supply(
mode: str = "WEEK",
horizonDays: int = 90,
includeForecast: bool = True,
startDate: str | None = None,
targetLoad: float = 0.85,
) -> dict:
"""PL-06 产供方向决策(P0)。"""
from server.aps_domain.planning import build_supply_decisions
try:
return build_supply_decisions(
get_store().data,
mode=mode,
start_date=startDate,
horizon_days=horizonDays,
include_forecast=includeForecast,
target_load=targetLoad,
)
except ValueError as exc:
return {"error": str(exc)}
@app.post("/api/forecast/stage")
async def forecast_stage(req: ForecastStageRequest) -> dict:
"""预测写入动作暂存确认卡(P2)。"""
from server.aps_domain.forecast import confirmation_for_forecast_action
if req.action not in ("forecast.upsert", "forecast.delete", "forecast.convert"):
return {"error": "不支持的预测动作"}
store = get_store()
try:
title, lines = confirmation_for_forecast_action(store.data, req.action, req.payload)
except ValueError as exc:
return {"error": str(exc)}
block = harness.stage_confirmation(req.sessionId or "web", req.action, req.payload,
title=title, summary_lines=lines)
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action=f"{req.action}.stage",
target={"type": "FORECAST_ORDER",
"id": req.payload.get("id") or req.payload.get("forecastNo") or "NEW"},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列", "block": block.model_dump()}
@app.post("/api/orders/submit")
async def order_submit(req: OrderStageRequest) -> dict:
"""订单提交审核(OR-03,P1 直通)。"""
from server.aps_domain.orders import apply_order_submit
store = get_store()
try:
applied = harness.guard("order.submit", req.payload,
lambda: apply_order_submit(store.data, req.payload))
except ValueError as exc:
return {"error": str(exc)}
from server.agent_core.audit import write_audit
o = applied["order"]
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="WORLD_WRITE",
action="order.submit",
target={"type": "SALES_ORDER", "id": o["id"], "orderNo": o["orderNo"]},
power="P1", rationale={"from": applied.get("beforeStatus"), "to": "SUBMITTED"})
store.save()
return {"message": f"订单 {o['orderNo']} 已提交审核",
"order": {"id": o["id"], "orderNo": o["orderNo"], "status": o["status"]}}
@app.post("/api/rush/evaluate")
async def rush_evaluate(req: RushEvaluateRequest) -> dict:
"""OR-04 插单影响快评(P1 沙盒,不改主干)。"""
from server.aps_domain import workflow as wf
from server.aps_domain.rush import evaluate_rush, format_impact_text
from server.agent_core.audit import write_audit
store = get_store()
payload = {k: v for k, v in req.model_dump().items()
if k != "sessionId" and v is not None}
try:
impact = harness.guard("rush.evaluate", payload,
lambda: evaluate_rush(store.data, payload))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
wf._LAST_RUSH_EVAL.clear()
wf._LAST_RUSH_EVAL.update(impact)
write_audit(store.data, store.next_id, actor=req.sessionId or "web",
category="ALGO_RUN", action="rush.evaluate",
target={"type": "SANDBOX", "id": impact["evalId"]}, power="P1",
rationale={"affected": impact["affectedOrderCount"],
"delayDelta": impact["delayDelta"],
"conflictDelta": impact["conflictDelta"]})
store.save()
return {"message": format_impact_text(impact), "impact": impact}
@app.post("/api/rush/stage")
async def rush_stage(req: RushApplyRequest) -> dict:
"""OR-04 采用插单确认卡(P2)。"""
from server.aps_domain import workflow as wf
from server.agent_core.audit import write_audit
store = get_store()
payload: dict[str, Any] = {
"payload": req.payload or None,
"strategy": req.strategy,
"evalId": req.evalId,
}
if not req.payload and wf._LAST_RUSH_EVAL:
payload = {
"payload": wf._LAST_RUSH_EVAL.get("payload"),
"strategy": req.strategy or wf._LAST_RUSH_EVAL.get("strategy"),
"evalId": req.evalId or wf._LAST_RUSH_EVAL.get("evalId"),
}
if not payload.get("payload"):
return {"error": "缺少插单载荷,请先执行插单快评"}
rush = payload["payload"]
label = rush.get("orderNo") or f"{rush.get('customerName', '急单')}×{rush.get('quantity', '?')}"
block = harness.stage_confirmation(
req.sessionId or "web", "rush.apply", payload,
title=f"采用紧急插单 · {label}",
summary_lines=[
f"策略:{payload.get('strategy') or 'DELIVERY_FIRST'}",
"将写入主干订单池(APPROVED + isRush),并生成 DRAFT 排产版本",
"执行前自动建档,可回滚",
],
)
write_audit(store.data, store.next_id, actor=req.sessionId or "web",
category="GATE", action="rush.apply.stage",
target={"type": "SALES_ORDER", "id": rush.get("orderNo") or "NEW"},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": "已生成采用插单确认卡", "block": block.model_dump()}
@app.post("/api/orders/stage")
async def order_stage(req: OrderStageRequest) -> dict:
"""订单写入动作暂存为确认卡(P2 唯一路径仍是 /api/actions/confirm)。"""
if req.action not in ("order.upsert", "order.cancel", "order.complete", "order.delete", "order.clear",
"order.approve", "order.reject"):
return {"error": "不支持的订单动作"}
store = get_store()
try:
title, lines = confirmation_for_order_action(store.data, req.action, req.payload)
except ValueError as exc:
return {"error": str(exc)}
block = harness.stage_confirmation(req.sessionId or "web", req.action, req.payload,
title=title, summary_lines=lines)
write_action = req.action + ".stage"
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action=write_action, target={"type": "SALES_ORDER", "id": req.payload.get("id") or "ALL"},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
# ---------------- 主数据维护(MD-01/02/03:P0 读;P2 写经确认卡) ----------------
@app.get("/api/master")
async def master() -> dict:
"""主数据管理页数据:资源树 + 物料/BOM/路线 + 日历/维保。"""
return master_overview(get_store().data)
@app.post("/api/master/stage")
async def master_stage(req: MasterStageRequest) -> dict:
"""主数据写入动作暂存为确认卡(P2 唯一执行路径仍是 /api/actions/confirm)。"""
if req.action not in MASTER_ACTIONS:
return {"error": "不支持的主数据动作"}
store = get_store()
try:
title, lines = confirmation_for_master_action(store.data, req.action, req.payload)
except ValueError as exc: # 校验失败:不出卡,直接回错误
return {"error": str(exc)}
block = harness.stage_confirmation(req.sessionId or "web", req.action, req.payload,
title=title, summary_lines=lines)
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action=req.action + ".stage", target={"type": "MASTER_DATA", "id": req.payload.get("id")},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
# ---------------- MRP 订单分解(P0 读;P1 分解直通) ----------------
@app.get("/api/mrp")
async def mrp() -> dict:
"""MRP 建议单投影:采购建议 + 委外建议。"""
return list_mrp(get_store().data)
@app.post("/api/mrp/decompose")
async def mrp_decompose(req: MrpDecomposeRequest) -> dict:
"""订单分解(P1 直通:只写 DRAFT 建议表,经 harness.guard 校验权级)。"""
store = get_store()
try:
result = harness.guard("order.decompose", {"orderNo": req.orderNo},
lambda: decompose_orders(store.data, store.next_id, req.orderNo))
except ValueError as exc:
return {"error": str(exc)}
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="ALGO_RUN",
action="order.decompose", target={"type": "MRP", "id": req.orderNo or "ALL"},
power="P1", rationale={"purchase": len(result["purchase"]),
"outsource": len(result["outsource"])})
store.save()
return {"message": summarize_decomposition(result), "result": result}
@app.get("/api/plan/trace")
async def plan_trace_api(orderNo: str | None = None, track: str | None = None) -> dict:
"""计划追溯 / 订单钉扎(P0):固定 SO 链或柔性 FO 链(OR-06)。"""
from server.aps_domain.trace import plan_trace
try:
return plan_trace(get_store().data, orderNo, track=track)
except ValueError as exc:
return {"error": str(exc)}
@app.post("/api/mrp/release")
async def mrp_release(req: MrpReleaseRequest) -> dict:
"""MRP 建议单下达暂存为确认卡(P2 唯一执行路径仍是 /api/actions/confirm)。"""
from server.aps_domain.mrp import confirmation_for_mrp_release
store = get_store()
try:
title, lines = confirmation_for_mrp_release(store.data, req.orderNo, req.kind)
except ValueError as exc:
return {"error": str(exc)}
params = {"orderNo": req.orderNo, "kind": req.kind}
block = harness.stage_confirmation(req.sessionId or "web", "mrp.release", params,
title=title, summary_lines=lines)
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action="mrp.release.stage", target={"type": "MRP", "id": req.orderNo or "ALL"},
power="P2", rationale={"confirmId": block.props["confirmId"], "kind": req.kind})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
# ---------------- MD-04 Excel/CSV 导入(preview P1 + commit P2) ----------------
@app.post("/api/import/preview")
async def import_preview(file: UploadFile = File(...)) -> dict:
"""上传 Excel/CSV,校验并返回可入库批次(不写主干)。"""
from server.aps_domain.importers import preview_file
raw = await file.read()
try:
result = preview_file(file.filename or "upload.xlsx", raw, get_store().data)
except ValueError as exc:
return {"error": str(exc)}
return result
class ImportCommitRequest(BaseModel):
sessionId: str = ""
filename: str = "upload"
batches: list[dict[str, Any]] = Field(default_factory=list)
@app.post("/api/import/commit")
async def import_commit(req: ImportCommitRequest) -> dict:
"""将 preview 通过的批次暂存为 P2 确认卡。"""
from server.aps_domain.importers import confirmation_for_import_commit
if not req.batches or not any(b.get("okRows") for b in req.batches):
return {"error": "没有可入库的有效行"}
preview = {
"filename": req.filename,
"batches": req.batches,
"totalOk": sum(len(b.get("okRows") or []) for b in req.batches),
"totalErrors": sum(len(b.get("errors") or []) for b in req.batches),
}
title, lines = confirmation_for_import_commit(preview)
# 确认卡只带 okRows,避免整份文件进会话
slim = [{"kind": b["kind"], "sheet": b.get("sheet"), "okRows": b.get("okRows") or []}
for b in req.batches if b.get("okRows")]
block = harness.stage_confirmation(
req.sessionId or "web", "import.commit",
{"filename": req.filename, "batches": slim},
title=title, summary_lines=lines,
)
from server.agent_core.audit import write_audit
store = get_store()
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action="import.commit.stage", target={"type": "IMPORT", "id": req.filename},
power="P2", rationale={"confirmId": block.props["confirmId"],
"totalOk": preview["totalOk"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
# ---------------- 现场完整生产路线 → flex*(P2 确认卡;applyNow 供脚本/联调) ----------------
@app.post("/api/flex/site/load")
async def flex_site_load(req: FlexSiteLoadRequest) -> dict:
"""加载现场「完整生产路线」xlsx,替换柔性演示种子。"""
from server.aps_domain.kangni_intake import (
build_flex_bundle, confirmation_for_site_load, load_site_into_world,
)
from server.agent_core.audit import write_audit
store = get_store()
try:
bundle = build_flex_bundle(
req.routePath, req.dataDir,
include_sibling_orders=req.includeSiblings,
station_count=req.stationCount,
)
except (FileNotFoundError, ValueError) as exc:
return {"error": str(exc)}
meta = bundle.get("_meta") or {}
if req.applyNow:
meta = load_site_into_world(
store.data, route_path=req.routePath, data_dir=req.dataDir,
include_sibling_orders=req.includeSiblings, station_count=req.stationCount,
)
write_audit(store.data, store.next_id, actor=req.sessionId or "web",
category="WORLD_WRITE", action="flex.site.load",
target={"type": "FLEX_SITE", "id": meta.get("primaryOrder")}, power="P2",
rationale={"applyNow": True, "meta": meta})
store.save()
return {"message": f"现场路线已加载:主单 {meta.get('primaryOrder')},"
f"订单 {meta.get('orderCount')}。", "meta": meta}
title, lines = confirmation_for_site_load(meta)
block = harness.stage_confirmation(
req.sessionId or "web", "flex.site.load",
{"routePath": req.routePath, "dataDir": req.dataDir,
"includeSiblings": req.includeSiblings, "stationCount": req.stationCount},
title=title, summary_lines=lines,
)
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action="flex.site.load.stage",
target={"type": "FLEX_SITE", "id": meta.get("primaryOrder")}, power="P2",
rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump(), "meta": meta}
# ---------------- 柔性排产(能力池+虚拟产线;P0 读,P1 排产直通) ----------------
@app.get("/api/flex/world")
async def flex_world() -> dict:
"""柔性排产页数据:区域/能力池/设备/模具/订单 + 最新版本虚拟产线与冲突。"""
from server.aps_domain.flex import flex_overview
return flex_overview(get_store().data)
@app.post("/api/flex/schedule")
async def flex_schedule(req: FlexScheduleRequest) -> dict:
"""执行柔性排产(P1 直通:能力池选设备 + 组虚拟产线,只写 flex* 草稿表)。"""
from server.aps_domain.flex import run_flex_schedule
store = get_store()
try:
result = run_flex_schedule(store, sort_mode=req.sortMode,
order_ids=req.orderIds or None,
actor=req.sessionId or "web",
window=req.window,
enforce_teams=req.enforceTeams)
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
vl = result["vlCount"]
win = result.get("window") or "full"
return {"message": f"柔性排产完成({result['sortMode']}/{win}):生成 {vl} 条虚拟产线 / "
f"{result['woCount']} 个工单,冲突 {result['conflictCount']},"
f"延期窗外 {result.get('deferredCount', 0)}。", "result": result}
@app.post("/api/flex/reschedule")
async def flex_reschedule(req: FlexRescheduleRequest) -> dict:
"""分级重排暂存为确认卡(DY-01 L2/L3/L4 → P2;执行走 /api/actions/confirm)。"""
from server.aps_domain.flex import preview_reschedule
from server.agent_core.audit import write_audit
store = get_store()
level = (req.level or "L2").upper()
try:
title, lines = preview_reschedule(store, level)
except ValueError as exc:
return {"error": str(exc)}
params = {"level": level, "sortMode": req.sortMode}
block = harness.stage_confirmation(req.sessionId or "web", "flex.reschedule", params,
title=title, summary_lines=lines)
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action="flex.reschedule.stage", target={"type": "FLEX", "id": level},
power="P2", rationale={"confirmId": block.props["confirmId"], "level": level})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
@app.post("/api/flex/swap")
async def flex_swap(req: FlexSwapRequest) -> dict:
"""L1 局部换机(P1):池内备机接手,他单不动。"""
from server.aps_domain.flex import local_swap_equipment
store = get_store()
try:
result = local_swap_equipment(
store, equipment_code=req.equipmentCode, wo_id=req.woId,
target_code=req.targetCode, mark_fault=req.markFault,
actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
n = len(result.get("swapped") or [])
return {"message": f"L1 局部换机完成:{n} 条工单改挂备机"
+ (f";设备 {result['faulted']} 已标故障" if result.get("faulted") else "")
+ "。", "result": result}
@app.get("/api/flex/gantt")
async def flex_gantt() -> dict:
"""柔性甘特视图数据(P0 只读):行=设备(按区域分组),条=柔性工单。"""
from server.aps_domain.flex import flex_gantt_view
return flex_gantt_view(get_store().data)
@app.post("/api/flex/simulate-due")
async def flex_simulate_due(req: FlexSimulateRequest) -> dict:
"""交期承诺模拟(PL-08,P0 只读沙盒):乐观/预计/悲观 + 缺口建议。"""
from server.aps_domain.flex import simulate_due
store = get_store()
result = simulate_due(store, req.productCode, req.quantity, sort_mode=req.sortMode)
return {"result": result}
@app.get("/api/flex/capacity")
async def flex_capacity() -> dict:
"""瓶颈产能法分析(PL-07,P0 只读):各能力池日产能 + 限制性瓶颈池。"""
from server.aps_domain.flex import capacity_analysis
return capacity_analysis(get_store().data)
@app.post("/api/flex/compare")
async def flex_compare() -> dict:
"""三模式对比(SC-13,P0 沙盒)。"""
from server.aps_domain.flex import compare_sort_modes
return compare_sort_modes(get_store(), compress_due=True)
@app.post("/api/flex/rush")
async def flex_rush(req: FlexRushRequest) -> dict:
"""紧急插单 + 重排(DY-01,P1)。"""
from server.aps_domain.flex import insert_rush_order
from server.agent_core import harness
store = get_store()
try:
harness.guard("flex.rush", {"productCode": req.productCode, "quantity": req.quantity},
lambda: None)
result = insert_rush_order(store, req.productCode, req.quantity,
due_date=req.dueDate, priority=req.priority,
actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": f"急单 {result['order']['orderNo']} 已插入并重排。", "result": result}
@app.post("/api/flex/fault")
async def flex_fault(req: FlexFaultRequest) -> dict:
"""设备故障/恢复 + 可选重排(DY-01,P1)。"""
from server.aps_domain.flex import apply_equipment_fault
from server.agent_core import harness
store = get_store()
try:
harness.guard("flex.fault", {"equipmentCode": req.equipmentCode, "status": req.status},
lambda: None)
result = apply_equipment_fault(store, req.equipmentCode, status=req.status,
reschedule=req.reschedule, actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
eq = result["equipment"]
level = result.get("responseLevel")
msg = f"设备 {eq['code']} {eq['beforeStatus']}→{eq['afterStatus']}"
if level == "L1":
n = len((result.get("swap") or {}).get("swapped") or [])
msg += f";已 L1 局部换机 {n} 条。"
elif result.get("schedule"):
msg += f";已缩池重排({level or 'L4'})。"
else:
msg += "。"
return {"message": msg, "result": result}
@app.post("/api/flex/resource/patch")
async def flex_resource_patch(req: FlexResourcePatchRequest) -> dict:
"""柔性资源补丁:设备状态/区域、模具寿命/锁定(EX-10 / MD-07,P1)。"""
from server.aps_domain.flex import patch_flex_resource
from server.agent_core import harness
store = get_store()
try:
harness.guard("flex.resource.patch", {"kind": req.kind, "code": req.code},
lambda: None)
result = patch_flex_resource(store, req.kind, req.code, req.patch,
actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": f"已更新 {req.kind} {req.code}。", "result": result}
@app.get("/api/flex/conflicts")
async def flex_conflicts(scope: str = "flex") -> dict:
"""冲突中心(EX-03,P0):最新版本未解决冲突 + 可执行建议。"""
from server.aps_domain.conflicts import list_conflict_center
return list_conflict_center(get_store().data, scope=scope)
@app.post("/api/flex/conflicts/resolve")
async def flex_conflict_resolve(req: ConflictResolveRequest) -> dict:
"""冲突一键修复(EX-03):P1 直通 / P2 出确认卡。"""
from server.aps_domain.conflicts import apply_conflict_fix
store = get_store()
try:
return apply_conflict_fix(
store, req.conflictId, req.action, params=req.params,
actor=req.sessionId or "web", session_id=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc), "applied": False}
@app.post("/api/flex/adjust/preview")
async def flex_adjust_preview(req: FlexAdjustRequest) -> dict:
"""甘特调程预览(EX-04,P0):不改主干。"""
from server.aps_domain.adjust import preview_flex_adjust
try:
return preview_flex_adjust(get_store().data, req.woId, req.newStart)
except ValueError as exc:
return {"ok": False, "error": str(exc), "reasons": [str(exc)], "moves": []}
@app.post("/api/flex/adjust/stage")
async def flex_adjust_stage(req: FlexAdjustRequest) -> dict:
"""甘特调程暂存确认卡(EX-04,P2)。"""
from server.aps_domain.adjust import stage_flex_adjust
store = get_store()
try:
return stage_flex_adjust(store, req.woId, req.newStart,
session_id=req.sessionId or "web",
actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
@app.post("/api/schedule/adjust/preview")
async def schedule_adjust_preview(req: ScheduleAdjustRequest) -> dict:
"""固定轨甘特调程预览(EX-04,P0)。"""
from server.aps_domain.adjust import preview_fixed_adjust
try:
return preview_fixed_adjust(get_store().data, req.woId, req.newStart)
except ValueError as exc:
return {"ok": False, "error": str(exc), "reasons": [str(exc)], "moves": []}
@app.post("/api/schedule/adjust/stage")
async def schedule_adjust_stage(req: ScheduleAdjustRequest) -> dict:
"""固定轨甘特调程暂存确认卡(EX-04,P2)。"""
from server.aps_domain.adjust import stage_fixed_adjust
store = get_store()
try:
return stage_fixed_adjust(store, req.woId, req.newStart,
session_id=req.sessionId or "web",
actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
@app.get("/api/sap/status")
async def sap_status() -> dict:
"""SAP 连接状态(MD-05,P0 / Mock 桩)。"""
from server.aps_domain.sap_sync import sap_connection_status
return sap_connection_status()
@app.get("/api/sap/inbound/preview")
async def sap_inbound_preview() -> dict:
"""SAP→APS 入站预览(MD-05,P0)。"""
from server.aps_domain.sap_sync import preview_inbound
return preview_inbound(get_store().data)
@app.post("/api/sap/inbound/stage")
async def sap_inbound_stage(req: SapSyncRequest) -> dict:
"""SAP 入站确认卡(MD-05,P2)。"""
from server.aps_domain.sap_sync import stage_sap_sync
return stage_sap_sync(get_store(), "inbound",
session_id=req.sessionId or "web",
actor=req.sessionId or "web")
@app.get("/api/sap/outbound/preview")
async def sap_outbound_preview() -> dict:
"""APS→SAP 出站预览(MD-05,P0)。"""
from server.aps_domain.sap_sync import preview_outbound
return preview_outbound(get_store().data)
@app.post("/api/sap/outbound/stage")
async def sap_outbound_stage(req: SapSyncRequest) -> dict:
"""SAP 出站确认卡(MD-05,P2)。"""
from server.aps_domain.sap_sync import stage_sap_sync
return stage_sap_sync(get_store(), "outbound",
session_id=req.sessionId or "web",
actor=req.sessionId or "web")
@app.get("/api/mes/status")
async def mes_status() -> dict:
"""MES 连接状态(EX-05,P0 / Mock)。"""
from server.aps_domain.mes import mes_connection_status
return mes_connection_status()
@app.get("/api/mes/dispatch/preview")
async def mes_dispatch_preview(track: str = "flex") -> dict:
"""MES 下发预览(EX-05,P0)。"""
from server.aps_domain.mes import preview_dispatch
return preview_dispatch(get_store().data, track=track)
@app.post("/api/mes/dispatch/stage")
async def mes_dispatch_stage(req: MesDispatchRequest) -> dict:
"""MES 下发确认卡(EX-05,P3)。"""
from server.aps_domain.mes import stage_dispatch
return stage_dispatch(get_store(), track=req.track or "flex",
session_id=req.sessionId or "web",
actor=req.sessionId or "web")
@app.get("/api/mes/execution")
async def mes_execution(track: str = "flex") -> dict:
"""已下发工单进度(EX-09,P0)。"""
from server.aps_domain.mes import list_execution
return list_execution(get_store().data, track=track)
@app.post("/api/mes/report")
async def mes_report(req: MesReportRequest) -> dict:
"""报工回流(EX-09,P1)。"""
from server.aps_domain.mes import apply_report
try:
return apply_report(
get_store(), req.woId, track=req.track or "flex",
progress_pct=req.progressPct, finish=req.finish,
actor=req.sessionId or "web")
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
@app.get("/api/analytics/kpi")
async def analytics_kpi() -> dict:
"""KPI 仪表盘(EX-08,P0)。"""
from server.aps_domain.analytics import build_kpi_dashboard
return build_kpi_dashboard(get_store().data)
@app.get("/api/analytics/utilization")
async def analytics_utilization(track: str = "flex", days: int = 7) -> dict:
"""资源利用率增强(EX-06,P0):设备热力 + 班组并发。"""
from server.aps_domain.analytics import build_utilization_report
return build_utilization_report(get_store().data, track=track, days=days)
@app.get("/api/analytics/compare")
async def analytics_compare(track: str = "both") -> dict:
"""方案对比表(EX-07,P0 沙盒)。"""
from server.aps_domain.analytics import build_compare_table
return build_compare_table(get_store().data, track=track)
@app.get("/api/guidance")
async def guidance_next(context: str | None = None) -> dict:
"""主动引导下一步建议(AG-07,P0)。"""
from server.aps_domain.guidance import build_guidance
return build_guidance(get_store().data, context=context)
@app.get("/api/params")
async def schedule_params() -> dict:
"""排产参数投影(OR-02,P0)。"""
from server.aps_domain.params import get_schedule_params
return get_schedule_params(get_store().data)
@app.get("/api/scenario/sensitivity")
async def scenario_sensitivity(strategy: str = "COMPREHENSIVE") -> dict:
"""SC-06 敏感性分析 Tornado(P1 沙盒,只读主干)。"""
from server.aps_domain.sensitivity import run_sensitivity
return run_sensitivity(get_store().data, strategy=strategy or "COMPREHENSIVE")
@app.post("/api/params/stage")
async def params_stage(req: ParamsStageRequest) -> dict:
"""排产参数更新暂存确认卡(P2 → /api/actions/confirm)。"""
from server.aps_domain.params import confirmation_for_params_update
store = get_store()
try:
title, lines = confirmation_for_params_update(store.data, req.payload)
except ValueError as exc:
return {"error": str(exc)}
block = harness.stage_confirmation(
req.sessionId or "web", "params.update", req.payload,
title=title, summary_lines=lines)
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action="params.update.stage",
target={"type": "SCHEDULE_PARAMS", "id": "scheduleParams"},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
@app.get("/api/constraints")
async def constraints_profile() -> dict:
"""约束配置剖面(SC-04,P0)。"""
from server.aps_domain.constraints import get_constraint_profile
return get_constraint_profile(get_store().data)
@app.post("/api/constraints/stage")
async def constraints_stage(req: ConstraintStageRequest) -> dict:
"""约束配置更新暂存确认卡(P2 → /api/actions/confirm)。"""
from server.aps_domain.constraints import confirmation_for_profile_save
store = get_store()
try:
title, lines = confirmation_for_profile_save(store.data, req.payload)
except ValueError as exc:
return {"error": str(exc)}
block = harness.stage_confirmation(
req.sessionId or "web", "constraint.profile.save", req.payload,
title=title, summary_lines=lines)
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=req.sessionId or "web", category="GATE",
action="constraint.profile.save.stage",
target={"type": "CONSTRAINT_PROFILE", "id": "default"},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump()}
@app.get("/api/sop/compilable")
async def sop_compilable() -> dict:
"""可编译 SOP 列表(IND-02,P0)。"""
from server.aps_domain.sop_rules import list_compilable_sops
from server.knowledge import get_knowledge
items = list_compilable_sops(get_knowledge().assets)
return {"items": items, "rulePacks": get_store().data.get("rulePacks") or []}
@app.get("/api/sop/compile")
async def sop_compile(query: str = "换线") -> dict:
"""SOP 编译预览(IND-02,P0)。"""
from server.aps_domain.sop_rules import compile_sop_by_query
from server.knowledge import get_knowledge
return compile_sop_by_query(get_knowledge().assets, query)
@app.post("/api/sop/stage")
async def sop_stage(req: SopStageRequest) -> dict:
"""SOP 规则包应用暂存确认卡(P2)。"""
from server.aps_domain.sop_rules import compile_sop_by_query, confirmation_for_sop_apply
from server.knowledge import get_knowledge
store = get_store()
session_id = req.sessionId or "web"
pack = req.pack
if pack is None:
compiled = compile_sop_by_query(get_knowledge().assets, req.query)
if not compiled.get("ok"):
return {"error": compiled.get("error") or "编译失败"}
pack = compiled["pack"]
title, lines = confirmation_for_sop_apply(pack)
block = harness.stage_confirmation(
session_id, "sop.apply", {"pack": pack, "query": req.query},
title=title, summary_lines=lines)
from server.agent_core.audit import write_audit
write_audit(store.data, store.next_id, actor=session_id, category="GATE",
action="sop.apply.stage",
target={"type": "RULE_PACK", "id": pack.get("packId")},
power="P2", rationale={"confirmId": block.props["confirmId"]})
store.save()
return {"message": f"{title} 已进入 P2 确认队列。", "block": block.model_dump(), "pack": pack}
# ---------------- 时间线导轨(§4.4 中观层数据源) ----------------
@app.get("/api/timeline")
async def timeline() -> dict:
"""检查点 + 排产版本的时间线(前端导轨渲染;P0 只读)。"""
world = get_store().data # 世界状态
versions = [{ # 版本投影(轻量字段)
"id": v["id"], "versionNo": v["versionNo"], "status": v["status"],
"createdAt": v["createdAt"], "conflictCount": v["conflictCount"],
"engineType": v["engineType"],
} for v in world["scheduleVersions"]]
return {"checkpoints": get_checkpoints().list_meta(), "versions": versions} # 两类锚点
# ---------------- AG-08 项目 / 会话工作区 ----------------
@app.get("/api/workspace")
async def workspace_get() -> dict:
"""工作区快照(P0):项目/会话/文件/消息 + worldKey 绑定。"""
from server.state.projects import get_project_store
return get_project_store().snapshot(include_messages=True)
@app.put("/api/workspace")
async def workspace_put(req: WorkspaceReplaceRequest) -> dict:
"""整包同步工作区(P1,本地迁移/防抖保存);不改世界。"""
from server.state.projects import get_project_store
store = get_project_store()
payload = {
"projects": req.projects, "sessions": req.sessions, "files": req.files,
"messages": req.messages, "activeProjectId": req.activeProjectId,
"activeSessionId": req.activeSessionId, "worldKey": req.worldKey or "default",
}
try:
snap = harness.guard("workspace.replace", {"activeSessionId": req.activeSessionId},
lambda: store.replace_workspace(payload))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": "工作区已同步", "workspace": snap}
@app.post("/api/projects")
async def project_create(req: ProjectCreateRequest) -> dict:
"""新建项目(P1)。"""
from server.state.projects import get_project_store
ps = get_project_store()
try:
result = harness.guard("project.create", {"name": req.name},
lambda: ps.create_project(req.name, scope_label=req.scopeLabel or "未设定作用域"))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": f"已创建项目 {result['project']['name']}", **result,
"workspace": ps.snapshot(include_messages=False)}
@app.delete("/api/projects/{project_id}")
async def project_delete(project_id: str) -> dict:
"""删除项目元数据(P1);明确不删除世界。"""
from server.state.projects import get_project_store
ps = get_project_store()
world = get_store()
order_n = len(world.data.get("salesOrders") or [])
try:
result = harness.guard("project.delete", {"projectId": project_id},
lambda: ps.delete_project(project_id))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
result["worldOrdersUnchanged"] = len(world.data.get("salesOrders") or []) == order_n
return {"message": f"已删除项目 {project_id}(世界未动)", **result,
"workspace": ps.snapshot(include_messages=False)}
@app.post("/api/sessions")
async def session_create(req: SessionCreateRequest) -> dict:
"""新建会话(P1)。"""
from server.state.projects import get_project_store
ps = get_project_store()
try:
result = harness.guard(
"session.create", {"projectId": req.projectId, "title": req.title},
lambda: ps.create_session(req.projectId, req.title or "新话题"))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": "已新建话题", **result, "workspace": ps.snapshot(include_messages=False)}
@app.get("/api/sessions/{session_id}/messages")
async def session_messages_get(session_id: str) -> dict:
"""读取会话消息(P0)。"""
from server.state.projects import get_project_store
return {"sessionId": session_id, "messages": get_project_store().get_messages(session_id)}
@app.put("/api/sessions/{session_id}/messages")
async def session_messages_put(session_id: str, req: MessagesReplaceRequest) -> dict:
"""整包替换会话消息(P1)。"""
from server.state.projects import get_project_store
ps = get_project_store()
try:
result = harness.guard(
"message.replace", {"sessionId": session_id, "count": len(req.messages)},
lambda: ps.replace_messages(session_id, req.messages))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": f"已同步 {result['count']} 条消息", **result}
@app.post("/api/sessions/{session_id}/messages")
async def session_messages_append(session_id: str, req: MessageAppendRequest) -> dict:
"""追加一条消息(P1)。"""
from server.state.projects import get_project_store
ps = get_project_store()
try:
result = harness.guard(
"message.append", {"sessionId": session_id},
lambda: ps.append_message(session_id, req.message))
except (ValueError, PermissionError) as exc:
return {"error": str(exc)}
return {"message": "消息已追加", **result}
# ---------------- 采用沙盒方案(P1:正式排产) ----------------
@app.post("/api/actions/scenario/apply")
async def scenario_apply(req: ScenarioApplyRequest) -> dict:
"""把沙盒对比中选中的策略落为正式排产(引擎确定性保证与卡片一致)。"""
store = get_store() # 世界状态
intent = IntentResult(intent="schedule.run", # 构造等价的排产意图(复用同一执行路径)
params={"strategy": req.strategy, "engine": req.engine},
confidence=1.0, source="RULE_FAST")
reply = await handle_intent(store, req.sessionId or "web", intent) # 走标准工作流(含审计落盘)
# 偏好信号:采用比试排权重更高(明确的方案选择 §8.3)
from server.knowledge import get_preferences # 局部导入(避免循环)
get_preferences().record(req.strategy, source="scenario.apply", actor=req.sessionId or "web")
return {"message": reply.text, "refresh": True} # 前端刷新世界与时间线
# ---------------- 确认卡回传(P2 唯一执行通道) ----------------
@app.post("/api/actions/confirm")
async def confirm(req: ConfirmRequest) -> dict:
"""批准/驳回一条 P2 动作(门禁放行后的执行入口,§3.3)。"""
store = get_store() # 世界状态
message = execute_confirmed(store, req.confirmId, req.approve, # 执行(内部写审计+落盘)
actor=req.sessionId or "planner")
return {"message": message, "refresh": req.approve} # 批准后前端应刷新世界数据
# ---------------- 治理可视化(§6.10 门禁管理台 / 重生中心 数据源) ----------------
@app.get("/api/gov/pending")
async def gov_pending() -> dict:
"""门禁管理台·待审批队列(聊天确认卡与此为同一数据的两个视图)。"""
return {"pending": harness.list_pending()} # 待确认动作投影
@app.get("/api/gov/audit")
async def gov_audit(limit: int = 50) -> dict:
"""门禁管理台·审计链(最近 N 条 + 全链完整性校验结果,§3.6)。"""
events = get_store().data.get("auditEvents", []) # 全部审计事件
return {"events": events[-limit:], # 最近 N 条(新在后)
"chain": verify_audit_chain(events)} # 哈希链完整性校验
@app.get("/api/gov/modules")
async def gov_modules() -> dict:
"""重生中心·可重生模块注册表(扫描源码 moduleId 声明,§9.4/§6.10.3)。"""
return {"modules": scan_modules()} # 注册表清单
@app.get("/api/gov/policy")
async def gov_policy() -> dict:
"""门禁管理台·权力矩阵(_POWER_MAP 只读投影;P0)。"""
return {"policy": harness.list_policy(), # 矩阵条目
"defaultPower": "P3"} # 未登记动作的默认等级(白名单原则)
@app.get("/api/gov/tests")
async def gov_tests(run: bool = False) -> dict:
"""重生中心·黄金测试看板(P0 只读缓存;?run=true 在线程池重跑,不阻塞事件循环)。"""
import asyncio # 线程池调度
from server.gateway.golden import golden_status # 局部导入(避免启动即依赖 pytest)
# 跑测是同步子进程(~10s):必须扔线程池,否则会饿死其他请求(世界视图全超时)
return await asyncio.to_thread(golden_status, run)
# ---------------- 知识库与报告(M3 §8/§9.10,P0 只读) ----------------
@app.get("/api/knowledge/assets")
async def knowledge_assets() -> dict:
"""知识资产元信息清单(知识面板/命令面板数据源)。"""
from server.knowledge import get_knowledge # 局部导入
return {"assets": get_knowledge().list_meta()} # 元信息(不含正文)
@app.get("/api/knowledge/assets/{asset_id}")
async def knowledge_asset(asset_id: str) -> dict:
"""单个知识资产全文(出处点开查看)。"""
from server.knowledge import get_knowledge # 局部导入
asset = get_knowledge().get(asset_id) # 按 ID 取
return asset or {"error": "资产不存在"} # 缺失给错误体(前端兜底)
@app.get("/api/reports/files/{report_id}")
async def report_file_download(report_id: str) -> Any:
"""下载已生成的排产工作计划 Excel。"""
import glob
import os
from urllib.parse import quote
from fastapi.responses import FileResponse
root = os.path.join(os.path.dirname(__file__), "..", "data", "exports")
matches = glob.glob(os.path.join(root, f"{report_id}__*"))
if not matches:
return {"error": "报告文件不存在或已清理,请重新说「生成排产方案报告」。"}
path = matches[0]
fname = os.path.basename(path).split("__", 1)[-1]
return FileResponse(
path,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=fname,
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(fname)}"},
)
@app.get("/api/reports/{report_type}")
async def report_export(report_type: str, orderNo: str | None = None, format: str | None = None) -> Any:
"""报告导出:Markdown 或 Excel(plan 默认 xlsx)。"""
from urllib.parse import quote
from fastapi.responses import PlainTextResponse, Response
from server.aps_domain.reports import build_report, persist_report_xlsx
fmt = (format or ("xlsx" if report_type in ("plan", "schedule-plan", "flex-plan") else "md")).lower()
report = build_report(get_store().data, report_type, order_no=orderNo)
if fmt == "xlsx" and report.get("xlsxBytes"):
if report.get("reportId"):
persist_report_xlsx(report["reportId"], report["xlsxBytes"], report.get("filename"))
fname = quote((report.get("filename") or f"{report['title']}.xlsx").replace(" ", "_"))
return Response(
content=report["xlsxBytes"],
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{fname}"},
)
filename = quote(f"{report['title'].replace(' ', '_')}.md")
return PlainTextResponse(report["markdown"], media_type="text/markdown; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"})
@app.get("/api/settings/llm")
async def settings_llm() -> dict:
"""设置中心·LLM Provider 状态(P0 只读;不暴露密钥)。"""
from server.agent_core.providers import get_provider # 单例
p = get_provider() # 当前配置
return {
"provider": p.provider or None, # deepseek / kimi / None
"model": p.model or None, # 模型名
"baseUrl": p.base_url or None, # 端点(无敏感信息)
"enabled": p.enabled, # False = 降级为规则解析
"fallback": "规则快路解析器(离线可用)", # 降级去向(固定文案)
}
return app # 返回配置完成的应用