按 docs/v2/05-implementation-plan.md §1 候选迁移表与 claude_use/T00 白名单, 从 55e2cdb 抽取 224 个文件(2.48 MB)到独立目录 F:/mini-OpenClaw-v2。 - 排除:凭据(.env/server-deploy.conf/旧DELIVERY.md)、运行数据(xingbang_data 等 4 个 SQLite)、依赖(venv/node_modules 符号链接)、归档(_backup*/test-evidence/ ori_file)、Office 临时文件 - 目标目录独立 git 仓库,初始恢复点;Gitea 远程与分支待 OQ-06 Q2 定案 - 未部署、未启动服务、未调用模型 Co-Authored-By: Claude Code <noreply@anthropic.com>
1172 lines
40 KiB
Python
1172 lines
40 KiB
Python
"""评审提效工作台 API
|
||
|
||
帮助兴邦金租的项目评审流程提效:
|
||
- 评审周期从16-17个工作日压缩
|
||
- 主审员负荷优化(每人3-7个项目排队)
|
||
- 看资料/算测算/写报告三大块耗时降低
|
||
"""
|
||
|
||
import json
|
||
from typing import Any, AsyncGenerator, Optional, List, Dict
|
||
|
||
from fastapi import APIRouter, Query
|
||
from fastapi.responses import StreamingResponse
|
||
from pydantic import BaseModel, Field
|
||
|
||
# 导入历史数据模块
|
||
from kaiju_history_data import (
|
||
get_all_projects,
|
||
get_projects_filtered,
|
||
get_reviewer_stats,
|
||
)
|
||
|
||
router = APIRouter(prefix="/api/review-assist", tags=["评审提效工作台"])
|
||
|
||
|
||
# ============ Pydantic 模型 ============
|
||
|
||
class ReviewerInfo(BaseModel):
|
||
"""主审员信息"""
|
||
name: str
|
||
current_projects: int # 在手项目数
|
||
specialty: str # 专长领域
|
||
pipeline: Optional[List[int]] = None # 在手项目复杂度列表(可选,用于精准负荷估算)
|
||
estimated_finish_days: Optional[int] = None # 预计完成天数(可选)
|
||
|
||
|
||
class LoadBalanceRequest(BaseModel):
|
||
"""评审负荷与分配建议请求"""
|
||
reviewers: list[ReviewerInfo]
|
||
new_project_industry: str = "" # 新项目行业
|
||
new_project_amount: float = 0 # 新项目金额(万元)
|
||
|
||
|
||
class DueDiligenceRequest(BaseModel):
|
||
"""尽调资料预处理请求"""
|
||
project_name: str
|
||
client_name: str
|
||
industry: str
|
||
financial_data: dict # 财务数据
|
||
credit_report: dict # 征信报告
|
||
guarantee_info: dict # 担保信息
|
||
external_risks: dict # 外部风险数据
|
||
|
||
|
||
class RentCalculationRequest(BaseModel):
|
||
"""租金测算辅助校验请求"""
|
||
credit_amount: float # 授信金额(万元)
|
||
lease_term: int # 租赁期限(月)
|
||
interest_rate: float # 年利率(%)
|
||
monthly_cashflow: float # 月现金流(万元)
|
||
project_type: str = "对公" # 对公/零售
|
||
|
||
|
||
class ReportDraftRequest(BaseModel):
|
||
"""评审报告初稿生成请求"""
|
||
intelligence_card: str # 情报卡内容
|
||
calculation_result: str # 测算结果
|
||
project_name: str
|
||
client_name: str
|
||
|
||
|
||
class PreScreeningRequest(BaseModel):
|
||
"""立项前置风险筛查请求"""
|
||
project_name: str
|
||
client_name: str
|
||
industry: str
|
||
document_checklist: list[str] # 立项资料清单
|
||
financial_data: dict
|
||
similar_cases: list[dict] = [] # 历史相似案例(可选)
|
||
|
||
|
||
class AllocateRequest(BaseModel):
|
||
"""智能分配请求(数据驱动)"""
|
||
reviewers: List[ReviewerInfo]
|
||
new_project_industry: str
|
||
new_project_amount: float # 万元
|
||
new_project_complexity: int = Field(3, ge=1, le=5) # 1-5,默认3
|
||
|
||
|
||
class RecommendationItem(BaseModel):
|
||
"""单个推荐结果"""
|
||
reviewer: str
|
||
score: float # 综合评分
|
||
reasons: List[str] # 推荐理由
|
||
est_complete_days: int # 预计完成天数(从现在算起)
|
||
industry_match_score: float # 行业匹配分
|
||
load_score: float # 负荷分
|
||
quality_score: float # 质量分
|
||
|
||
|
||
class AllocateResponse(BaseModel):
|
||
"""智能分配响应"""
|
||
reviewers: List[Dict] # 主审员当前状态列表
|
||
recommendation: List[RecommendationItem] # Top-N推荐
|
||
balance_before: float # 分配前负荷方差
|
||
balance_after: float # 分配后负荷方差(假设分配给Top1)
|
||
algorithm_version: str # 算法版本标识
|
||
|
||
|
||
# ============ 示例数据(供前端使用)============
|
||
|
||
SAMPLE_DATA = {
|
||
"load_balance": {
|
||
"reviewers": [
|
||
{"name": "李明", "current_projects": 5, "specialty": "制造业"},
|
||
{"name": "王芳", "current_projects": 3, "specialty": "新能源"},
|
||
{"name": "张强", "current_projects": 7, "specialty": "交通运输"},
|
||
{"name": "刘洋", "current_projects": 4, "specialty": "医疗健康"},
|
||
],
|
||
"new_project_industry": "光伏设备制造",
|
||
"new_project_amount": 5000
|
||
},
|
||
"due_diligence": {
|
||
"project_name": "阳光光伏设备融资租赁项目",
|
||
"client_name": "阳光光伏设备有限公司",
|
||
"industry": "光伏设备制造",
|
||
"financial_data": {
|
||
"registered_capital": 8000,
|
||
"established_years": 6,
|
||
"asset_liability_ratio": 0.58,
|
||
"current_ratio": 1.45,
|
||
"net_profit_2024": 1200,
|
||
"net_profit_2025": 1450,
|
||
"revenue_growth_rate": 0.18
|
||
},
|
||
"credit_report": {
|
||
"overdue_count": 0,
|
||
"query_count_recent_6m": 4,
|
||
"credit_limit": 20000,
|
||
"enforcement_records": 0
|
||
},
|
||
"guarantee_info": {
|
||
"type": "法人连带担保+设备抵押",
|
||
"guarantor": "阳光集团有限公司",
|
||
"guarantor_net_assets": 50000
|
||
},
|
||
"external_risks": {
|
||
"litigation_records": [],
|
||
"negative_news": 0,
|
||
"industry_policy": "国家支持光伏产业发展"
|
||
}
|
||
},
|
||
"rent_calculation": {
|
||
"credit_amount": 5000,
|
||
"lease_term": 36,
|
||
"interest_rate": 5.8,
|
||
"monthly_cashflow": 85,
|
||
"project_type": "对公"
|
||
},
|
||
"report_draft": {
|
||
"intelligence_card": "# 客户情报卡\n\n## 基本信息\n- 客户名称:阳光光伏设备有限公司\n- 行业:光伏设备制造\n\n## 财务状况\n- 资产负债率:58%(良好)\n- 流动比率:1.45(符合标准)\n\n## 风险评估\n🟢 低风险:财务指标优良,行业前景好",
|
||
"calculation_result": "# 租金测算结果\n\n- 月租金:152.8万元\n- 覆盖率:180%(远超120%标准)\n- 风险等级:低",
|
||
"project_name": "阳光光伏设备融资租赁项目",
|
||
"client_name": "阳光光伏设备有限公司"
|
||
},
|
||
"pre_screening": {
|
||
"project_name": "阳光光伏设备融资租赁项目",
|
||
"client_name": "阳光光伏设备有限公司",
|
||
"industry": "光伏设备制造",
|
||
"document_checklist": [
|
||
"营业执照",
|
||
"财务报表(近3年)",
|
||
"征信报告",
|
||
"租赁物清单",
|
||
"担保函",
|
||
"法人身份证",
|
||
"银行流水(近6个月)"
|
||
],
|
||
"financial_data": {
|
||
"registered_capital": 8000,
|
||
"asset_liability_ratio": 0.58,
|
||
"net_profit_2025": 1450
|
||
},
|
||
"similar_cases": [
|
||
{
|
||
"project_name": "某光伏企业融资租赁(2025年)",
|
||
"rejection_reason": "担保方资产不足,要求补充第二担保人",
|
||
"lesson": "光伏行业波动大,需要双重担保"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
|
||
# ============ 辅助1:评审负荷与分配建议 ============
|
||
|
||
async def _generate_load_balance(request: LoadBalanceRequest) -> AsyncGenerator[str, None]:
|
||
"""生成评审负荷与分配建议"""
|
||
|
||
# 计算平均负荷
|
||
total_projects = sum(r.current_projects for r in request.reviewers)
|
||
avg_load = total_projects / len(request.reviewers) if request.reviewers else 0
|
||
|
||
# 排序:按当前项目数升序
|
||
sorted_reviewers = sorted(request.reviewers, key=lambda r: r.current_projects)
|
||
|
||
# 生成 Markdown 内容
|
||
content = f"""# 评审负荷与分配建议
|
||
|
||
## 📊 当前负荷看板
|
||
|
||
| 主审员 | 在手项目数 | 专长领域 | 负荷状态 |
|
||
|--------|-----------|---------|---------|
|
||
"""
|
||
|
||
for r in request.reviewers:
|
||
load_status = "🔴 超载" if r.current_projects >= 6 else ("🟡 偏高" if r.current_projects >= 4 else "🟢 正常")
|
||
content += f"| {r.name} | {r.current_projects} | {r.specialty} | {load_status} |\n"
|
||
|
||
content += f"\n**平均负荷**:{avg_load:.1f} 个项目/人\n\n"
|
||
|
||
yield content
|
||
|
||
# 新项目推荐
|
||
if request.new_project_industry and request.new_project_amount:
|
||
content = f"""## 🎯 新项目分配推荐
|
||
|
||
**项目信息**:{request.new_project_industry} | 授信 {request.new_project_amount} 万元
|
||
|
||
### Top 3 推荐主审员
|
||
|
||
"""
|
||
# 推荐逻辑:专长匹配 > 负荷最低
|
||
recommendations = []
|
||
for r in sorted_reviewers:
|
||
match_score = 10 if request.new_project_industry in r.specialty or r.specialty in request.new_project_industry else 0
|
||
load_score = (10 - r.current_projects) # 项目越少分数越高
|
||
total_score = match_score + load_score
|
||
recommendations.append((r, match_score, load_score, total_score))
|
||
|
||
recommendations.sort(key=lambda x: x[3], reverse=True)
|
||
|
||
for i, (r, match, load, total) in enumerate(recommendations[:3], 1):
|
||
reason = []
|
||
if match > 0:
|
||
reason.append(f"专长匹配({r.specialty})")
|
||
if r.current_projects < avg_load:
|
||
reason.append("负荷较低")
|
||
reason_str = "、".join(reason) if reason else "负荷可承接"
|
||
|
||
content += f"**{i}. {r.name}** — {reason_str}\n"
|
||
content += f" - 当前负荷:{r.current_projects} 个项目\n"
|
||
content += f" - 专长:{r.specialty}\n"
|
||
content += f" - 综合评分:{total} 分\n\n"
|
||
|
||
yield content
|
||
|
||
# 瓶颈预警
|
||
bottleneck_reviewers = [r for r in request.reviewers if r.current_projects >= 6]
|
||
if bottleneck_reviewers:
|
||
content = f"""## ⚠️ 瓶颈预警
|
||
|
||
以下主审员负荷已达瓶颈(≥6个项目),建议暂停新项目分配:
|
||
|
||
"""
|
||
for r in bottleneck_reviewers:
|
||
content += f"- **{r.name}**:{r.current_projects} 个项目(专长:{r.specialty})\n"
|
||
|
||
content += f"\n**建议措施**:\n"
|
||
content += f"1. 优先分配给负荷较低的主审员({sorted_reviewers[0].name}、{sorted_reviewers[1].name if len(sorted_reviewers) > 1 else ''})\n"
|
||
content += f"2. 考虑延长评审周期或增加评审人力\n"
|
||
content += f"3. 对简单项目启用快速评审通道\n"
|
||
|
||
yield content
|
||
else:
|
||
content = """## ✅ 无瓶颈预警
|
||
|
||
当前所有主审员负荷均在合理范围内,可正常分配新项目。
|
||
"""
|
||
yield content
|
||
|
||
|
||
@router.post("/load-balance/stream")
|
||
async def load_balance_stream(request: LoadBalanceRequest):
|
||
"""评审负荷与分配建议(流式返回)"""
|
||
|
||
async def event_generator():
|
||
try:
|
||
yield f"data: {json.dumps({'type': 'start', 'message': '正在分析评审负荷...'}, ensure_ascii=False)}\n\n"
|
||
|
||
async for content in _generate_load_balance(request):
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'done', 'message': '分析完成'}, ensure_ascii=False)}\n\n"
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 辅助2:尽调资料预处理·情报卡 ============
|
||
|
||
async def _generate_intelligence_card(request: DueDiligenceRequest) -> AsyncGenerator[str, None]:
|
||
"""生成客户情报卡"""
|
||
|
||
content = f"""# 📋 客户情报卡
|
||
|
||
## 基本信息
|
||
- **项目名称**:{request.project_name}
|
||
- **客户名称**:{request.client_name}
|
||
- **所属行业**:{request.industry}
|
||
|
||
---
|
||
|
||
## 💰 财务状况分析
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 财务指标分析
|
||
fd = request.financial_data
|
||
content = ""
|
||
|
||
# 注册资本
|
||
reg_capital = fd.get("registered_capital", 0)
|
||
if reg_capital >= 1000:
|
||
content += f"- **注册资本**:{reg_capital} 万元 🟢 符合标准(≥1000万)\n"
|
||
else:
|
||
content += f"- **注册资本**:{reg_capital} 万元 🔴 不足(要求≥1000万)\n"
|
||
|
||
# 资产负债率
|
||
debt_ratio = fd.get("asset_liability_ratio", 0)
|
||
if debt_ratio <= 0.70:
|
||
content += f"- **资产负债率**:{debt_ratio*100:.1f}% 🟢 良好(≤70%)\n"
|
||
else:
|
||
content += f"- **资产负债率**:{debt_ratio*100:.1f}% 🔴 偏高(标准≤70%)\n"
|
||
|
||
# 流动比率
|
||
current_ratio = fd.get("current_ratio", 0)
|
||
if current_ratio >= 1.2:
|
||
content += f"- **流动比率**:{current_ratio:.2f} 🟢 健康(≥1.2)\n"
|
||
else:
|
||
content += f"- **流动比率**:{current_ratio:.2f} 🟡 偏低(标准≥1.2)\n"
|
||
|
||
# 净利润
|
||
profit_2024 = fd.get("net_profit_2024", 0)
|
||
profit_2025 = fd.get("net_profit_2025", 0)
|
||
if profit_2024 > 0 and profit_2025 > 0:
|
||
growth = ((profit_2025 - profit_2024) / profit_2024 * 100) if profit_2024 else 0
|
||
content += f"- **净利润**:2024年 {profit_2024} 万元,2025年 {profit_2025} 万元 🟢 连续为正,增长 {growth:.1f}%\n"
|
||
else:
|
||
content += f"- **净利润**:2024年 {profit_2024} 万元,2025年 {profit_2025} 万元 🔴 存在亏损\n"
|
||
|
||
yield content
|
||
|
||
# 征信报告分析
|
||
cr = request.credit_report
|
||
content = f"""
|
||
---
|
||
|
||
## 🔍 征信记录分析
|
||
|
||
"""
|
||
|
||
overdue = cr.get("overdue_count", 0)
|
||
if overdue == 0:
|
||
content += f"- **逾期记录**:无 🟢 信用良好\n"
|
||
else:
|
||
content += f"- **逾期记录**:{overdue} 次 🔴 存在逾期风险\n"
|
||
|
||
enforcement = cr.get("enforcement_records", 0)
|
||
if enforcement == 0:
|
||
content += f"- **被执行记录**:无 🟢\n"
|
||
else:
|
||
content += f"- **被执行记录**:{enforcement} 条 🔴 严重风险\n"
|
||
|
||
query_count = cr.get("query_count_recent_6m", 0)
|
||
if query_count <= 5:
|
||
content += f"- **近6个月查询次数**:{query_count} 次 🟢 正常\n"
|
||
else:
|
||
content += f"- **近6个月查询次数**:{query_count} 次 🟡 频繁(可能存在资金压力)\n"
|
||
|
||
yield content
|
||
|
||
# 担保信息
|
||
gi = request.guarantee_info
|
||
content = f"""
|
||
---
|
||
|
||
## 🛡️ 担保信息
|
||
|
||
- **担保方式**:{gi.get('type', '未提供')}
|
||
- **担保人**:{gi.get('guarantor', '未提供')}
|
||
- **担保人净资产**:{gi.get('guarantor_net_assets', 0)} 万元
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 外部风险
|
||
er = request.external_risks
|
||
content = f"""
|
||
---
|
||
|
||
## ⚠️ 外部风险评估
|
||
|
||
- **涉诉记录**:{len(er.get('litigation_records', []))} 条
|
||
- **负面舆情**:{er.get('negative_news', 0)} 条
|
||
- **行业政策**:{er.get('industry_policy', '未知')}
|
||
|
||
---
|
||
|
||
## 📝 数据完整性检查
|
||
|
||
"""
|
||
|
||
# 检查缺失项
|
||
missing = []
|
||
if not fd:
|
||
missing.append("财务数据缺失")
|
||
if not cr:
|
||
missing.append("征信报告缺失")
|
||
if not gi or not gi.get('type'):
|
||
missing.append("担保信息不完整")
|
||
|
||
if missing:
|
||
content += "### 🔴 缺失项\n"
|
||
for item in missing:
|
||
content += f"- {item}\n"
|
||
content += "\n"
|
||
else:
|
||
content += "### ✅ 无缺失项\n\n"
|
||
|
||
# 检查矛盾点
|
||
contradictions = []
|
||
if fd.get("net_profit_2025", 0) > 0 and fd.get("asset_liability_ratio", 0) > 0.80:
|
||
contradictions.append("盈利但负债率过高,需核实资产结构")
|
||
if cr.get("overdue_count", 0) > 0 and cr.get("query_count_recent_6m", 0) > 10:
|
||
contradictions.append("征信逾期且查询频繁,存在资金链风险")
|
||
|
||
if contradictions:
|
||
content += "### 🟡 矛盾点\n"
|
||
for item in contradictions:
|
||
content += f"- {item}\n"
|
||
else:
|
||
content += "### ✅ 无明显矛盾\n"
|
||
|
||
yield content
|
||
|
||
|
||
@router.post("/due-diligence/stream")
|
||
async def due_diligence_stream(request: DueDiligenceRequest):
|
||
"""尽调资料预处理·情报卡(流式返回)"""
|
||
|
||
async def event_generator():
|
||
try:
|
||
yield f"data: {json.dumps({'type': 'start', 'message': '正在生成情报卡...'}, ensure_ascii=False)}\n\n"
|
||
|
||
async for content in _generate_intelligence_card(request):
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'done', 'message': '情报卡生成完成'}, ensure_ascii=False)}\n\n"
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 辅助3:租金测算辅助校验 ============
|
||
|
||
async def _generate_calculation_check(request: RentCalculationRequest) -> AsyncGenerator[str, None]:
|
||
"""生成租金测算校验报告"""
|
||
|
||
content = f"""# 💹 租金测算辅助校验
|
||
|
||
## 输入参数
|
||
- **授信金额**:{request.credit_amount} 万元
|
||
- **租赁期限**:{request.lease_term} 个月
|
||
- **年利率**:{request.interest_rate}%
|
||
- **月现金流**:{request.monthly_cashflow} 万元
|
||
- **项目类型**:{request.project_type}
|
||
|
||
---
|
||
|
||
## 📊 二次核算结果
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 计算月租金(等额本息)
|
||
r = request.interest_rate / 100 / 12 # 月利率
|
||
n = request.lease_term
|
||
P = request.credit_amount
|
||
|
||
# 等额本息公式:A = P * r * (1+r)^n / ((1+r)^n - 1)
|
||
monthly_rent = P * r * ((1 + r) ** n) / (((1 + r) ** n) - 1)
|
||
total_payment = monthly_rent * n
|
||
total_interest = total_payment - P
|
||
|
||
content = f"""- **月租金**:{monthly_rent:.2f} 万元
|
||
- **总还款额**:{total_payment:.2f} 万元
|
||
- **总利息**:{total_interest:.2f} 万元
|
||
- **实际年化利率**:{request.interest_rate}%
|
||
|
||
---
|
||
|
||
## ✅ 覆盖率验证
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 覆盖率 = 月现金流 / 月租金
|
||
coverage_ratio = (request.monthly_cashflow / monthly_rent * 100) if monthly_rent > 0 else 0
|
||
required_ratio = 120 if request.project_type == "对公" else 150 # 对公≥120%,零售≥150%
|
||
|
||
if coverage_ratio >= required_ratio:
|
||
content = f"""- **现金流覆盖率**:{coverage_ratio:.1f}% 🟢 **符合标准**({request.project_type}要求 ≥{required_ratio}%)
|
||
- **月现金流**:{request.monthly_cashflow} 万元
|
||
- **月租金**:{monthly_rent:.2f} 万元
|
||
- **富余现金流**:{request.monthly_cashflow - monthly_rent:.2f} 万元
|
||
|
||
**结论**:现金流覆盖充足,还款能力较强。
|
||
|
||
"""
|
||
else:
|
||
content = f"""- **现金流覆盖率**:{coverage_ratio:.1f}% 🔴 **不符合标准**({request.project_type}要求 ≥{required_ratio}%)
|
||
- **月现金流**:{request.monthly_cashflow} 万元
|
||
- **月租金**:{monthly_rent:.2f} 万元
|
||
- **缺口**:{monthly_rent - request.monthly_cashflow:.2f} 万元
|
||
|
||
**结论**:现金流覆盖不足,存在还款风险。
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 敏感性分析
|
||
content = f"""---
|
||
|
||
## 📉 敏感性分析
|
||
|
||
### 利率变动影响
|
||
|
||
| 年利率 | 月租金(万元) | 覆盖率 | 风险评估 |
|
||
|--------|---------------|--------|---------|
|
||
"""
|
||
|
||
for rate_adj in [-1.0, -0.5, 0, 0.5, 1.0]:
|
||
test_rate = (request.interest_rate + rate_adj) / 100 / 12
|
||
test_rent = P * test_rate * ((1 + test_rate) ** n) / (((1 + test_rate) ** n) - 1) if test_rate > 0 else P / n
|
||
test_coverage = (request.monthly_cashflow / test_rent * 100) if test_rent > 0 else 0
|
||
risk = "🟢 安全" if test_coverage >= required_ratio else "🔴 风险"
|
||
content += f"| {request.interest_rate + rate_adj:.1f}% | {test_rent:.2f} | {test_coverage:.1f}% | {risk} |\n"
|
||
|
||
content += "\n### 现金流变动影响\n\n| 现金流变动 | 覆盖率 | 风险评估 |\n|-----------|--------|----------|\n"
|
||
|
||
for cf_pct in [-20, -10, 0, 10, 20]:
|
||
test_cf = request.monthly_cashflow * (1 + cf_pct / 100)
|
||
test_coverage = (test_cf / monthly_rent * 100)
|
||
risk = "🟢 安全" if test_coverage >= required_ratio else "🔴 风险"
|
||
content += f"| {cf_pct:+d}% ({test_cf:.2f}万) | {test_coverage:.1f}% | {risk} |\n"
|
||
|
||
yield content
|
||
|
||
# 风险提示
|
||
content = f"""
|
||
---
|
||
|
||
## ⚠️ 风险提示
|
||
|
||
"""
|
||
|
||
if coverage_ratio < required_ratio:
|
||
content += f"1. 🔴 **高风险**:现金流覆盖率不足{required_ratio}%,还款压力大\n"
|
||
elif coverage_ratio < required_ratio * 1.2:
|
||
content += f"1. 🟡 **中风险**:现金流覆盖率接近临界值,建议增加担保措施\n"
|
||
else:
|
||
content += f"1. 🟢 **低风险**:现金流覆盖率充足\n"
|
||
|
||
if request.lease_term > 36:
|
||
content += f"2. 🟡 租赁期限较长({request.lease_term}个月),注意长期经营风险\n"
|
||
|
||
if request.interest_rate > 7.0:
|
||
content += f"3. 🟡 利率较高({request.interest_rate}%),客户成本压力大\n"
|
||
|
||
content += f"\n**评审建议**:\n"
|
||
if coverage_ratio >= required_ratio * 1.5:
|
||
content += "- ✅ 测算合理,可通过财务审核\n"
|
||
elif coverage_ratio >= required_ratio:
|
||
content += "- ✅ 基本符合要求,建议关注现金流稳定性\n"
|
||
else:
|
||
content += "- ❌ 不符合标准,建议:\n"
|
||
content += " - 降低授信金额或延长期限\n"
|
||
content += " - 要求客户补充收入证明\n"
|
||
content += " - 增加连带担保人\n"
|
||
|
||
yield content
|
||
|
||
|
||
@router.post("/rent-calculation/stream")
|
||
async def rent_calculation_stream(request: RentCalculationRequest):
|
||
"""租金测算辅助校验(流式返回)"""
|
||
|
||
async def event_generator():
|
||
try:
|
||
yield f"data: {json.dumps({'type': 'start', 'message': '正在校验测算结果...'}, ensure_ascii=False)}\n\n"
|
||
|
||
async for content in _generate_calculation_check(request):
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'done', 'message': '校验完成'}, ensure_ascii=False)}\n\n"
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 辅助4:评审报告初稿生成 ============
|
||
|
||
async def _generate_report_draft(request: ReportDraftRequest) -> AsyncGenerator[str, None]:
|
||
"""生成评审报告初稿"""
|
||
|
||
content = f"""# 📄 评审报告初稿
|
||
|
||
> ⚠️ **本报告为AI辅助生成的初稿,所有结论性意见需评审员人工复核修订后方可使用。**
|
||
|
||
---
|
||
|
||
## 一、项目基本信息
|
||
|
||
- **项目名称**:{request.project_name}
|
||
- **申请人**:{request.client_name}
|
||
- **报告生成时间**:{__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||
- **评审员**:【需填写】
|
||
|
||
---
|
||
|
||
## 二、客户基本情况
|
||
|
||
{request.intelligence_card if request.intelligence_card else '【需评审员补充客户情报卡内容】'}
|
||
|
||
---
|
||
|
||
## 三、财务测算分析
|
||
|
||
{request.calculation_result if request.calculation_result else '【需评审员补充测算结果】'}
|
||
|
||
---
|
||
|
||
## 四、风险分析与控制措施
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 从情报卡中提取风险点
|
||
content = """### 4.1 主要风险点
|
||
|
||
"""
|
||
|
||
# 简单解析情报卡中的风险标记
|
||
if "🔴" in request.intelligence_card:
|
||
content += "- 🔴 **高风险项**:【需评审员根据情报卡中🔴标记项补充具体分析】\n"
|
||
if "🟡" in request.intelligence_card:
|
||
content += "- 🟡 **中风险项**:【需评审员根据情报卡中🟡标记项补充具体分析】\n"
|
||
|
||
if "🔴" not in request.intelligence_card and "🟡" not in request.intelligence_card:
|
||
content += "- 🟢 **风险较低**:客户资质良好,财务状况健康\n"
|
||
|
||
content += """
|
||
### 4.2 风险控制措施
|
||
|
||
【需评审员补充】建议包括但不限于:
|
||
1. 担保措施(保证金、抵押、第三方担保)
|
||
2. 保险要求(险种、保额、第一受益人)
|
||
3. 监管措施(资金监管、租赁物监管)
|
||
4. 特殊条款(提前还款、违约处理)
|
||
|
||
---
|
||
|
||
## 五、评审意见
|
||
|
||
### 5.1 综合评估
|
||
|
||
【需评审员填写】基于以上分析,对项目整体风险进行综合评估。
|
||
|
||
### 5.2 评审结论
|
||
|
||
- [ ] 同意通过
|
||
- [ ] 附条件通过(条件:__________)
|
||
- [ ] 不予通过
|
||
|
||
**评审员签字**:__________
|
||
**日期**:__________
|
||
|
||
---
|
||
|
||
## 六、附件清单
|
||
|
||
1. 客户营业执照
|
||
2. 近三年财务报表
|
||
3. 征信报告
|
||
4. 担保函
|
||
5. 租赁物清单及评估报告
|
||
6. 【其他附件】
|
||
|
||
---
|
||
|
||
> 📌 **修订说明**:本报告由评审提效工作台自动生成,评审员需重点关注以下部分:
|
||
> - [ ] 客户基本情况的准确性
|
||
> - [ ] 财务测算的合理性
|
||
> - [ ] 风险控制措施的完整性
|
||
> - [ ] 评审结论的专业性
|
||
"""
|
||
yield content
|
||
|
||
|
||
@router.post("/report-draft/stream")
|
||
async def report_draft_stream(request: ReportDraftRequest):
|
||
"""评审报告初稿生成(流式返回)"""
|
||
|
||
async def event_generator():
|
||
try:
|
||
yield f"data: {json.dumps({'type': 'start', 'message': '正在生成报告初稿...'}, ensure_ascii=False)}\n\n"
|
||
|
||
async for content in _generate_report_draft(request):
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'done', 'message': '初稿生成完成'}, ensure_ascii=False)}\n\n"
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 辅助5:立项前置风险筛查 ============
|
||
|
||
async def _generate_pre_screening(request: PreScreeningRequest) -> AsyncGenerator[str, None]:
|
||
"""生成立项前置风险筛查报告"""
|
||
|
||
content = f"""# 🔍 立项前置风险筛查
|
||
|
||
> **重要**:本筛查仅识别风险点,不给"通过/不通过"结论,最终决策由评审员作出。
|
||
|
||
---
|
||
|
||
## 项目信息
|
||
- **项目名称**:{request.project_name}
|
||
- **客户名称**:{request.client_name}
|
||
- **所属行业**:{request.industry}
|
||
|
||
---
|
||
|
||
## 📋 资料完整性检查
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 标准资料清单
|
||
standard_docs = [
|
||
"营业执照",
|
||
"财务报表(近3年)",
|
||
"征信报告",
|
||
"租赁物清单",
|
||
"担保函",
|
||
"法人身份证",
|
||
"银行流水(近6个月)",
|
||
"租赁物评估报告",
|
||
"保险单"
|
||
]
|
||
|
||
missing_docs = [doc for doc in standard_docs if doc not in request.document_checklist]
|
||
|
||
content = "### ✅ 已提交资料\n\n"
|
||
for doc in request.document_checklist:
|
||
content += f"- {doc}\n"
|
||
|
||
if missing_docs:
|
||
content += "\n### 🔴 缺失资料\n\n"
|
||
for doc in missing_docs:
|
||
content += f"- {doc}\n"
|
||
content += "\n**风险提示**:资料不完整可能导致评审周期延长或项目退回。\n"
|
||
else:
|
||
content += "\n### ✅ 资料齐全\n\n所有必要资料已提交。\n"
|
||
|
||
yield content
|
||
|
||
# 风险点列表
|
||
content = f"""
|
||
---
|
||
|
||
## ⚠️ 风险点识别
|
||
|
||
"""
|
||
|
||
risk_points = []
|
||
|
||
# 财务风险
|
||
fd = request.financial_data
|
||
if fd.get("registered_capital", 0) < 1000:
|
||
risk_points.append({
|
||
"category": "资质风险",
|
||
"level": "🔴",
|
||
"description": f"注册资本不足1000万(实际:{fd.get('registered_capital', 0)}万)",
|
||
"impact": "不符合准入门槛,评审退回风险高"
|
||
})
|
||
|
||
if fd.get("asset_liability_ratio", 0) > 0.70:
|
||
risk_points.append({
|
||
"category": "财务风险",
|
||
"level": "🟡",
|
||
"description": f"资产负债率超过70%(实际:{fd.get('asset_liability_ratio', 0)*100:.1f}%)",
|
||
"impact": "偿债压力较大,可能需要增加担保"
|
||
})
|
||
|
||
if fd.get("net_profit_2025", 0) <= 0:
|
||
risk_points.append({
|
||
"category": "盈利风险",
|
||
"level": "🔴",
|
||
"description": "最近一年净利润为负或为零",
|
||
"impact": "盈利能力不足,还款能力存疑"
|
||
})
|
||
|
||
# 行业风险
|
||
high_risk_industries = ["房地产", "金融", "矿业", "钢铁", "煤炭"]
|
||
if any(ind in request.industry for ind in high_risk_industries):
|
||
risk_points.append({
|
||
"category": "行业风险",
|
||
"level": "🟡",
|
||
"description": f"{request.industry}属于政策敏感或波动较大行业",
|
||
"impact": "需重点关注行业政策变化和经营稳定性"
|
||
})
|
||
|
||
# 渲染风险点
|
||
if risk_points:
|
||
for i, rp in enumerate(risk_points, 1):
|
||
content += f"""### {rp['level']} 风险点 {i}:{rp['category']}
|
||
|
||
**描述**:{rp['description']}
|
||
|
||
**影响**:{rp['impact']}
|
||
|
||
"""
|
||
else:
|
||
content += "### 🟢 未发现明显风险点\n\n根据现有资料,客户资质和财务状况基本符合准入标准。\n"
|
||
|
||
yield content
|
||
|
||
# 历史相似案例
|
||
if request.similar_cases:
|
||
content = f"""
|
||
---
|
||
|
||
## 📚 历史相似案例参考
|
||
|
||
"""
|
||
for i, case in enumerate(request.similar_cases, 1):
|
||
content += f"""### 案例 {i}:{case.get('project_name', '未命名')}
|
||
|
||
- **退回原因**:{case.get('rejection_reason', '未说明')}
|
||
- **经验教训**:{case.get('lesson', '无')}
|
||
|
||
"""
|
||
yield content
|
||
|
||
# 筛查建议
|
||
content = f"""
|
||
---
|
||
|
||
## 💡 筛查建议
|
||
|
||
"""
|
||
|
||
if len([r for r in risk_points if r['level'] == '🔴']) > 0:
|
||
content += """**建议**:存在高风险项,建议:
|
||
1. 要求客户补充资料或说明
|
||
2. 增加风险控制措施(如提高担保比例)
|
||
3. 谨慎评估是否继续推进
|
||
|
||
"""
|
||
elif len([r for r in risk_points if r['level'] == '🟡']) > 0:
|
||
content += """**建议**:存在中风险项,建议:
|
||
1. 在评审报告中重点说明
|
||
2. 设置附加条件(如追加担保、增加保险)
|
||
3. 密切监控项目执行
|
||
|
||
"""
|
||
else:
|
||
content += """**建议**:风险可控,可正常推进评审流程。
|
||
|
||
"""
|
||
|
||
content += """
|
||
> 📌 **重要提醒**:本筛查不代表评审结论,所有风险点需评审员结合实际情况判断。
|
||
"""
|
||
|
||
yield content
|
||
|
||
|
||
@router.post("/pre-screening/stream")
|
||
async def pre_screening_stream(request: PreScreeningRequest):
|
||
"""立项前置风险筛查(流式返回)"""
|
||
|
||
async def event_generator():
|
||
try:
|
||
yield f"data: {json.dumps({'type': 'start', 'message': '正在筛查风险点...'}, ensure_ascii=False)}\n\n"
|
||
|
||
async for content in _generate_pre_screening(request):
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'done', 'message': '筛查完成'}, ensure_ascii=False)}\n\n"
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 示例数据接口 ============
|
||
|
||
@router.get("/sample-data/{data_type}")
|
||
async def get_sample_data(data_type: str):
|
||
"""获取示例数据"""
|
||
if data_type in SAMPLE_DATA:
|
||
return {"success": True, "data": SAMPLE_DATA[data_type]}
|
||
else:
|
||
return {"success": False, "message": "未找到示例数据"}
|
||
|
||
|
||
# ============ 新增1:历史评审数据查询 ============
|
||
|
||
@router.get("/history")
|
||
async def get_history(
|
||
reviewer: Optional[str] = Query(None, description="主审员姓名"),
|
||
industry: Optional[str] = Query(None, description="行业(模糊匹配)"),
|
||
):
|
||
"""查询历史评审记录(支持按主审员/行业过滤)"""
|
||
try:
|
||
projects = get_projects_filtered(reviewer=reviewer, industry=industry)
|
||
return {
|
||
"success": True,
|
||
"count": len(projects),
|
||
"data": projects
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "message": str(e)}
|
||
|
||
|
||
@router.get("/reviewer-stats")
|
||
async def get_reviewer_statistics():
|
||
"""获取所有主审员的统计画像"""
|
||
try:
|
||
stats = get_reviewer_stats()
|
||
return {
|
||
"success": True,
|
||
"data": stats
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "message": str(e)}
|
||
|
||
|
||
# ============ 新增2:智能分配算法(数据驱动)============
|
||
|
||
def _calculate_smart_allocation(request: AllocateRequest) -> AllocateResponse:
|
||
"""智能分配算法核心逻辑
|
||
|
||
算法公式:
|
||
score = w1 * 行业匹配度 + w2 * 空闲度 + w3 * 质量 + w4 * 均衡度
|
||
|
||
其中:
|
||
- 行业匹配度:基于历史数据的行业专长质量分(0-100)
|
||
- 空闲度:基于当前负荷的空闲程度(0-100,越高越空闲)
|
||
- 质量:历史平均质量分(0-100)
|
||
- 均衡度:分配后是否拉平工作负担(0-100)
|
||
|
||
权重配置:w1=0.35, w2=0.35, w3=0.20, w4=0.10
|
||
"""
|
||
|
||
# 获取历史统计画像
|
||
reviewer_stats = get_reviewer_stats()
|
||
|
||
# 权重配置
|
||
W_INDUSTRY = 0.35 # 行业匹配
|
||
W_LOAD = 0.35 # 空闲度
|
||
W_QUALITY = 0.20 # 质量
|
||
W_BALANCE = 0.10 # 均衡度
|
||
|
||
# 计算当前平均负荷(用于均衡度计算)
|
||
total_load = sum(r.current_projects for r in request.reviewers)
|
||
avg_load = total_load / len(request.reviewers) if request.reviewers else 0
|
||
|
||
# 计算分配前负荷方差
|
||
loads = [r.current_projects for r in request.reviewers]
|
||
balance_before = sum((load - avg_load) ** 2 for load in loads) / len(loads) if loads else 0
|
||
|
||
recommendations = []
|
||
|
||
for reviewer_info in request.reviewers:
|
||
reviewer_name = reviewer_info.name
|
||
|
||
# 获取该主审员的历史画像
|
||
stats = reviewer_stats.get(reviewer_name, {})
|
||
if not stats:
|
||
# 如果没有历史数据,给默认值
|
||
stats = {
|
||
"avg_actual_days": 10,
|
||
"avg_quality_score": 80,
|
||
"industry_expertise": {},
|
||
"total_projects": 0
|
||
}
|
||
|
||
# 1. 行业匹配度(0-100)
|
||
industry_expertise = stats.get("industry_expertise", {})
|
||
industry_match_score = 0.0
|
||
|
||
# 查找新项目行业在该主审员的历史记录中的表现
|
||
matched_industry = None
|
||
for hist_industry, expertise_data in industry_expertise.items():
|
||
if request.new_project_industry in hist_industry or hist_industry in request.new_project_industry:
|
||
matched_industry = hist_industry
|
||
# 行业匹配分 = 该行业历史质量分
|
||
industry_match_score = expertise_data.get("avg_quality", 0)
|
||
break
|
||
|
||
# 如果没有直接匹配,但专长字段匹配,给60分基础分
|
||
if industry_match_score == 0 and (
|
||
request.new_project_industry in reviewer_info.specialty or
|
||
reviewer_info.specialty in request.new_project_industry
|
||
):
|
||
industry_match_score = 60.0
|
||
|
||
# 如果完全不匹配,给30分保底分(通用能力)
|
||
if industry_match_score == 0:
|
||
industry_match_score = 30.0
|
||
|
||
# 2. 空闲度(0-100,越高越空闲)
|
||
# 基于当前项目数,考虑在手项目复杂度
|
||
if reviewer_info.pipeline and len(reviewer_info.pipeline) > 0:
|
||
# 如果提供了在手项目复杂度,计算折算天数
|
||
avg_days_per_complexity = stats.get("avg_actual_days", 10) / 3 # 假设复杂度3的平均天数
|
||
estimated_workload_days = sum(
|
||
complexity * avg_days_per_complexity
|
||
for complexity in reviewer_info.pipeline
|
||
)
|
||
elif reviewer_info.estimated_finish_days:
|
||
# 如果直接提供了预计完成天数
|
||
estimated_workload_days = reviewer_info.estimated_finish_days
|
||
else:
|
||
# 简单按项目数估算
|
||
avg_days = stats.get("avg_actual_days", 10)
|
||
estimated_workload_days = reviewer_info.current_projects * avg_days
|
||
|
||
# 空闲度 = 100 - (当前负荷天数 / 最大合理负荷天数) * 100
|
||
# 假设最大合理负荷 = 6个复杂度3的项目 = 6 * 10天 = 60天
|
||
max_reasonable_workload = 60
|
||
load_score = max(0, 100 - (estimated_workload_days / max_reasonable_workload) * 100)
|
||
|
||
# 3. 质量分(直接使用历史平均质量分)
|
||
quality_score = stats.get("avg_quality_score", 80)
|
||
|
||
# 4. 均衡度(分配给该主审员后,是否拉平负荷分布)
|
||
# 如果当前负荷低于平均,分配给他能提高均衡度
|
||
current_load = reviewer_info.current_projects
|
||
if current_load < avg_load:
|
||
balance_score = 100 - abs(current_load + 1 - avg_load) / avg_load * 100
|
||
else:
|
||
balance_score = max(0, 100 - abs(current_load + 1 - avg_load) / avg_load * 100)
|
||
balance_score = max(0, min(100, balance_score))
|
||
|
||
# 综合评分
|
||
total_score = (
|
||
W_INDUSTRY * industry_match_score +
|
||
W_LOAD * load_score +
|
||
W_QUALITY * quality_score +
|
||
W_BALANCE * balance_score
|
||
)
|
||
|
||
# 预计完成天数(当前负荷 + 新项目)
|
||
new_project_days = request.new_project_complexity * (stats.get("avg_actual_days", 10) / 3)
|
||
est_complete_days = int(estimated_workload_days + new_project_days)
|
||
|
||
# 生成推荐理由
|
||
reasons = []
|
||
|
||
if industry_match_score >= 85:
|
||
reasons.append(f"✨ 行业专长突出:{matched_industry or reviewer_info.specialty}历史质量{industry_match_score:.0f}分")
|
||
elif industry_match_score >= 70:
|
||
reasons.append(f"✓ 行业经验丰富:{matched_industry or reviewer_info.specialty}")
|
||
elif industry_match_score < 40:
|
||
reasons.append(f"⚠ 行业匹配度较低")
|
||
|
||
if load_score >= 70:
|
||
reasons.append(f"✓ 当前负荷较轻:预计{estimated_workload_days:.0f}天可开工")
|
||
elif load_score < 30:
|
||
reasons.append(f"⚠ 当前负荷较重:预计{estimated_workload_days:.0f}天后才能开工")
|
||
|
||
if quality_score >= 90:
|
||
reasons.append(f"⭐ 历史质量优秀:平均{quality_score:.0f}分")
|
||
elif quality_score >= 80:
|
||
reasons.append(f"✓ 历史质量良好:平均{quality_score:.0f}分")
|
||
|
||
if current_load < avg_load:
|
||
reasons.append(f"✓ 有助于负荷均衡(当前{current_load}个 < 平均{avg_load:.1f}个)")
|
||
|
||
if not reasons:
|
||
reasons.append("基础推荐")
|
||
|
||
recommendations.append(
|
||
RecommendationItem(
|
||
reviewer=reviewer_name,
|
||
score=round(total_score, 2),
|
||
reasons=reasons,
|
||
est_complete_days=est_complete_days,
|
||
industry_match_score=round(industry_match_score, 2),
|
||
load_score=round(load_score, 2),
|
||
quality_score=round(quality_score, 2),
|
||
)
|
||
)
|
||
|
||
# 按综合评分排序
|
||
recommendations.sort(key=lambda x: x.score, reverse=True)
|
||
|
||
# 计算分配后负荷方差(假设分配给Top1)
|
||
if recommendations:
|
||
top_reviewer_name = recommendations[0].reviewer
|
||
loads_after = [
|
||
r.current_projects + 1 if r.name == top_reviewer_name else r.current_projects
|
||
for r in request.reviewers
|
||
]
|
||
avg_load_after = sum(loads_after) / len(loads_after) if loads_after else 0
|
||
balance_after = sum((load - avg_load_after) ** 2 for load in loads_after) / len(loads_after) if loads_after else 0
|
||
else:
|
||
balance_after = balance_before
|
||
|
||
# 生成主审员当前状态列表
|
||
reviewers_status = []
|
||
for r in request.reviewers:
|
||
stats = reviewer_stats.get(r.name, {})
|
||
reviewers_status.append({
|
||
"name": r.name,
|
||
"current_projects": r.current_projects,
|
||
"specialty": r.specialty,
|
||
"avg_quality": stats.get("avg_quality_score", 0),
|
||
"total_history_projects": stats.get("total_projects", 0),
|
||
})
|
||
|
||
return AllocateResponse(
|
||
reviewers=reviewers_status,
|
||
recommendation=recommendations,
|
||
balance_before=round(balance_before, 2),
|
||
balance_after=round(balance_after, 2),
|
||
algorithm_version="v1.0-multi-dimensional"
|
||
)
|
||
|
||
|
||
@router.post("/allocate")
|
||
async def smart_allocate(request: AllocateRequest):
|
||
"""智能分配算法(JSON返回,数据驱动)
|
||
|
||
基于历史数据的多维度评分:
|
||
- 行业专长匹配(历史该行业质量分)
|
||
- 当前负荷空闲度(预计开工时间)
|
||
- 历史质量表现
|
||
- 负荷均衡优化
|
||
|
||
返回Top-N推荐 + 详细理由
|
||
"""
|
||
try:
|
||
result = _calculate_smart_allocation(request)
|
||
return {
|
||
"success": True,
|
||
"data": result.dict()
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "message": str(e)}
|