# ============================================================ # 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 # 返回配置完成的应用