Files
mini-openclaw/backend/api/xingbang_workspace.py
T

170 lines
9.2 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.
"""Decision-record prototype: shared, persisted project state; no external model calls."""
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
router = APIRouter(prefix="/api/xingbang/workspace", tags=["兴邦方案工作台(演示)"])
DB_PATH = Path(__file__).resolve().parents[1] / "xingbang_data" / "workspace.sqlite3"
def now():
return datetime.now(timezone.utc).isoformat()
def connect():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
db = sqlite3.connect(DB_PATH)
db.execute("CREATE TABLE IF NOT EXISTS projects (id TEXT PRIMARY KEY, body TEXT NOT NULL)")
return db
def get(db, project_id):
row = db.execute("SELECT body FROM projects WHERE id=?", (project_id,)).fetchone()
if not row:
raise HTTPException(404, "项目不存在")
return json.loads(row[0])
def save(db, project):
db.execute("INSERT OR REPLACE INTO projects VALUES (?,?)", (project["id"], json.dumps(project, ensure_ascii=False)))
class ProjectInput(BaseModel):
name: str = Field(min_length=1, max_length=120)
client: str = Field(min_length=1, max_length=120)
amount: float = Field(gt=0, allow_inf_nan=False)
assets: float | None = Field(default=None, gt=0, allow_inf_nan=False)
liabilities: float | None = Field(default=None, ge=0, allow_inf_nan=False)
lease_value: float | None = Field(default=None, ge=0, allow_inf_nan=False)
insurance: float | None = Field(default=None, ge=0, allow_inf_nan=False)
class Action(BaseModel):
kind: str
target: str = ""
note: str = Field(default="", max_length=4000)
actor: str = Field(min_length=1, max_length=80)
amount: float | None = Field(default=None, gt=0, allow_inf_nan=False)
def assess(project):
snapshot = project["snapshots"][-1]
data = snapshot["data"]
items = []
rules = [
("debt", "资产负债率", ["liabilities", "assets"], "负债总额 / 资产总额", 0.7, "gt"),
("lease", "租赁物价值覆盖", ["lease_value", "amount"], "租赁物价值 / 授信金额", 1.2, "lt"),
("insurance", "保额覆盖", ["insurance", "amount"], "保额 / 授信金额", 1, "lt"),
]
for key, title, fields, formula, threshold, op in rules:
missing = [f for f in fields if data.get(f) is None]
value = None if missing else round(data[fields[0]] / data[fields[1]], 6)
triggered = value is not None and (value > threshold if op == "gt" else value < threshold)
items.append({"id": key, "title": title, "value": value,
"category": "信息缺失" if missing else "待核查疑点" if triggered else "指标未触发(非无风险)",
"status": "待复核", "note": "", "evidence": {"snapshot": snapshot["id"],
"source": snapshot["source"], "time": snapshot["time"], "fields": {f: data.get(f) for f in fields},
"formula": formula, "rule_version": "demo-1", "threshold": threshold,
"operator": op, "basis": "演示阈值,非兴邦正式制度"}})
project["risks"] = items
project["assessment_snapshot"] = snapshot["id"]
project["stale"] = False
return items
@router.get("/projects")
def projects():
with connect() as db:
return [json.loads(row[0]) for row in db.execute("SELECT body FROM projects ORDER BY rowid DESC")]
@router.post("/projects")
def create(data: ProjectInput):
project = {"id": uuid4().hex, "name": data.name, "client": data.client, "mode": "演示 / 人工录入",
"snapshots": [], "risks": [], "tasks": [], "reports": [], "cards": [], "audit": [], "stale": True}
project["snapshots"].append({"id": uuid4().hex, "time": now(), "source": "人工录入(未接入ERP)",
"template_version": "manual-1", "mapping_version": "manual-1",
"unit": "万元", "data": data.model_dump()})
with connect() as db:
save(db, project)
return project
@router.post("/projects/{project_id}/actions")
def action(project_id: str, request: Action):
with connect() as db:
db.execute("BEGIN IMMEDIATE")
p = get(db, project_id)
k = request.kind
if k == "assess":
if any(r["status"] != "待复核" for r in p["risks"]) and not p["stale"]:
raise HTTPException(409, "已有人工判断;请保留当前结果,数据变更后再重新研判")
previous = p["risks"]
assess(p)
p["comparison"] = [{"title": r["title"], "before": next((x["category"] for x in previous if x["id"] == r["id"]), "首次研判"), "after": r["category"]} for r in p["risks"]]
elif k == "change":
if request.amount is None or not request.note.strip():
raise HTTPException(422, "请填写新额度和变更原因")
data = dict(p["snapshots"][-1]["data"], amount=request.amount)
p["snapshots"].append(dict(p["snapshots"][-1], id=uuid4().hex, time=now(), data=data))
p["stale"] = True
p["impacts"] = ["租赁物价值覆盖率", "保额覆盖率", "担保与保证金条件(待人工复核)", "报告授信方案与风险章节"]
for report in p["reports"]:
report["stale"] = True
elif k in ("confirm", "reject", "task"):
if p["stale"]:
raise HTTPException(409, "请先对当前快照重新研判")
risk = next((r for r in p["risks"] if r["id"] == request.target), None)
if not risk or not request.note.strip():
raise HTTPException(422, "请选择风险项并填写判断依据或补件要求")
if k == "task":
p["tasks"].append({"id": uuid4().hex, "risk": risk["title"], "owner": request.actor,
"requirement": request.note, "status": "待提交", "snapshot": p["snapshots"][-1]["id"]})
else:
risk.update(status="人工确认" if k == "confirm" else "人工驳回", note=request.note, actor=request.actor)
elif k in ("submit_task", "close_task"):
task = next((t for t in p["tasks"] if t["id"] == request.target), None)
if not task or not request.note.strip():
raise HTTPException(422, "请提供任务及处理说明")
expected = "待提交" if k == "submit_task" else "已提交待确认"
if task["status"] != expected:
raise HTTPException(409, "任务状态不允许此操作")
task.update(status="已提交待确认" if k == "submit_task" else "已解决", note=request.note, actor=request.actor)
elif k == "report":
if p["stale"] or not p["risks"] or any(r["status"] == "待复核" for r in p["risks"]):
raise HTTPException(409, "请先完成当前快照的研判和逐项人工复核")
snapshot = p["snapshots"][-1]
body = f"# {p['name']} 评审材料草稿\n\n客户:{p['client']}\n\n授信金额:{snapshot['data']['amount']} 万元\n\n数据来源:人工录入;快照:{snapshot['id']}\n\n"
body += "\n\n".join(f"## {r['title']}\n{r['category']} / {r['status']}\n\n判断:{r['note']}\n\n证据:{json.dumps(r['evidence'], ensure_ascii=False)}" for r in p["risks"])
body += "\n\n审批结果、法律有效性、附件原文及租后表现:待补充。本文使用演示规则,不能作为正式审批依据。"
p["reports"].append({"id": uuid4().hex, "body": body, "snapshot": snapshot["id"], "stale": False, "status": "草稿"})
elif k == "finalize":
report = next((r for r in p["reports"] if r["id"] == request.target), None)
if not report or report["stale"] or report["status"] != "草稿" or not request.note.strip():
raise HTTPException(409, "仅当前版本草稿可定稿,且需填写确认意见")
report.update(status="已定稿", actor=request.actor, note=request.note)
p["cards"].append({"id": uuid4().hex, "status": "草稿", "report": report["id"], "snapshot": report["snapshot"],
"facts": json.loads(json.dumps(p["risks"])), "history": [], "outcome": ""})
elif k == "card":
card = next((c for c in p["cards"] if c["id"] == request.target), None)
if not card or not request.note.strip():
raise HTTPException(422, "请选择经验卡并填写审核依据或租后结果")
states = ["草稿", "主审员已确认", "部门审核", "可检索", "已验证", "已过期/归档"]
index = states.index(card["status"])
if index == len(states) - 1:
raise HTTPException(409, "经验卡已归档")
card["status"] = states[index + 1]
card["history"].append({"status": card["status"], "actor": request.actor, "note": request.note, "time": now()})
if card["status"] == "已验证":
card["outcome"] = request.note
else:
raise HTTPException(422, "不支持的操作")
p["audit"].append({"time": now(), **request.model_dump()})
save(db, p)
return p