chore(v2): 从兴邦预审v2白名单抽取独立可运行基座
按 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>
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
"""主审员画像库
|
||||
|
||||
功能:
|
||||
1. 存储主审员基础信息(专长、效率、质量评分)
|
||||
2. 动态更新主审员负荷
|
||||
3. 多维度评分(用于智能分配)
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReviewerProfileStore:
|
||||
"""主审员画像存储"""
|
||||
|
||||
def __init__(self, data_dir: str = "./xingbang_data/reviewers"):
|
||||
self.data_dir = Path(data_dir)
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.profiles_file = self.data_dir / "reviewer_profiles.json"
|
||||
|
||||
# 初始化默认数据
|
||||
if not self.profiles_file.exists():
|
||||
self._init_default_profiles()
|
||||
|
||||
def _init_default_profiles(self):
|
||||
"""初始化默认主审员画像"""
|
||||
default_profiles = {
|
||||
"李明": {
|
||||
"name": "李明",
|
||||
"specialty": ["制造业", "新能源"],
|
||||
"current_projects": 5,
|
||||
"avg_completion_days": 8,
|
||||
"quality_score": 0.85, # 0-1,基于历史评审质量
|
||||
"efficiency_score": 0.78, # 0-1,基于完成速度
|
||||
"workload_capacity": 7, # 最大承载项目数
|
||||
"industry_experience": {
|
||||
"制造业": 45, # 历史评审项目数
|
||||
"新能源": 32,
|
||||
"光伏": 18,
|
||||
},
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
},
|
||||
"王芳": {
|
||||
"name": "王芳",
|
||||
"specialty": ["新能源", "光伏", "风电"],
|
||||
"current_projects": 3,
|
||||
"avg_completion_days": 7,
|
||||
"quality_score": 0.92,
|
||||
"efficiency_score": 0.88,
|
||||
"workload_capacity": 6,
|
||||
"industry_experience": {
|
||||
"新能源": 56,
|
||||
"光伏": 38,
|
||||
"风电": 22,
|
||||
},
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
},
|
||||
"张强": {
|
||||
"name": "张强",
|
||||
"specialty": ["交通运输", "物流", "制造业"],
|
||||
"current_projects": 7,
|
||||
"avg_completion_days": 9,
|
||||
"quality_score": 0.80,
|
||||
"efficiency_score": 0.72,
|
||||
"workload_capacity": 8,
|
||||
"industry_experience": {
|
||||
"交通运输": 67,
|
||||
"物流": 43,
|
||||
"制造业": 28,
|
||||
},
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
},
|
||||
"刘洋": {
|
||||
"name": "刘洋",
|
||||
"specialty": ["医疗健康", "服务业", "消费"],
|
||||
"current_projects": 4,
|
||||
"avg_completion_days": 8,
|
||||
"quality_score": 0.88,
|
||||
"efficiency_score": 0.82,
|
||||
"workload_capacity": 7,
|
||||
"industry_experience": {
|
||||
"医疗健康": 51,
|
||||
"服务业": 34,
|
||||
"消费": 29,
|
||||
},
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
},
|
||||
}
|
||||
|
||||
with open(self.profiles_file, "w", encoding="utf-8") as f:
|
||||
json.dump(default_profiles, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info("主审员画像初始化完成")
|
||||
|
||||
def get_all_profiles(self) -> Dict[str, Dict]:
|
||||
"""获取所有主审员画像"""
|
||||
with open(self.profiles_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def get_profile(self, reviewer_name: str) -> Optional[Dict]:
|
||||
"""获取指定主审员画像"""
|
||||
profiles = self.get_all_profiles()
|
||||
return profiles.get(reviewer_name)
|
||||
|
||||
def update_workload(self, reviewer_name: str, current_projects: int):
|
||||
"""更新主审员工作负荷"""
|
||||
profiles = self.get_all_profiles()
|
||||
|
||||
if reviewer_name in profiles:
|
||||
profiles[reviewer_name]["current_projects"] = current_projects
|
||||
profiles[reviewer_name]["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
with open(self.profiles_file, "w", encoding="utf-8") as f:
|
||||
json.dump(profiles, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"已更新 {reviewer_name} 的负荷: {current_projects} 个项目")
|
||||
|
||||
def update_quality_score(self, reviewer_name: str, quality_score: float):
|
||||
"""更新主审员质量评分"""
|
||||
profiles = self.get_all_profiles()
|
||||
|
||||
if reviewer_name in profiles:
|
||||
profiles[reviewer_name]["quality_score"] = quality_score
|
||||
profiles[reviewer_name]["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
with open(self.profiles_file, "w", encoding="utf-8") as f:
|
||||
json.dump(profiles, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"已更新 {reviewer_name} 的质量评分: {quality_score:.2f}")
|
||||
|
||||
def calculate_match_score(
|
||||
self,
|
||||
reviewer_name: str,
|
||||
project_industry: str,
|
||||
project_complexity: int = 3
|
||||
) -> Dict:
|
||||
"""计算主审员与项目的匹配度(多维度评分)
|
||||
|
||||
评分维度:
|
||||
- 35% 行业匹配度(专长+历史经验)
|
||||
- 35% 可用性(当前负荷)
|
||||
- 20% 质量评分(历史评审质量)
|
||||
- 10% 团队平衡(避免某人长期超载)
|
||||
"""
|
||||
profile = self.get_profile(reviewer_name)
|
||||
if not profile:
|
||||
return {"total_score": 0, "breakdown": {}}
|
||||
|
||||
# 1. 行业匹配度(35%)
|
||||
industry_match = 0
|
||||
if any(spec in project_industry or project_industry in spec for spec in profile["specialty"]):
|
||||
industry_match = 0.8 # 专长匹配
|
||||
|
||||
# 检查历史经验
|
||||
exp_count = 0
|
||||
for industry_key in profile["industry_experience"]:
|
||||
if industry_key in project_industry or project_industry in industry_key:
|
||||
exp_count = profile["industry_experience"][industry_key]
|
||||
break
|
||||
|
||||
if exp_count >= 30:
|
||||
industry_match = 1.0
|
||||
elif exp_count >= 10:
|
||||
industry_match = max(industry_match, 0.6)
|
||||
elif exp_count > 0:
|
||||
industry_match = max(industry_match, 0.3)
|
||||
|
||||
industry_score = industry_match * 35
|
||||
|
||||
# 2. 可用性(35%)
|
||||
current_load = profile["current_projects"]
|
||||
capacity = profile["workload_capacity"]
|
||||
|
||||
if current_load >= capacity:
|
||||
availability = 0.1 # 超载
|
||||
elif current_load >= capacity * 0.8:
|
||||
availability = 0.5 # 偏高
|
||||
else:
|
||||
availability = 1.0 - (current_load / capacity) # 线性递减
|
||||
|
||||
availability_score = availability * 35
|
||||
|
||||
# 3. 质量评分(20%)
|
||||
quality_score = profile["quality_score"] * 20
|
||||
|
||||
# 4. 团队平衡(10%)- 优先分配给负荷较低的
|
||||
all_profiles = self.get_all_profiles()
|
||||
avg_load = sum(p["current_projects"] for p in all_profiles.values()) / len(all_profiles)
|
||||
|
||||
if current_load < avg_load:
|
||||
balance_score = 10
|
||||
elif current_load == avg_load:
|
||||
balance_score = 7
|
||||
else:
|
||||
balance_score = max(0, 10 - (current_load - avg_load) * 2)
|
||||
|
||||
# 总分
|
||||
total_score = industry_score + availability_score + quality_score + balance_score
|
||||
|
||||
return {
|
||||
"total_score": round(total_score, 2),
|
||||
"breakdown": {
|
||||
"industry_match": round(industry_score, 2),
|
||||
"availability": round(availability_score, 2),
|
||||
"quality": round(quality_score, 2),
|
||||
"balance": round(balance_score, 2),
|
||||
},
|
||||
"estimated_days": profile["avg_completion_days"] + current_load // 2, # 考虑负荷影响
|
||||
}
|
||||
|
||||
def get_recommended_reviewers(
|
||||
self,
|
||||
project_industry: str,
|
||||
project_complexity: int = 3,
|
||||
top_n: int = 3
|
||||
) -> List[Dict]:
|
||||
"""获取推荐主审员列表(Top N)"""
|
||||
all_profiles = self.get_all_profiles()
|
||||
|
||||
recommendations = []
|
||||
for reviewer_name in all_profiles:
|
||||
match_result = self.calculate_match_score(
|
||||
reviewer_name,
|
||||
project_industry,
|
||||
project_complexity
|
||||
)
|
||||
|
||||
profile = all_profiles[reviewer_name]
|
||||
recommendations.append({
|
||||
"reviewer": reviewer_name,
|
||||
"score": match_result["total_score"],
|
||||
"breakdown": match_result["breakdown"],
|
||||
"estimated_days": match_result["estimated_days"],
|
||||
"current_projects": profile["current_projects"],
|
||||
"specialty": profile["specialty"],
|
||||
})
|
||||
|
||||
# 按总分降序排序
|
||||
recommendations.sort(key=lambda x: x["score"], reverse=True)
|
||||
|
||||
return recommendations[:top_n]
|
||||
|
||||
|
||||
# 全局单例
|
||||
_reviewer_store: Optional[ReviewerProfileStore] = None
|
||||
|
||||
|
||||
def get_reviewer_store() -> ReviewerProfileStore:
|
||||
"""获取主审员画像存储单例"""
|
||||
global _reviewer_store
|
||||
if _reviewer_store is None:
|
||||
_reviewer_store = ReviewerProfileStore()
|
||||
return _reviewer_store
|
||||
Reference in New Issue
Block a user