feat: 完成三角色工作台及金租对公Skills全流程演示
This commit is contained in:
@@ -103,17 +103,86 @@ class ExperienceInput(Input):
|
||||
expires_at: str
|
||||
|
||||
|
||||
class CommandInput(Input):
|
||||
project_id: str
|
||||
message: str = Field(min_length=1, max_length=2000)
|
||||
|
||||
|
||||
class ReviewDecisionInput(Input):
|
||||
finding_id: str | None = None
|
||||
node_id: str | None = Field(default=None, max_length=50)
|
||||
decision: str = Field(max_length=30)
|
||||
opinion: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
@router.get("/projects")
|
||||
def projects(request: Request):
|
||||
allowed = request.state.identity.get("projects", [])
|
||||
items = [p for p in service().list_projects() if ("*" in allowed or p["id"] in allowed) and (flags()["demo"] or p["data_domain"] != "demo")]
|
||||
if request.state.identity["role"] == "reviewer":
|
||||
reviewer_id = request.state.identity["id"]
|
||||
items = [item for item in items if item.get("submitted_at") and item.get("workflow_state") not in ("review_approved", "returned")
|
||||
and item.get("reviewer_id") in (None, reviewer_id)]
|
||||
for item in items:
|
||||
latest = item.get("latest_run") or {}
|
||||
if latest.get("run_kind") == "independent_review" and latest.get("status") in ("queued", "running"):
|
||||
item["next_action"] = "查看独立评审进度"
|
||||
elif item.get("reviewer_id") == reviewer_id:
|
||||
item["next_action"] = "继续办理"
|
||||
else:
|
||||
item["next_action"] = "领取并开始评审"
|
||||
items.sort(key=lambda item: item.get("review_wait_hours") or 0, reverse=True)
|
||||
return {"projects": items, "identity": request.state.identity}
|
||||
|
||||
|
||||
@router.get("/review-pool")
|
||||
def review_pool(request: Request):
|
||||
role(request, ("manager", "reviewer"))
|
||||
if request.state.identity["role"] not in ("manager", "reviewer"):
|
||||
return {"projects": [], "identity": request.state.identity}
|
||||
allowed = request.state.identity.get("projects", [])
|
||||
items = [item for item in service().review_pool(request.state.identity["id"], request.state.identity["role"])
|
||||
if "*" in allowed or item["project_id"] in allowed]
|
||||
return {"projects": items, "identity": request.state.identity}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}")
|
||||
def project(project_id: str, request: Request):
|
||||
guard(request, project_id)
|
||||
return translate(lambda: service().get_project(project_id))
|
||||
result = translate(lambda: service().get_project(project_id))
|
||||
current_role = request.state.identity["role"]
|
||||
result["plans"] = [p for p in result["plans"] if current_role == "admin" or not p.get("audience") or current_role in p["audience"]]
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/review-claim", status_code=201)
|
||||
def review_claim(project_id: str, request: Request):
|
||||
guard(request, project_id)
|
||||
role(request, ("reviewer",))
|
||||
return translate(lambda: service().claim_review(project_id, request.state.identity["id"]))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/review-withdraw")
|
||||
def review_withdraw(project_id: str, request: Request):
|
||||
guard(request, project_id)
|
||||
role(request, ("manager",))
|
||||
return translate(lambda: service().withdraw_review(project_id, request.state.identity["id"]))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/review-resubmit", status_code=201)
|
||||
def review_resubmit(project_id: str, request: Request):
|
||||
guard(request, project_id)
|
||||
role(request, ("manager",))
|
||||
return translate(lambda: service().resubmit_review(project_id, request.state.identity["id"]))
|
||||
|
||||
|
||||
@router.get("/time-dashboard")
|
||||
def time_dashboard(request: Request):
|
||||
allowed = request.state.identity.get("projects", [])
|
||||
project_ids = [p["id"] for p in service().list_projects() if "*" in allowed or p["id"] in allowed]
|
||||
result = service().time_dashboard(project_ids)
|
||||
result["identity"] = request.state.identity
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/runs", status_code=202)
|
||||
@@ -189,6 +258,13 @@ def finding_action(project_id: str, finding_id: str, payload: ActionInput, reque
|
||||
return translate(lambda: service().finding_action(project_id, finding_id, request.state.identity["id"], payload.action, payload.note, payload.revision))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/review-decisions")
|
||||
def review_decision(project_id: str, payload: ReviewDecisionInput, request: Request):
|
||||
guard(request, project_id)
|
||||
role(request, ("reviewer",))
|
||||
return translate(lambda: service().review_decision(project_id, request.state.identity["id"], payload.decision, payload.opinion, payload.finding_id, payload.node_id))
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/scenarios", status_code=201)
|
||||
def scenario(project_id: str, payload: ScenarioInput, request: Request):
|
||||
guard(request, project_id)
|
||||
@@ -204,6 +280,14 @@ def adopt(project_id: str, scenario_id: str, payload: ActionInput, request: Requ
|
||||
@router.post("/projects/{project_id}/reports", status_code=201)
|
||||
def report(project_id: str, payload: ReportInput, request: Request):
|
||||
guard(request, project_id)
|
||||
current_role = request.state.identity["role"]
|
||||
allowed_templates = {
|
||||
"manager": {"template.ai-pre-review", "template.due-diligence"},
|
||||
"reviewer": {"template.independent-review", "template.committee-brief", "template.review", "template.committee"},
|
||||
"admin": {"template.ai-pre-review", "template.independent-review", "template.committee-brief", "template.due-diligence", "template.review", "template.committee"},
|
||||
}
|
||||
if payload.template_id not in allowed_templates[current_role] and not request.state.identity.get("preview"):
|
||||
raise HTTPException(403, "当前身份不能使用该报告模板")
|
||||
key = request.headers.get("idempotency-key", "")
|
||||
if len(key) > 160:
|
||||
raise HTTPException(422, "幂等键过长")
|
||||
@@ -215,9 +299,48 @@ def report_action(project_id: str, report_id: str, payload: ActionInput, request
|
||||
guard(request, project_id)
|
||||
if payload.action == "finalize":
|
||||
role(request, ("reviewer",))
|
||||
if payload.action == "submit":
|
||||
role(request, ("manager",))
|
||||
return translate(lambda: service().report_action(project_id, report_id, request.state.identity["id"], payload.action, payload.payload, payload.revision))
|
||||
|
||||
|
||||
@router.post("/commands")
|
||||
def business_command(payload: CommandInput, request: Request):
|
||||
"""Small, auditable command surface; every mutation uses the same project service as buttons."""
|
||||
guard(request, payload.project_id)
|
||||
svc = service()
|
||||
project_body = translate(lambda: svc.get_project(payload.project_id))
|
||||
message = payload.message.strip()
|
||||
current_role = request.state.identity["role"]
|
||||
key = request.headers.get("idempotency-key", "")
|
||||
if any(word in message for word in ("预审", "评审", "检查", "复核")) and not any(word in message for word in ("查看", "还缺", "多少", "状态", "生成", "报告", "提交")):
|
||||
plan_id = "corporate.independent-review" if current_role == "reviewer" else "corporate.pre-review"
|
||||
run = translate(lambda: svc.enqueue(payload.project_id, request.state.identity, message, plan_id, key, project_body["revision"]))
|
||||
return {"intent": "start_run", "message": ("独立评审" if current_role == "reviewer" else "AI预审") + "已进入队列。", "object_id": run["id"], "url": f"/xingbang-workbench?project={payload.project_id}&run={run['id']}"}
|
||||
if "生成" in message and "报告" in message:
|
||||
run_kind = "independent_review" if current_role == "reviewer" else "pre_review"
|
||||
source = next((r for r in project_body["runs"] if r.get("run_kind", "pre_review") == run_kind and r["status"] in ("completed", "partial")), None)
|
||||
if not source:
|
||||
raise HTTPException(409, "请先完成对应的mini-openclaw运行")
|
||||
template_id = "template.independent-review" if current_role == "reviewer" else "template.ai-pre-review"
|
||||
report_body = translate(lambda: svc.create_report(payload.project_id, source["id"], request.state.identity["id"], template_id, key))
|
||||
return {"intent": "create_report", "message": "报告草稿已生成,来源版本已冻结。", "object_id": report_body["id"], "url": f"/xingbang-workbench?project={payload.project_id}&report={report_body['id']}"}
|
||||
if "提交" in message and "评审" in message:
|
||||
role(request, ("manager",))
|
||||
report_body = next((r for r in project_body["reports"] if r["template"].get("id") in ("template.ai-pre-review", "template.due-diligence") and r["status"] != "stale"), None)
|
||||
if not report_body:
|
||||
raise HTTPException(409, "请先生成当前数据版本的AI预审报告")
|
||||
submitted = translate(lambda: svc.report_action(payload.project_id, report_body["id"], request.state.identity["id"], "submit", {}, report_body["revision"]))
|
||||
return {"intent": "submit_review", "message": "AI预审报告已提交评审,历史版本保持可查看。", "object_id": submitted["id"], "url": f"/review-pool?submitted={payload.project_id}"}
|
||||
unresolved = [f for f in project_body["findings"] if f["status"] not in ("已解决", "不适用")]
|
||||
latest = project_body["runs"][0] if project_body["runs"] else None
|
||||
message_text = f"{project_body['customer']}当前有{len(unresolved)}项待处理风险、{len(project_body['pending_candidates'])}项材料候选待确认。"
|
||||
if latest:
|
||||
message_text += f"最近一次{('独立评审' if latest.get('run_kind') == 'independent_review' else 'AI预审')}状态为{latest['status']}。"
|
||||
message_text += "下一步:" + (f"处理“{unresolved[0]['title']}”" if unresolved else "生成报告或提交下一环节")
|
||||
return {"intent": "project_status", "message": message_text, "items": [{"id": f["id"], "title": f["title"], "status": f["status"]} for f in unresolved[:5]], "url": f"/xingbang-workbench?project={payload.project_id}"}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/reports/{report_id}/export")
|
||||
def report_export(project_id: str, report_id: str, request: Request):
|
||||
guard(request, project_id)
|
||||
|
||||
Reference in New Issue
Block a user