## 安全修复 (12项) - Webhook接口添加全局Token认证,过滤敏感请求头 - 修复JWT Base64 padding公式,防止签名验证绕过 - 数据库密码/飞书Token从源码移除,改为环境变量 - 工作流引擎添加路径遍历防护 (_resolve_safe_path) - eval()添加模板长度上限检查 - 审批API添加认证依赖 - 前端v-html增强XSS转义,console.log仅开发模式输出 - 500错误不再暴露内部异常详情 ## Agent运行时修复 (7项) - 删除_inject_knowledge_context中未定义db变量的finally块 - 工具执行添加try/except保护,异常不崩溃Agent - LLM重试计入budget计数器 - self_review异常时passed=False - max_iterations截断标记success=False - 工具参数JSON解析失败时记录警告日志 - run()开始时重置_llm_invocations计数器 ## 配置与基础设施 - DEBUG默认False,SQL_ECHO独立配置项 - init_db()补全13个缺失模型导入 - 新增WEBHOOK_AUTH_TOKEN/SQL_ECHO配置项 - 新增.env.example模板文件 ## 前端修复 (12项) - 登录改用URLSearchParams替代FormData - 401拦截器通过Pinia store统一清理状态 - SSE流超时从60s延长至300s - final/error事件时清除streamTimeout - localStorage聊天记录添加24h TTL - safeParseArgCount替代模板中裸JSON.parse - fetchUser 401时同时清除user对象 ## 新增模块 - 知识进化: knowledge_extractor/retriever/tasks - 数字孪生: shadow_executor/comparison模型 - 行为采集: behavior_middleware/collector/fingerprint_engine - 代码审查: code_review_agent/document_review_agent - 反馈学习: feedback_learner - 瓶颈检测/优化引擎/成本估算/需求估算 - 速率限制器 (rate_limiter) - Alembic迁移 015-020 ## 文档 - 商业化落地计划 - 8篇docs文档 (架构/API/部署/开发/贡献等) - Docker Compose生产配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
222 lines
8.4 KiB
Python
222 lines
8.4 KiB
Python
"""
|
|
用户行为指纹引擎 — 从行为日志中学习偏好权重和决策规则
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict, List, Optional
|
|
from collections import Counter
|
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func
|
|
|
|
from app.core.database import SessionLocal
|
|
from app.models.user_behavior import UserBehaviorLog, BehaviorCategory
|
|
from app.models.user_fingerprint import UserFingerprint
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_WEIGHTS = {
|
|
"code_review": {"security": 0.3, "performance": 0.25, "readability": 0.25, "style": 0.2},
|
|
"document": {"structure": 0.3, "clarity": 0.3, "completeness": 0.25, "style": 0.15},
|
|
"decision": {"data_driven": 0.4, "risk_averse": 0.3, "speed": 0.3},
|
|
"email": {"formality": 0.3, "conciseness": 0.35, "responsiveness": 0.35},
|
|
}
|
|
|
|
|
|
class FingerprintEngine:
|
|
"""从行为日志学习用户行为指纹"""
|
|
|
|
def compute_fingerprint(self, user_id: str) -> Optional[Dict[str, Any]]:
|
|
"""计算用户行为指纹。"""
|
|
db: Optional[Session] = None
|
|
try:
|
|
db = SessionLocal()
|
|
|
|
total = db.query(func.count(UserBehaviorLog.id)).filter(
|
|
UserBehaviorLog.user_id == user_id
|
|
).scalar() or 0
|
|
|
|
if total < 10:
|
|
logger.info("用户 %s 行为数据不足 (%d 条)", user_id, total)
|
|
return None
|
|
|
|
by_category = {}
|
|
for cat in BehaviorCategory:
|
|
count = db.query(func.count(UserBehaviorLog.id)).filter(
|
|
UserBehaviorLog.user_id == user_id,
|
|
UserBehaviorLog.category == cat.value,
|
|
).scalar() or 0
|
|
by_category[cat.value] = count
|
|
|
|
preference_weights = self._extract_preferences(db, user_id)
|
|
decision_rules = self._extract_rules(db, user_id)
|
|
|
|
behaviors = (
|
|
db.query(UserBehaviorLog)
|
|
.filter(UserBehaviorLog.user_id == user_id)
|
|
.order_by(UserBehaviorLog.created_at.desc())
|
|
.limit(100)
|
|
.all()
|
|
)
|
|
durations = []
|
|
for b in behaviors:
|
|
if b.result and isinstance(b.result, dict):
|
|
d = b.result.get("duration_ms")
|
|
if d:
|
|
durations.append(d)
|
|
avg_response = int(sum(durations) / len(durations)) if durations else None
|
|
|
|
return {
|
|
"user_id": user_id,
|
|
"preference_weights": preference_weights,
|
|
"decision_rules": decision_rules,
|
|
"total_behaviors": total,
|
|
"behaviors_by_category": by_category,
|
|
"avg_response_time_ms": avg_response,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("指纹计算失败: %s", e)
|
|
return None
|
|
finally:
|
|
if db:
|
|
try:
|
|
db.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def _extract_preferences(self, db: Session, user_id: str) -> Dict[str, Any]:
|
|
weights = dict(DEFAULT_WEIGHTS)
|
|
recent = (
|
|
db.query(UserBehaviorLog)
|
|
.filter(UserBehaviorLog.user_id == user_id)
|
|
.order_by(UserBehaviorLog.created_at.desc())
|
|
.limit(200)
|
|
.all()
|
|
)
|
|
for b in recent:
|
|
if b.result and isinstance(b.result, dict):
|
|
priority = b.result.get("priority")
|
|
if priority and isinstance(priority, dict):
|
|
cat = b.category
|
|
if cat in weights:
|
|
for k, v in priority.items():
|
|
if k in weights[cat]:
|
|
weights[cat][k] = weights[cat][k] * 0.9 + v * 0.1
|
|
return weights
|
|
|
|
def _extract_rules(self, db: Session, user_id: str) -> List[Dict[str, Any]]:
|
|
rules = []
|
|
recent = (
|
|
db.query(UserBehaviorLog)
|
|
.filter(UserBehaviorLog.user_id == user_id)
|
|
.order_by(UserBehaviorLog.created_at.desc())
|
|
.limit(300)
|
|
.all()
|
|
)
|
|
action_counts = Counter(b.action for b in recent)
|
|
for action, count in action_counts.most_common(20):
|
|
if count >= 5:
|
|
actions_of_type = [b for b in recent if b.action == action]
|
|
status_codes = Counter()
|
|
for b in actions_of_type:
|
|
if b.result and isinstance(b.result, dict):
|
|
sc = b.result.get("status_code")
|
|
if sc:
|
|
status_codes[sc] += 1
|
|
if status_codes and status_codes.most_common(1)[0][1] / count > 0.8:
|
|
rules.append({
|
|
"action": action,
|
|
"expected_outcome": status_codes.most_common(1)[0][0],
|
|
"confidence": round(status_codes.most_common(1)[0][1] / count, 2),
|
|
"sample_count": count,
|
|
})
|
|
return rules[:50]
|
|
|
|
def save_or_update(self, user_id: str, fingerprint: Dict[str, Any]) -> Optional[str]:
|
|
db: Optional[Session] = None
|
|
try:
|
|
db = SessionLocal()
|
|
existing = db.query(UserFingerprint).filter(
|
|
UserFingerprint.user_id == user_id
|
|
).first()
|
|
|
|
if existing:
|
|
existing.preference_weights = fingerprint["preference_weights"]
|
|
existing.decision_rules = fingerprint["decision_rules"]
|
|
existing.total_behaviors = fingerprint["total_behaviors"]
|
|
existing.behaviors_by_category = fingerprint["behaviors_by_category"]
|
|
existing.avg_response_time_ms = fingerprint.get("avg_response_time_ms")
|
|
existing.model_version = str(float(existing.model_version or "1.0") + 0.1)
|
|
existing.last_trained_at = func.now()
|
|
fid = str(existing.id)
|
|
else:
|
|
entry = UserFingerprint(
|
|
user_id=user_id,
|
|
preference_weights=fingerprint["preference_weights"],
|
|
decision_rules=fingerprint["decision_rules"],
|
|
total_behaviors=fingerprint["total_behaviors"],
|
|
behaviors_by_category=fingerprint["behaviors_by_category"],
|
|
avg_response_time_ms=fingerprint.get("avg_response_time_ms"),
|
|
)
|
|
db.add(entry)
|
|
db.flush()
|
|
fid = str(entry.id)
|
|
|
|
db.commit()
|
|
return fid
|
|
except Exception as e:
|
|
logger.error("保存指纹失败: %s", e)
|
|
if db:
|
|
try:
|
|
db.rollback()
|
|
except Exception:
|
|
pass
|
|
return None
|
|
finally:
|
|
if db:
|
|
try:
|
|
db.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def train(self, user_id: str) -> Optional[Dict[str, Any]]:
|
|
fingerprint = self.compute_fingerprint(user_id)
|
|
if fingerprint:
|
|
self.save_or_update(user_id, fingerprint)
|
|
logger.info("用户 %s 指纹训练完成: %d 条行为", user_id, fingerprint["total_behaviors"])
|
|
return fingerprint
|
|
|
|
def get_fingerprint(self, user_id: str) -> Optional[Dict[str, Any]]:
|
|
db: Optional[Session] = None
|
|
try:
|
|
db = SessionLocal()
|
|
fp = db.query(UserFingerprint).filter(
|
|
UserFingerprint.user_id == user_id
|
|
).first()
|
|
if not fp:
|
|
return None
|
|
return {
|
|
"user_id": fp.user_id,
|
|
"preference_weights": fp.preference_weights,
|
|
"decision_rules": fp.decision_rules,
|
|
"total_behaviors": fp.total_behaviors,
|
|
"behaviors_by_category": fp.behaviors_by_category,
|
|
"avg_response_time_ms": fp.avg_response_time_ms,
|
|
"model_version": fp.model_version,
|
|
"last_trained_at": fp.last_trained_at.isoformat() if fp.last_trained_at else None,
|
|
}
|
|
except Exception as e:
|
|
logger.error("获取指纹失败: %s", e)
|
|
return None
|
|
finally:
|
|
if db:
|
|
try:
|
|
db.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
fingerprint_engine = FingerprintEngine()
|