Files
aiagent/backend/app/api/goals.py
renjianbo ab1589921a fix: 修复35个安全与功能缺陷,补全知识进化/数字孪生/行为采集模块
## 安全修复 (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>
2026-05-10 19:50:20 +08:00

353 lines
11 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.
"""
Goal API — 目标管理接口
"""
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
import logging
from app.core.database import get_db
from app.api.auth import get_current_user
from app.models.user import User
from app.services import goal_service
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/goals",
tags=["goals"],
responses={
401: {"description": "未授权"},
404: {"description": "资源不存在"},
400: {"description": "请求参数错误"},
},
)
# ──────────────────────────── Schemas ────────────────────────────
class GoalCreate(BaseModel):
title: str
description: str = ""
priority: int = Field(default=5, ge=1, le=10)
deadline: Optional[datetime] = None
main_agent_id: Optional[str] = None
parent_goal_id: Optional[str] = None
autonomy_config: Optional[Dict[str, Any]] = None
class GoalUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
priority: Optional[int] = Field(default=None, ge=1, le=10)
plan: Optional[List[Dict[str, Any]]] = None
autonomy_config: Optional[Dict[str, Any]] = None
deadline: Optional[datetime] = None
main_agent_id: Optional[str] = None
class GoalResponse(BaseModel):
id: str
title: str
description: Optional[str]
status: str
priority: int
progress: float
plan: Optional[Any]
autonomy_config: Optional[Any]
creator_id: str
main_agent_id: Optional[str]
parent_goal_id: Optional[str]
started_at: Optional[datetime]
completed_at: Optional[datetime]
deadline: Optional[datetime]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class GoalTaskTreeResponse(BaseModel):
goal: Dict[str, Any]
tasks: List[Dict[str, Any]]
# ──────────────────────────── Endpoints ────────────────────────────
@router.post("", response_model=GoalResponse, status_code=201)
def create_goal(
data: GoalCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""创建新目标"""
return goal_service.create_goal(
db=db,
creator_id=current_user.id,
title=data.title,
description=data.description,
priority=data.priority,
deadline=data.deadline,
main_agent_id=data.main_agent_id,
parent_goal_id=data.parent_goal_id,
autonomy_config=data.autonomy_config,
)
@router.get("", response_model=List[GoalResponse])
def list_goals(
status: Optional[str] = None,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""列出目标"""
return goal_service.list_goals(
db=db,
creator_id=current_user.id,
status=status,
skip=skip,
limit=limit,
)
@router.get("/{goal_id}", response_model=GoalResponse)
def get_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""获取目标详情"""
return goal_service.get_goal(db, goal_id)
@router.put("/{goal_id}", response_model=GoalResponse)
def update_goal(
goal_id: str,
data: GoalUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""更新目标"""
return goal_service.update_goal(
db=db,
goal_id=goal_id,
title=data.title,
description=data.description,
status=data.status,
priority=data.priority,
plan=data.plan,
autonomy_config=data.autonomy_config,
deadline=data.deadline,
main_agent_id=data.main_agent_id,
)
@router.delete("/{goal_id}", status_code=204)
def delete_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""删除目标"""
goal_service.delete_goal(db, goal_id)
return None
@router.post("/{goal_id}/start", response_model=GoalResponse)
def start_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""启动目标执行(同步创建自主循环调度)"""
goal = goal_service.update_goal(db, goal_id, status="active")
goal.started_at = datetime.now()
db.commit()
db.refresh(goal)
from app.services.agent_schedule_service import sync_autonomy_schedule_for_goal
sync_autonomy_schedule_for_goal(db, goal_id)
logger.info(f"Goal started: {goal_id}")
return goal
@router.post("/{goal_id}/pause", response_model=GoalResponse)
def pause_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""暂停目标执行(移除自主循环调度)"""
from app.services.agent_schedule_service import sync_autonomy_schedule_for_goal
result = goal_service.update_goal(db, goal_id, status="paused")
sync_autonomy_schedule_for_goal(db, goal_id)
return result
@router.post("/{goal_id}/resume", response_model=GoalResponse)
def resume_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""恢复目标执行(重新创建自主循环调度)"""
from app.services.agent_schedule_service import sync_autonomy_schedule_for_goal
result = goal_service.update_goal(db, goal_id, status="active")
sync_autonomy_schedule_for_goal(db, goal_id)
return result
@router.get("/{goal_id}/tasks", response_model=GoalTaskTreeResponse)
def get_goal_task_tree(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""获取目标的任务树"""
return goal_service.get_goal_task_tree(db, goal_id)
class DecomposeResponse(BaseModel):
goal_id: str
task_count: int
message: str
@router.post("/{goal_id}/decompose", response_model=DecomposeResponse)
def decompose_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""触发目标分解(同步调用 LLM 拆解为 Task 树)"""
import asyncio
from app.services.main_agent_service import MainAgentService
service = MainAgentService(db)
try:
# 先验证 Goal 存在
goal_service.get_goal(db, goal_id)
goal = asyncio.run(service.decompose_goal(goal_id))
tasks = goal_service.list_tasks(db, goal_id=goal_id, limit=200)
return {
"goal_id": goal_id,
"task_count": len(tasks),
"message": f"目标已分解为 {len(tasks)} 个任务",
}
except Exception as e:
logger.error(f"Goal decompose failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"目标分解失败: {e}")
class ExecuteAsyncResponse(BaseModel):
goal_id: str
celery_task_id: Optional[str]
message: str
@router.post("/{goal_id}/execute-async", response_model=ExecuteAsyncResponse)
def execute_goal_async(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""异步执行目标(通过 Celery Worker 执行,适合长时间运行的目标)"""
from app.tasks.goal_tasks import execute_goal_task
goal_service.get_goal(db, goal_id)
goal_service.update_goal(db, goal_id, status="active")
task = execute_goal_task.delay(goal_id)
logger.info(f"Goal {goal_id} dispatched to Celery: {task.id}")
return {
"goal_id": goal_id,
"celery_task_id": task.id,
"message": "目标已提交异步执行,可通过 goal 状态和 task 列表追踪进度",
}
class InteractRequest(BaseModel):
message: str = Field(..., min_length=1, max_length=5000, description="用户回复消息")
attachments: Optional[List[Dict[str, Any]]] = Field(
default=None,
description="附件列表,每项包含 relative_path, filename, content_type 等字段"
)
class InteractResponse(BaseModel):
goal_id: str
reply: str
actions: List[Dict[str, Any]] = []
actions_count: int
goal_status: str
@router.post("/{goal_id}/interact", response_model=InteractResponse)
def interact_with_goal(
goal_id: str,
data: InteractRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""向目标发送用户反馈消息Agent 根据反馈重新调整任务。
用户在查看任务结果时可以直接回复 Agent 的问题,
Agent 收到回复后会重新执行相关任务。
"""
import asyncio
from app.services.main_agent_service import MainAgentService
goal_service.get_goal(db, goal_id)
service = MainAgentService(db)
try:
result = asyncio.run(service.interact_with_goal(
goal_id, data.message, attachments=data.attachments
))
return {
"goal_id": goal_id,
"reply": result.get("reply", ""),
"actions": result.get("actions", []),
"actions_count": result.get("actions_count", 0),
"goal_status": result.get("goal_status", "unknown"),
}
except Exception as e:
logger.error(f"Goal interact failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"交互失败: {e}")
@router.post("/{goal_id}/replan", response_model=DecomposeResponse)
def replan_goal(
goal_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""重新规划目标(清除未完成任务后重新分解)"""
import asyncio
from app.services.main_agent_service import MainAgentService
from app.models.task import Task
goal = goal_service.get_goal(db, goal_id)
# 清除 pending 和 failed 任务(保留已完成的)
db.query(Task).filter(
Task.goal_id == goal_id,
Task.status.in_(["pending", "failed", "cancelled"]),
).delete()
db.commit()
service = MainAgentService(db)
goal = asyncio.run(service.decompose_goal(goal_id))
tasks = goal_service.list_tasks(db, goal_id=goal_id, limit=200)
return {
"goal_id": goal_id,
"task_count": len(tasks),
"message": f"目标已重新规划,生成 {len(tasks)} 个新任务",
}