Files
qihuaiflow/backend/python/app/services/evolution_service.py
T

1238 lines
48 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.
"""
AI自进化服务层
负责管理用户级别的进化引擎、技能数据、驾驶舱数据
"""
import os
import json
import logging
import threading
import time
import random
from datetime import datetime, timedelta, date
from typing import Optional, List, Dict
from sqlalchemy.orm import Session
from sqlalchemy import func, desc
from collections import Counter
from ..models import (
User, EvolutionSkill, EvolutionEvent, EvolutionTrajectory,
UserEvolutionConfig, Conversation, Message,
SkillVersion, SkillTrajectoryLink,
MarketApp, SkillPublishRecord,
EvolutionReport, Notification,
TeamSharedSkill, Team
)
from ..config import UPLOAD_DIR
logger = logging.getLogger(__name__)
# 能力维度定义
ABILITY_DIMENSIONS = [
{"label": "文字创作", "key": "writing"},
{"label": "数据分析", "key": "data_analysis"},
{"label": "代码能力", "key": "coding"},
{"label": "知识问答", "key": "qa"},
{"label": "流程自动化", "key": "automation"},
{"label": "创意生成", "key": "creativity"},
{"label": "沟通协作", "key": "communication"},
{"label": "逻辑推理", "key": "reasoning"},
]
# 分类到维度的映射
CATEGORY_DIMENSION_MAP = {
"writing": "writing",
"code": "coding",
"data": "data_analysis",
"automation": "automation",
"creative": "creativity",
"knowledge": "qa",
"communication": "communication",
"reasoning": "reasoning",
}
class PipelineScheduler:
"""每30秒扫描discovered轨迹,投递到xSkill流水线"""
def __init__(self, user_id: str, db_session_factory, interval: int = 30):
self.user_id = user_id
self.db_session_factory = db_session_factory
self.poll_interval = interval
self._stop = threading.Event()
self._thread = None
def start(self):
if self._thread and self._thread.is_alive():
logger.info(f"PipelineScheduler 已在运行,用户 {self.user_id}")
return
self._stop.clear()
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
logger.info(f"PipelineScheduler 已启动,用户 {self.user_id},间隔 {self.poll_interval}s")
def stop(self):
self._stop.set()
if self._thread:
self._thread.join(timeout=10)
logger.info(f"PipelineScheduler 已停止,用户 {self.user_id}")
def is_running(self) -> bool:
return self._thread is not None and self._thread.is_alive()
def _run_loop(self):
logger.info(f"PipelineScheduler 开始运行循环,用户 {self.user_id}")
while not self._stop.is_set():
try:
db = self.db_session_factory()
try:
trajectories = db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.user_id == self.user_id,
EvolutionTrajectory.status == "discovered"
).order_by(EvolutionTrajectory.created_at).limit(10).all()
for traj in trajectories:
if self._stop.is_set():
break
try:
self._process_trajectory(db, traj)
except Exception as e:
logger.error(f"处理轨迹 {traj.id} 失败: {e}")
# 先回滚失效事务(_process_trajectory 内 commit 失败会让 session
# 进入 rolled-back 状态,不回滚直接再 commit 会抛 PendingRollbackError
# 并级联到外层日志),再写错误状态。
try:
db.rollback()
except Exception:
pass
try:
traj.error_msg = str(e)[:500]
traj.retry_count = (traj.retry_count or 0) + 1
traj.status = "error"
db.commit()
except Exception as ce:
logger.error(f"更新轨迹 {traj.id} 错误状态失败: {ce}")
try:
db.rollback()
except Exception:
pass
finally:
db.close()
except Exception as e:
logger.error(f"PipelineScheduler 扫描循环异常: {e}")
self._stop.wait(self.poll_interval)
def _process_trajectory(self, db, traj):
"""处理单条轨迹:discovered -> splitting -> split_done -> indexed -> clustering -> done"""
logger.info(f"开始处理轨迹 {traj.id},用户 {self.user_id}")
# step 1: discovered -> splitting
traj.status = "splitting"
db.commit()
# 模拟AtomTask拆分:生成2-5个模拟atom
time.sleep(random.uniform(0.1, 0.5))
atom_count = random.randint(2, 5)
traj.atom_count = atom_count
traj.tasks_extracted = atom_count
db.commit()
# step 2: splitting -> split_done
traj.status = "split_done"
db.commit()
# 为每个atom创建SkillTrajectoryLink
for i in range(atom_count):
link = SkillTrajectoryLink(
skill_name="__pending__",
user_id=self.user_id,
trajectory_id=traj.id,
atom_id=f"{traj.id}_atom_{i}",
weightscore=random.randint(1, 10)
)
db.add(link)
db.commit()
# step 3: split_done -> indexed (模拟向量索引)
time.sleep(random.uniform(0.1, 0.3))
traj.status = "indexed"
db.commit()
# step 4: indexed -> clustering (模拟聚类)
time.sleep(random.uniform(0.1, 0.3))
traj.status = "clustering"
db.commit()
# step 5: clustering -> done
# 检查是否有同名skill,有则归属,无则创建新baby skill
existing_skill = db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).order_by(func.random()).first()
if existing_skill:
skill_name = existing_skill.skill_name
# 更新SkillTrajectoryLink关联
links = db.query(SkillTrajectoryLink).filter(
SkillTrajectoryLink.trajectory_id == traj.id,
SkillTrajectoryLink.skill_name == "__pending__"
).all()
for link in links:
link.skill_name = skill_name
existing_skill.use_count += 1
logger.info(f"轨迹 {traj.id} 归属于现有技能 {skill_name}")
else:
# 创建新baby skill
skill_name = f"new_skill_{traj.id[:8]}"
new_skill = EvolutionSkill(
user_id=self.user_id,
skill_name=skill_name,
description=f"自动发掘的技能(源于轨迹{traj.id[:8]})",
tags=[],
category="未分类",
state="baby",
use_count=1,
version=1,
)
db.add(new_skill)
db.flush()
# 更新关联
links = db.query(SkillTrajectoryLink).filter(
SkillTrajectoryLink.trajectory_id == traj.id,
SkillTrajectoryLink.skill_name == "__pending__"
).all()
for link in links:
link.skill_name = skill_name
logger.info(f"轨迹 {traj.id} 创建新baby技能 {skill_name}")
db.commit()
# 检查某skill的candidates(关联轨迹数)≥3,自动 baby→staging
candidate_count = db.query(SkillTrajectoryLink).filter(
SkillTrajectoryLink.user_id == self.user_id,
SkillTrajectoryLink.skill_name == skill_name
).count()
if candidate_count >= 3:
skill_obj = db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name,
EvolutionSkill.state == "baby"
).first()
if skill_obj:
skill_obj.state = "staging"
skill_obj.updated_at = datetime.now()
# 记录事件
event = EvolutionEvent(
user_id=self.user_id,
skill_name=skill_name,
event_type="promoted",
description=f"技能 {skill_name} 自动晋升 baby->staging,关联轨迹数 {candidate_count}",
)
db.add(event)
db.commit()
logger.info(f"技能 {skill_name} 自动晋升 baby->staging,关联轨迹数 {candidate_count}")
traj.status = "done"
traj.processed_at = datetime.now()
db.commit()
# 更新pipeline_processed计数
config = db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.user_id == self.user_id
).first()
if config:
config.pipeline_processed = (config.pipeline_processed or 0) + 1
db.commit()
logger.info(f"轨迹 {traj.id} 处理完成,归属于技能 {skill_name}")
class EvolutionWatcherManager:
"""全局watcher线程管理器(单例)"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, '_initialized'):
self._schedulers: dict[str, PipelineScheduler] = {}
self._max_schedulers = 100
self._initialized = True
def start_user_evolution(self, user_id: str, db_session_factory) -> bool:
"""为用户启动进化引擎"""
if user_id in self._schedulers:
logger.info(f"用户 {user_id} 的进化引擎已在运行")
return False
if len(self._schedulers) >= self._max_schedulers:
logger.warning(f"已达到最大scheduler数量限制 {self._max_schedulers}")
return False
# 创建并启动 PipelineScheduler
scheduler = PipelineScheduler(user_id, db_session_factory)
scheduler.start()
self._schedulers[user_id] = scheduler
# 更新 user_evolution_config 表
db = db_session_factory()
try:
config = db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.user_id == user_id
).first()
if config:
config.watcher_status = "running"
config.pipeline_status = "running"
config.watcher_error = None
else:
config = UserEvolutionConfig(
user_id=user_id,
watcher_status="running",
pipeline_status="running",
)
db.add(config)
db.commit()
logger.info(f"用户 {user_id} 进化引擎已启动")
except Exception as e:
logger.error(f"更新用户进化配置失败: {e}")
return False
finally:
db.close()
return True
def stop_user_evolution(self, user_id: str) -> bool:
"""停止用户进化引擎"""
scheduler = self._schedulers.pop(user_id, None)
if not scheduler:
logger.info(f"用户 {user_id} 的进化引擎未在运行")
return False
scheduler.stop()
# 更新 user_evolution_config 表
db = self.db_session_factory() if hasattr(self, 'db_session_factory') else None
if db:
try:
config = db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.user_id == user_id
).first()
if config:
config.watcher_status = "stopped"
config.pipeline_status = "idle"
db.commit()
logger.info(f"用户 {user_id} 进化引擎已停止")
except Exception as e:
logger.error(f"更新用户进化配置失败: {e}")
return False
finally:
db.close()
return True
def get_user_status(self, user_id: str, db_session_factory) -> dict:
"""获取用户进化引擎状态"""
scheduler = self._schedulers.get(user_id)
is_running = scheduler.is_running() if scheduler else False
db = db_session_factory()
try:
config = db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.user_id == user_id
).first()
watcher_status = config.watcher_status if config else "unknown"
pipeline_status = config.pipeline_status if config else "unknown"
pipeline_processed = config.pipeline_processed if config else 0
watcher_error = config.watcher_error if config else None
# 统计该用户的技能数、轨迹数
skill_count = db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == user_id
).count()
trajectory_count = db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.user_id == user_id
).count()
return {
"user_id": user_id,
"scheduler_running": is_running,
"watcher_status": watcher_status,
"pipeline_status": pipeline_status,
"pipeline_processed": pipeline_processed,
"watcher_error": watcher_error,
"skill_count": skill_count,
"trajectory_count": trajectory_count,
}
except Exception as e:
logger.error(f"获取用户进化状态失败: {e}")
return {"error": str(e)}
finally:
db.close()
def restart_user_evolution(self, user_id: str, db_session_factory) -> bool:
"""重启用户进化引擎"""
self.stop_user_evolution(user_id)
return self.start_user_evolution(user_id, db_session_factory)
def shutdown_all(self):
"""优雅关闭所有scheduler"""
logger.info("正在关闭所有进化引擎...")
for user_id in list(self._schedulers.keys()):
self.stop_user_evolution(user_id)
logger.info("所有进化引擎已关闭")
class EvolutionService:
"""AI自进化服务"""
def __init__(self, db: Session, user_id: str):
self.db = db
self.user_id = user_id
# ============================================================
# 技能管理
# ============================================================
def get_skills(self, state: Optional[str] = None, keyword: Optional[str] = None,
sort_by: str = "updated_at", order: str = "desc") -> List[dict]:
"""获取用户技能列表"""
query = self.db.query(EvolutionSkill).filter(EvolutionSkill.user_id == self.user_id)
if state:
query = query.filter(EvolutionSkill.state == state)
if keyword:
query = query.filter(EvolutionSkill.skill_name.contains(keyword))
if order == "desc":
query = query.order_by(desc(getattr(EvolutionSkill, sort_by, EvolutionSkill.updated_at)))
else:
query = query.order_by(getattr(EvolutionSkill, sort_by, EvolutionSkill.updated_at))
skills = query.all()
return [self._skill_to_dict(s) for s in skills]
def get_skill_detail(self, skill_name: str) -> Optional[dict]:
"""获取技能详情(含SKILL.md内容)"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
return None
result = self._skill_to_dict(skill)
# 读取SKILL.md文件内容
if skill.skill_md_path and os.path.exists(skill.skill_md_path):
with open(skill.skill_md_path, 'r') as f:
result['skill_md_content'] = f.read()
else:
result['skill_md_content'] = None
return result
def update_skill(self, skill_name: str, data: dict) -> Optional[dict]:
"""编辑技能"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
return None
for key, value in data.items():
if hasattr(skill, key) and key not in ('id', 'user_id', 'created_at'):
setattr(skill, key, value)
skill.version += 1
skill.updated_at = datetime.now()
# 记录编辑事件
self._add_event(skill_name, "edited", f"技能 {skill_name} 已更新",
version_before=skill.version - 1, version_after=skill.version)
self.db.commit()
self.db.refresh(skill)
return self._skill_to_dict(skill)
def delete_skill(self, skill_name: str) -> bool:
"""删除技能"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
return False
self._add_event(skill_name, "deleted", f"技能 {skill_name} 已删除")
self.db.delete(skill)
self.db.commit()
return True
def freeze_skill(self, skill_name: str) -> Optional[dict]:
"""冻结技能"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
return None
skill.state = "frozen"
skill.updated_at = datetime.now()
self._add_event(skill_name, "frozen", f"技能 {skill_name} 已冻结")
self.db.commit()
self.db.refresh(skill)
return self._skill_to_dict(skill)
def unfreeze_skill(self, skill_name: str) -> Optional[dict]:
"""解冻技能"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
return None
skill.state = "main"
skill.updated_at = datetime.now()
self._add_event(skill_name, "unfrozen", f"技能 {skill_name} 已解冻")
self.db.commit()
self.db.refresh(skill)
return self._skill_to_dict(skill)
def rollback_skill(self, skill_name: str, target_version: int) -> Optional[dict]:
"""技能回滚:将技能恢复到指定的历史版本"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
logger.warning(f"rollback_skill: 技能 {skill_name} 不存在,用户 {self.user_id}")
return None
# 查询目标版本的SKILL.md内容
target_sv = self.db.query(SkillVersion).filter(
SkillVersion.user_id == self.user_id,
SkillVersion.skill_name == skill_name,
SkillVersion.version == target_version
).first()
if not target_sv:
logger.warning(f"rollback_skill: 版本 {target_version} 不存在,技能 {skill_name}")
return None
version_before = skill.version
# 如果skill_md_path存在,以新内容覆盖写入该文件
if skill.skill_md_path:
try:
os.makedirs(os.path.dirname(skill.skill_md_path), exist_ok=True)
with open(skill.skill_md_path, 'w', encoding='utf-8') as f:
f.write(target_sv.skill_md_content or "")
logger.info(f"rollback_skill: 已覆盖SKILL.md文件 {skill.skill_md_path}")
except Exception as e:
logger.error(f"rollback_skill: 写入SKILL.md文件失败: {e}")
return {"error": f"写入SKILL.md文件失败: {str(e)}"}
# 保存回滚前的当前版本作为快照(新SkillVersion记录)
current_content = ""
if skill.skill_md_path and os.path.exists(skill.skill_md_path):
with open(skill.skill_md_path, 'r', encoding='utf-8') as f:
current_content = f.read()
else:
# 尝试从数据库获取当前版本内容
current_sv = self.db.query(SkillVersion).filter(
SkillVersion.user_id == self.user_id,
SkillVersion.skill_name == skill_name,
SkillVersion.version == version_before
).first()
if current_sv:
current_content = current_sv.skill_md_content or ""
snapshot_version = SkillVersion(
user_id=self.user_id,
skill_name=skill_name,
version=version_before,
skill_md_content=current_content,
git_commit_sha=None,
description=f"回滚前快照(版本{version_before})"
)
self.db.add(snapshot_version)
# 更新evolution_skills
skill.version += 1
skill.git_commit_sha = None
skill.updated_at = datetime.now()
# 记录回滚事件
self._add_event(
skill_name, "rolled_back",
f"版本 V{version_before} 回滚至 V{target_version}",
version_before=version_before,
version_after=skill.version
)
self.db.commit()
self.db.refresh(skill)
logger.info(f"rollback_skill: 技能 {skill_name} 从版本 {version_before} 回滚至 {target_version}")
return self._skill_to_dict(skill)
def get_skill_history(self, skill_name: str) -> List[dict]:
"""获取技能事件历史"""
events = self.db.query(EvolutionEvent).filter(
EvolutionEvent.user_id == self.user_id,
EvolutionEvent.skill_name == skill_name
).order_by(desc(EvolutionEvent.created_at)).all()
return [self._event_to_dict(e) for e in events]
def get_skill_trajectories(self, skill_name: str) -> List[dict]:
"""获取技能来源轨迹(通过 skill_trajectory_links 表关联)"""
from ..models import SkillTrajectoryLink
# 通过关联表查询该技能关联的轨迹ID
links = self.db.query(SkillTrajectoryLink).filter(
SkillTrajectoryLink.user_id == self.user_id,
SkillTrajectoryLink.skill_name == skill_name
).order_by(desc(SkillTrajectoryLink.created_at)).limit(20).all()
if not links:
return []
traj_ids = [link.trajectory_id for link in links if link.trajectory_id]
trajectories = self.db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.id.in_(traj_ids)
).order_by(desc(EvolutionTrajectory.created_at)).all()
result = []
for t in trajectories:
d = self._trajectory_to_dict(t)
# 附加权重分
for link in links:
if link.trajectory_id == t.id:
d['weightscore'] = link.weightscore
d['atom_id'] = link.atom_id
break
result.append(d)
return result
# ============================================================
# 进化事件
# ============================================================
def get_events(self, event_type: Optional[str] = None,
skill_name: Optional[str] = None,
limit: int = 50) -> List[dict]:
"""获取进化事件列表"""
query = self.db.query(EvolutionEvent).filter(EvolutionEvent.user_id == self.user_id)
if event_type:
query = query.filter(EvolutionEvent.event_type == event_type)
if skill_name:
query = query.filter(EvolutionEvent.skill_name == skill_name)
events = query.order_by(desc(EvolutionEvent.created_at)).limit(limit).all()
return [self._event_to_dict(e) for e in events]
# ============================================================
# 驾驶舱数据
# ============================================================
def get_skill_tree(self) -> Dict:
"""获取技能图谱数据"""
skills = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).all()
categories = {}
for s in skills:
cat = s.category or "未分类"
if cat not in categories:
categories[cat] = []
categories[cat].append(self._skill_to_dict(s))
# 统计
total = len(skills)
main_count = sum(1 for s in skills if s.state == "main")
baby_count = sum(1 for s in skills if s.state == "baby")
staging_count = sum(1 for s in skills if s.state == "staging")
frozen_count = sum(1 for s in skills if s.state == "frozen")
return {
"categories": [{"name": k, "skills": v} for k, v in categories.items()],
"total_count": total,
"main_count": main_count,
"baby_count": baby_count,
"staging_count": staging_count,
"frozen_count": frozen_count,
}
def get_timeline(self, event_type: Optional[str] = None,
limit: int = 50) -> List[dict]:
"""获取进化时间线"""
return self.get_events(event_type=event_type, limit=limit)
def get_statistics(self) -> Dict:
"""获取使用统计数据"""
skills = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).all()
# Top排名
sorted_skills = sorted(skills, key=lambda s: s.use_count, reverse=True)
ranking = []
for s in sorted_skills[:10]:
ranking.append({
"skill_name": s.skill_name,
"category": s.category or "未分类",
"use_count": s.use_count,
"state": s.state,
"last_used_at": s.last_used_at.isoformat() if s.last_used_at else None,
})
# 类别分布
category_dist = {}
for s in skills:
cat = s.category or "未分类"
category_dist[cat] = category_dist.get(cat, 0) + s.use_count
# 总调用次数
total_use = sum(s.use_count for s in skills)
# 最近7天的趋势(按日期分组)
seven_days_ago = datetime.now() - timedelta(days=7)
recent_trajectories = self.db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.user_id == self.user_id,
EvolutionTrajectory.created_at >= seven_days_ago,
EvolutionTrajectory.status == "done"
).all()
# 按天汇总
daily_counts = {}
for t in recent_trajectories:
day = t.created_at.strftime("%m-%d")
daily_counts[day] = daily_counts.get(day, 0) + 1
# 低使用提醒(超过30天未使用)
thirty_days_ago = datetime.now() - timedelta(days=30)
zero_use_skills = [
self._skill_to_dict(s) for s in skills
if s.use_count < 5 and (s.last_used_at is None or s.last_used_at < thirty_days_ago)
]
return {
"ranking": ranking,
"total_use_count": total_use,
"category_distribution": [{"category": k, "count": v} for k, v in category_dist.items()],
"daily_trend": [{"date": k, "count": v} for k, v in sorted(daily_counts.items())],
"low_use_skills": zero_use_skills,
}
def get_profile(self) -> Dict:
"""获取AI能力画像"""
skills = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).all()
total = len(skills)
main_count = sum(1 for s in skills if s.state == "main")
staging_count = sum(1 for s in skills if s.state == "staging")
baby_count = sum(1 for s in skills if s.state == "baby")
total_use = sum(s.use_count for s in skills)
# 计算能力维度
dim_scores = {d["key"]: {"label": d["label"], "value": 0, "maxValue": 10} for d in ABILITY_DIMENSIONS}
for s in skills:
dim_key = CATEGORY_DIMENSION_MAP.get(s.category, "qa")
if dim_key in dim_scores:
# 成熟技能贡献更大
multiplier = {"main": 1.5, "staging": 1.0, "baby": 0.5, "frozen": 0.3}.get(s.state, 1.0)
dim_scores[dim_key]["value"] += min(s.use_count * 0.2 * multiplier, 3)
dimensions = list(dim_scores.values())
for d in dimensions:
d["value"] = round(min(d["value"], 10), 1)
# 进化等级计算
level_score = (total * 2 + main_count * 5 + staging_count * 3 + baby_count * 1 + total_use * 0.01)
level = min(max(int(level_score / 10) + 1, 1), 10)
level_titles = {
1: "AI初生", 2: "学习幼苗", 3: "技能萌芽", 4: "成长小树",
5: "能力之林", 6: "智慧果园", 7: "知识大师", 8: "AI专家",
9: "进化先驱", 10: "超级龙虾"
}
# 能力标签
all_tags = []
for s in skills:
if s.tags:
all_tags.extend(s.tags)
# 去重并按技能数量加权排序
tag_counter = Counter(all_tags)
top_tags = [tag for tag, _ in tag_counter.most_common(12)]
if not top_tags:
top_tags = ["AI进化中", "持续学习", "数据分析", "文字处理", "自动化", "智能助手"]
# 成长建议
suggestions = []
if total == 0:
suggestions = [
"多与AI进行各类对话,让它从实际交互中学习你的需求和风格",
"尝试在不同领域使用AI:写作、编程、数据分析、创意生成等"
]
else:
dim_v = {d["label"]: d["value"] for d in dimensions}
weak_dims = [d for d in dimensions if d["value"] < 3]
if weak_dims:
suggestions.append(f"建议多进行{weak_dims[0]['label']}类对话,培养该领域技能")
if baby_count > 0:
suggestions.append(f"你有{baby_count}个萌芽技能,保持使用帮助它们成长")
if not any(s.state == "staging" for s in skills):
suggestions.append("继续积累高质量对话,新技能将自动进入灰度验证")
suggestions.append("持续在各类场景使用AI,系统会自动发掘你的专属技能")
return {
"level": level,
"level_title": level_titles.get(level, "进化者"),
"total_skills": total,
"active_skills": main_count,
"staging_skills": staging_count,
"baby_skills": baby_count,
"total_use_count": total_use,
"dimensions": dimensions,
"tags": top_tags,
"growth_suggestions": suggestions[:4],
}
# ============================================================
# 轨迹管理
# ============================================================
def submit_trajectory(self, conversation_id: str, messages_data: List[dict]) -> dict:
"""提交对话轨迹(对话结束后调用)"""
user_dir = UPLOAD_DIR.parent / "users" / self.user_id / "trajectories"
user_dir.mkdir(parents=True, exist_ok=True)
import uuid
traj_id = str(uuid.uuid4())[:8]
traj_dir = user_dir / f"traj_{traj_id}"
traj_dir.mkdir(exist_ok=True)
# 生成标准化轨迹文件
traj_md = self._format_trajectory_md(messages_data)
traj_file = traj_dir / f"traj_{traj_id}.md"
with open(traj_file, 'w', encoding='utf-8') as f:
f.write(traj_md)
# 记录到数据库
trajectory = EvolutionTrajectory(
user_id=self.user_id,
conversation_id=conversation_id,
file_path=str(traj_file),
status="discovered",
atom_count=0,
token_used=sum(m.get("tokens", 0) for m in messages_data),
)
self.db.add(trajectory)
self.db.commit()
self.db.refresh(trajectory)
logger.info(f"用户 {self.user_id} 提交轨迹 {traj_id}, 对话 {conversation_id}")
return self._trajectory_to_dict(trajectory)
def _format_trajectory_md(self, messages: List[dict]) -> str:
"""将消息列表格式化为xSkill兼容的轨迹Markdown"""
lines = ["# Conversation Trajectory\n"]
for msg in messages:
role = msg.get("role", "unknown")
content = msg.get("content", "")
if role == "user":
lines.append(f"## User\n\n{content}\n")
elif role == "assistant":
lines.append(f"## Assistant\n\n{content}\n")
return "\n".join(lines)
# ============================================================
# P0核心:流水线状态、发布到市场、进化报告、团队共享
# ============================================================
def get_pipeline_status(self) -> dict:
"""获取流水线运行状态"""
# scheduler运行状态(通过全局单例)
watcher = EvolutionWatcherManager()
scheduler = watcher._schedulers.get(self.user_id)
scheduler_running = scheduler.is_running() if scheduler else False
# 查询 user_evolution_config
config = self.db.query(UserEvolutionConfig).filter(
UserEvolutionConfig.user_id == self.user_id
).first()
watcher_status = config.watcher_status if config else "unknown"
pipeline_status = config.pipeline_status if config else "unknown"
# discovered_count(待处理轨迹数)
discovered_count = self.db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.user_id == self.user_id,
EvolutionTrajectory.status == "discovered"
).count()
# done_count(已完成轨迹数)
done_count = self.db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.user_id == self.user_id,
EvolutionTrajectory.status == "done"
).count()
# total_skills
total_skills = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).count()
# total_trajectories
total_trajectories = self.db.query(EvolutionTrajectory).filter(
EvolutionTrajectory.user_id == self.user_id
).count()
logger.info(f"get_pipeline_status: 用户 {self.user_id}, scheduler_running={scheduler_running}")
return {
"scheduler_running": scheduler_running,
"watcher_status": watcher_status,
"pipeline_status": pipeline_status,
"discovered_count": discovered_count,
"done_count": done_count,
"total_skills": total_skills,
"total_trajectories": total_trajectories,
}
def publish_to_market(self, skill_name: str, price: float = 0.00, market_description: str = "") -> dict:
"""将技能发布到应用市场"""
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
logger.warning(f"publish_to_market: 技能 {skill_name} 不存在")
return {"error": "技能不存在"}
# 验证技能 state=main,否则返回错误
if skill.state != "main":
logger.warning(f"publish_to_market: 技能 {skill_name} 状态为 {skill.state},不是main")
return {"error": f"只有main状态的技能才能发布到市场,当前状态: {skill.state}"}
# 查询是否已经发布过
existing_record = self.db.query(SkillPublishRecord).filter(
SkillPublishRecord.user_id == self.user_id,
SkillPublishRecord.skill_name == skill_name,
SkillPublishRecord.status == "published"
).first()
if existing_record:
logger.warning(f"publish_to_market: 技能 {skill_name} 已发布过")
return {"error": "该技能已发布到市场,不能重复发布"}
# 在 market_apps 表中创建记录
market_app = MarketApp(
name=f"{skill_name}(进化)",
type="skill",
source="user",
publisher_id=self.user_id,
description=market_description or skill.description or "",
price=price,
skill_content="来自AI自进化技能的SKILL.md",
status="published",
)
self.db.add(market_app)
self.db.flush()
# 在 skill_publish_records 表中创建记录
publish_record = SkillPublishRecord(
skill_id=skill.id,
skill_name=skill_name,
user_id=self.user_id,
market_app_id=market_app.id,
price=price,
market_description=market_description,
status="published",
)
self.db.add(publish_record)
# 记录事件
self._add_event(skill_name, "published", f"技能 {skill_name} 已发布到市场,价格 {price}")
self.db.commit()
logger.info(f"publish_to_market: 技能 {skill_name} 已发布到市场,market_app_id={market_app.id}")
return {
"skill_name": skill_name,
"market_app_id": market_app.id,
"price": price,
"status": "published",
"publish_record_id": publish_record.id,
}
def generate_report(self, report_type: str = "weekly") -> dict:
"""生成进化报告(weekly/monthly)"""
now = datetime.now()
if report_type == "weekly":
period_start = (now - timedelta(days=7)).date()
period_end = now.date()
elif report_type == "monthly":
period_start = (now - timedelta(days=30)).date()
period_end = now.date()
else:
return {"error": f"不支持的报告类型: {report_type},仅支持 weekly/monthly"}
# 新技能数(skills表中created_at在时间范围内的)
new_skills_count = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.created_at >= period_start,
EvolutionSkill.created_at <= period_end
).count()
# 技能升级数(events中state_changed事件,event_type含 promoted)
upgrades_count = self.db.query(EvolutionEvent).filter(
EvolutionEvent.user_id == self.user_id,
EvolutionEvent.created_at >= period_start,
EvolutionEvent.created_at <= period_end,
EvolutionEvent.event_type.in_(["promoted", "edited", "milestone"])
).count()
# 最活跃技能TOP5
top_skills = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).order_by(desc(EvolutionSkill.use_count)).limit(5).all()
top_skills_data = [self._skill_to_dict(s) for s in top_skills]
# 总调用次数
total_calls_raw = self.db.query(func.sum(EvolutionSkill.use_count)).filter(
EvolutionSkill.user_id == self.user_id
).scalar() or 0
total_calls = int(total_calls_raw) if total_calls_raw else 0
# 等级变化(查询报告期内的promoted事件数作为等级变化的近似)
level_changes = self.db.query(EvolutionEvent).filter(
EvolutionEvent.user_id == self.user_id,
EvolutionEvent.created_at >= period_start,
EvolutionEvent.created_at <= period_end,
EvolutionEvent.event_type == "promoted"
).count()
# 维度分析(基于当前skills计算)
skills = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id
).all()
dim_scores = {d["key"]: {"label": d["label"], "value": 0, "maxValue": 10} for d in ABILITY_DIMENSIONS}
for s in skills:
dim_key = CATEGORY_DIMENSION_MAP.get(s.category, "qa")
if dim_key in dim_scores:
multiplier = {"main": 1.5, "staging": 1.0, "baby": 0.5, "frozen": 0.3}.get(s.state, 1.0)
dim_scores[dim_key]["value"] += min(s.use_count * 0.2 * multiplier, 3)
dimensions = [{"label": v["label"], "value": round(min(v["value"], 10), 1)} for v in dim_scores.values()]
# 构建报告数据
report_data = {
"report_type": report_type,
"period_start": period_start.isoformat(),
"period_end": period_end.isoformat(),
"generated_at": now.isoformat(),
"summary": {
"new_skills_count": new_skills_count,
"upgrades_count": upgrades_count,
"total_skills": len(skills),
"total_calls": total_calls,
"level_changes": level_changes,
},
"top_skills": top_skills_data,
"dimensions": dimensions,
}
# 存入 evolution_reports 表
evolution_report = EvolutionReport(
user_id=self.user_id,
report_type=report_type,
period_start=period_start,
period_end=period_end,
report_data=report_data,
)
self.db.add(evolution_report)
# 发送通知给用户
notification = Notification(
user_id=self.user_id,
type="evolution_report",
title=f"{'周报' if report_type == 'weekly' else '月报'}已生成",
content=f"你的AI进化{'周报' if report_type == 'weekly' else '月报'}已生成。"
f"新增技能 {new_skills_count} 个,技能升级 {upgrades_count} 次,总调用次数 {total_calls}。",
)
self.db.add(notification)
self.db.commit()
self.db.refresh(evolution_report)
logger.info(f"generate_report: 用户 {self.user_id} 生成 {report_type} 报告, report_id={evolution_report.id}")
return report_data
def get_reports(self, report_type: Optional[str] = None) -> List[dict]:
"""获取用户所有报告列表"""
query = self.db.query(EvolutionReport).filter(
EvolutionReport.user_id == self.user_id
)
if report_type:
query = query.filter(EvolutionReport.report_type == report_type)
reports = query.order_by(desc(EvolutionReport.created_at)).all()
result = []
for r in reports:
result.append({
"id": r.id,
"report_type": r.report_type,
"period_start": r.period_start.isoformat() if r.period_start else None,
"period_end": r.period_end.isoformat() if r.period_end else None,
"created_at": r.created_at.isoformat() if r.created_at else None,
})
return result
def get_report_detail(self, report_id: str) -> Optional[dict]:
"""获取报告详情"""
report = self.db.query(EvolutionReport).filter(
EvolutionReport.user_id == self.user_id,
EvolutionReport.id == report_id
).first()
if not report:
return None
return {
"id": report.id,
"user_id": report.user_id,
"report_type": report.report_type,
"period_start": report.period_start.isoformat() if report.period_start else None,
"period_end": report.period_end.isoformat() if report.period_end else None,
"report_data": report.report_data or {},
"created_at": report.created_at.isoformat() if report.created_at else None,
}
def share_to_team(self, skill_name: str, team_id: str) -> dict:
"""将技能分享到团队"""
# 验证技能存在且属于当前用户
skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name
).first()
if not skill:
logger.warning(f"share_to_team: 技能 {skill_name} 不存在或不属于用户 {self.user_id}")
return {"error": "技能不存在或不属于当前用户"}
# 验证用户属于该team(检查team存在且用户是owner,或者有team_member关系需要检查)
team = self.db.query(Team).filter(
Team.id == team_id
).first()
if not team:
logger.warning(f"share_to_team: team {team_id} 不存在")
return {"error": "团队不存在"}
# 检查用户是team的owner或属于该team(根据模型结构,Team有owner_id字段)
is_owner = team.owner_id == self.user_id
if not is_owner:
logger.warning(f"share_to_team: 用户 {self.user_id} 不属于 team {team_id}")
return {"error": "用户不属于该团队"}
# 检查是否已分享
existing = self.db.query(TeamSharedSkill).filter(
TeamSharedSkill.team_id == team_id,
TeamSharedSkill.skill_id == skill.id
).first()
if existing:
logger.warning(f"share_to_team: 技能 {skill_name} 已分享至 team {team_id}")
return {"error": "该技能已分享至此团队"}
# 在 team_shared_skills 表中创建记录
shared_skill = TeamSharedSkill(
team_id=team_id,
skill_id=skill.id,
skill_name=skill_name,
shared_by=self.user_id,
shared_by_name=None,
is_public=True,
)
self.db.add(shared_skill)
# 记录事件
self._add_event(skill_name, "shared", f"技能 {skill_name} 已分享至团队 {team.name}")
self.db.commit()
self.db.refresh(shared_skill)
logger.info(f"share_to_team: 技能 {skill_name} 已分享至 team {team_id}, shared_id={shared_skill.id}")
return {
"shared_id": shared_skill.id,
"skill_name": skill_name,
"team_id": team_id,
"team_name": team.name,
"shared_by": self.user_id,
"is_public": True,
}
# ============================================================
# 辅助方法
# ============================================================
def _skill_to_dict(self, skill: EvolutionSkill) -> dict:
return {
"id": skill.id,
"skill_name": skill.skill_name,
"description": skill.description or "",
"tags": skill.tags or [],
"category": skill.category or "未分类",
"state": skill.state,
"use_count": skill.use_count,
"version": skill.version,
"last_used_at": skill.last_used_at.isoformat() if skill.last_used_at else None,
"created_at": skill.created_at.isoformat() if skill.created_at else None,
"updated_at": skill.updated_at.isoformat() if skill.updated_at else None,
}
def _event_to_dict(self, event: EvolutionEvent) -> dict:
return {
"id": event.id,
"skill_name": event.skill_name,
"event_type": event.event_type,
"description": event.description or "",
"version_before": event.version_before,
"version_after": event.version_after,
"created_at": event.created_at.isoformat() if event.created_at else None,
}
def _trajectory_to_dict(self, traj: EvolutionTrajectory) -> dict:
return {
"id": traj.id,
"conversation_id": traj.conversation_id,
"file_path": traj.file_path,
"status": traj.status,
"atom_count": traj.atom_count,
"token_used": traj.token_used,
"created_at": traj.created_at.isoformat() if traj.created_at else None,
"processed_at": traj.processed_at.isoformat() if traj.processed_at else None,
}
def _add_event(self, skill_name: str, event_type: str, description: str,
version_before: Optional[int] = None, version_after: Optional[int] = None):
event = EvolutionEvent(
user_id=self.user_id,
skill_name=skill_name,
event_type=event_type,
description=description,
version_before=version_before,
version_after=version_after,
)
self.db.add(event)
def get_evolution_service(db: Session, user_id: str) -> EvolutionService:
return EvolutionService(db, user_id)