diff --git a/backend/alembic/versions/014_add_schedule_goal_fields.py b/backend/alembic/versions/014_add_schedule_goal_fields.py new file mode 100644 index 0000000..55ac0a1 --- /dev/null +++ b/backend/alembic/versions/014_add_schedule_goal_fields.py @@ -0,0 +1,30 @@ +"""add schedule_type, goal_id, goal_config to agent_schedules + +Revision ID: 014_add_schedule_goal_fields +Revises: 013_add_goals_tasks +Create Date: 2026-05-08 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.mysql import CHAR + +revision = "014_add_schedule_goal_fields" +down_revision = "013_add_goals_tasks" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("agent_schedules", sa.Column("schedule_type", sa.String(20), default="agent", comment="调度类型: agent / goal")) + op.add_column("agent_schedules", sa.Column("goal_id", CHAR(36), sa.ForeignKey("goals.id"), nullable=True, comment="关联 Goal ID")) + op.add_column("agent_schedules", sa.Column("goal_config", sa.JSON, nullable=True, comment="Goal 调度配置")) + op.create_index("ix_agent_schedules_goal_id", "agent_schedules", ["goal_id"]) + op.alter_column("agent_schedules", "agent_id", existing_type=CHAR(36), nullable=True) + + +def downgrade() -> None: + op.drop_index("ix_agent_schedules_goal_id", table_name="agent_schedules") + op.drop_column("agent_schedules", "goal_config") + op.drop_column("agent_schedules", "goal_id") + op.drop_column("agent_schedules", "schedule_type") + op.alter_column("agent_schedules", "agent_id", existing_type=CHAR(36), nullable=False) diff --git a/backend/app/models/agent_schedule.py b/backend/app/models/agent_schedule.py index 081c7a3..547e1f0 100644 --- a/backend/app/models/agent_schedule.py +++ b/backend/app/models/agent_schedule.py @@ -1,7 +1,7 @@ """Agent 定时任务表:按 cron 表达式周期执行 Agent""" import uuid from datetime import datetime -from sqlalchemy import Column, String, Text, Integer, DateTime, ForeignKey, Boolean +from sqlalchemy import Column, String, Text, Integer, DateTime, ForeignKey, Boolean, JSON from sqlalchemy.dialects.mysql import CHAR from app.core.database import Base @@ -11,7 +11,10 @@ class AgentSchedule(Base): __tablename__ = "agent_schedules" id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) - agent_id = Column(CHAR(36), ForeignKey("agents.id"), nullable=False, index=True, comment="关联 Agent ID") + agent_id = Column(CHAR(36), ForeignKey("agents.id"), nullable=True, index=True, comment="关联 Agent ID(schedule_type=agent 时必填)") + schedule_type = Column(String(20), default="agent", comment="调度类型: agent / goal") + goal_id = Column(CHAR(36), ForeignKey("goals.id"), nullable=True, index=True, comment="关联 Goal ID(schedule_type=goal 时必填)") + goal_config = Column(JSON, nullable=True, comment="Goal 调度配置: {title, description, priority}") name = Column(String(100), nullable=False, comment="任务名称") cron_expression = Column(String(100), nullable=False, comment="cron 表达式,如 0 9 * * *") input_message = Column(Text, nullable=False, comment="定时执行时发送的消息内容") diff --git a/backend/app/services/agent_schedule_service.py b/backend/app/services/agent_schedule_service.py index acc6def..f979055 100644 --- a/backend/app/services/agent_schedule_service.py +++ b/backend/app/services/agent_schedule_service.py @@ -47,6 +47,46 @@ def compute_next_run(cron_expression: str, after: Optional[datetime] = None, tz: return next_utc.replace(tzinfo=None) +def _create_execution_for_goal_schedule(db: Session, schedule) -> Optional[str]: + """为 Goal 类型定时任务创建 Goal 并投递执行。 + + Args: + db: 数据库会话 + schedule: AgentSchedule ORM 对象 (schedule_type == "goal") + + Returns: + 创建的 goal_id,失败返回 None + """ + from app.services.goal_service import create_goal, update_goal + from app.tasks.goal_tasks import execute_goal_task + + gc = schedule.goal_config or {} + title = gc.get("title", schedule.input_message or schedule.name) + description = gc.get("description", "") + priority = gc.get("priority", 5) + main_agent_id = schedule.agent_id or gc.get("main_agent_id") + + try: + goal = create_goal( + db=db, + creator_id=schedule.user_id, + title=title, + description=description, + priority=priority, + main_agent_id=main_agent_id, + ) + task = execute_goal_task.delay(str(goal.id)) + update_goal(db, str(goal.id), status="active") + logger.info( + "Goal 定时任务 %s 已投递: goal_id=%s celery_task=%s", + schedule.id, goal.id, task.id, + ) + return str(goal.id) + except Exception as e: + logger.error("Goal 定时任务 %s 投递失败: %s", schedule.id, e) + return None + + def create_execution_for_schedule(db: Session, schedule) -> Optional[str]: """为定时任务创建 Execution 记录并投递 Celery 任务。 @@ -60,6 +100,10 @@ def create_execution_for_schedule(db: Session, schedule) -> Optional[str]: from app.models.execution import Execution from app.models.agent import Agent + # Goal 类型调度:创建 Goal 并投递执行 + if getattr(schedule, "schedule_type", "agent") == "goal" and schedule.goal_id: + return _create_execution_for_goal_schedule(db, schedule) + agent = db.query(Agent).filter(Agent.id == schedule.agent_id).first() if not agent: logger.warning("定时任务 %s 关联的 Agent %s 不存在", schedule.id, schedule.agent_id) diff --git a/frontend/src/components/MainLayout.vue b/frontend/src/components/MainLayout.vue index a25b5ec..0680488 100644 --- a/frontend/src/components/MainLayout.vue +++ b/frontend/src/components/MainLayout.vue @@ -39,6 +39,10 @@ Agent协作 + + + 数字员工工厂 + 定时任务 @@ -106,7 +110,7 @@ import { computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useUserStore } from '@/stores/user' -import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell, Grid, DataAnalysis, Tools, Clock, Share, ChatLineSquare, Shop } from '@element-plus/icons-vue' +import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell, Grid, DataAnalysis, Tools, Clock, Share, ChatLineSquare, Shop, UserFilled } from '@element-plus/icons-vue' const router = useRouter() const route = useRoute() @@ -131,6 +135,7 @@ const activeMenu = computed(() => { if (route.path === '/alert-rules') return 'alert-rules' if (route.path === '/agent-chat' || route.path.startsWith('/agent-chat/')) return 'agent-chat' if (route.path === '/agent-schedules') return 'agent-schedules' + if (route.path === '/digital-employees' || route.path.startsWith('/goals/')) return 'digital-employees' return 'workflows' }) @@ -166,6 +171,8 @@ const handleMenuSelect = (key: string) => { router.push('/alert-rules') } else if (key === 'agent-schedules') { router.push('/agent-schedules') + } else if (key === 'digital-employees') { + router.push('/digital-employees') } } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 05f094d..520a243 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -153,6 +153,18 @@ const router = createRouter({ name: 'agent-market', component: () => import('@/views/AgentMarket.vue'), meta: { requiresAuth: true } + }, + { + path: '/digital-employees', + name: 'digital-employees', + component: () => import('@/views/DigitalEmployeeFactory.vue'), + meta: { requiresAuth: true } + }, + { + path: '/goals/:id', + name: 'goal-detail', + component: () => import('@/views/GoalDetail.vue'), + meta: { requiresAuth: true } } ] }) diff --git a/frontend/src/stores/goal.ts b/frontend/src/stores/goal.ts new file mode 100644 index 0000000..111555c --- /dev/null +++ b/frontend/src/stores/goal.ts @@ -0,0 +1,195 @@ +/** + * Goal/Task 状态管理 — 数字员工工厂 + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import api from '@/api' + +export interface Goal { + id: string + title: string + description?: string + status: string + priority: number + progress: number + plan?: any + autonomy_config?: any + creator_id: string + main_agent_id?: string + parent_goal_id?: string + started_at?: string + completed_at?: string + deadline?: string + created_at: string + updated_at: string +} + +export interface Task { + id: string + goal_id: string + title: string + description?: string + status: string + priority: number + task_config?: any + parent_task_id?: string + depends_on?: string[] + result?: any + error_message?: string + execution_id?: string + assigned_agent_id?: string + assigned_agent_name?: string + requires_approval: boolean + approver_id?: string + approval_status?: string + started_at?: string + completed_at?: string + deadline?: string + created_at: string + updated_at: string +} + +export interface GoalTaskTree { + goal: Record + tasks: any[] +} + +export const useGoalStore = defineStore('goal', () => { + const goals = ref([]) + const currentGoal = ref(null) + const taskTree = ref(null) + const tasks = ref([]) + const loading = ref(false) + + // ─── Goals ─── + + const fetchGoals = async (options?: { status?: string; skip?: number; limit?: number }) => { + loading.value = true + try { + const params: any = {} + if (options?.status) params.status = options.status + if (options?.skip !== undefined) params.skip = options.skip + if (options?.limit !== undefined) params.limit = options.limit + const response = await api.get('/api/v1/goals', { params }) + goals.value = response.data + return response.data + } finally { + loading.value = false + } + } + + const fetchGoal = async (goalId: string) => { + loading.value = true + try { + const response = await api.get(`/api/v1/goals/${goalId}`) + currentGoal.value = response.data + return response.data + } finally { + loading.value = false + } + } + + const createGoal = async (data: Partial & { title: string }) => { + const response = await api.post('/api/v1/goals', data) + goals.value.unshift(response.data) + return response.data + } + + const updateGoal = async (goalId: string, data: Partial) => { + const response = await api.put(`/api/v1/goals/${goalId}`, data) + const idx = goals.value.findIndex(g => g.id === goalId) + if (idx >= 0) goals.value[idx] = response.data + if (currentGoal.value?.id === goalId) currentGoal.value = response.data + return response.data + } + + const deleteGoal = async (goalId: string) => { + await api.delete(`/api/v1/goals/${goalId}`) + goals.value = goals.value.filter(g => g.id !== goalId) + if (currentGoal.value?.id === goalId) currentGoal.value = null + } + + const decomposeGoal = async (goalId: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/decompose`) + return response.data + } + + const startGoal = async (goalId: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/start`) + if (currentGoal.value?.id === goalId) currentGoal.value = response.data + return response.data + } + + const pauseGoal = async (goalId: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/pause`) + if (currentGoal.value?.id === goalId) currentGoal.value = response.data + return response.data + } + + const resumeGoal = async (goalId: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/resume`) + if (currentGoal.value?.id === goalId) currentGoal.value = response.data + return response.data + } + + const executeGoalAsync = async (goalId: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/execute-async`) + return response.data + } + + const replanGoal = async (goalId: string) => { + const response = await api.post(`/api/v1/goals/${goalId}/replan`) + return response.data + } + + // ─── Tasks ─── + + const fetchTaskTree = async (goalId: string) => { + const response = await api.get(`/api/v1/goals/${goalId}/tasks`) + taskTree.value = response.data + return response.data + } + + const fetchTasks = async (options?: { goal_id?: string; status?: string; limit?: number }) => { + const params: any = {} + if (options?.goal_id) params.goal_id = options.goal_id + if (options?.status) params.status = options.status + if (options?.limit) params.limit = options.limit + const response = await api.get('/api/v1/tasks', { params }) + tasks.value = response.data + return response.data + } + + const fetchTask = async (taskId: string) => { + const response = await api.get(`/api/v1/tasks/${taskId}`) + return response.data + } + + const updateTask = async (taskId: string, data: Partial) => { + const response = await api.put(`/api/v1/tasks/${taskId}`, data) + return response.data + } + + const approveTask = async (taskId: string) => { + const response = await api.post(`/api/v1/tasks/${taskId}/approve`) + return response.data + } + + const rejectTask = async (taskId: string) => { + const response = await api.post(`/api/v1/tasks/${taskId}/reject`) + return response.data + } + + const checkTaskDeps = async (taskId: string) => { + const response = await api.get(`/api/v1/tasks/${taskId}/check-dependencies`) + return response.data + } + + return { + goals, currentGoal, taskTree, tasks, loading, + fetchGoals, fetchGoal, createGoal, updateGoal, deleteGoal, + decomposeGoal, startGoal, pauseGoal, resumeGoal, executeGoalAsync, replanGoal, + fetchTaskTree, fetchTasks, fetchTask, updateTask, + approveTask, rejectTask, checkTaskDeps, + } +}) diff --git a/frontend/src/views/DigitalEmployeeFactory.vue b/frontend/src/views/DigitalEmployeeFactory.vue new file mode 100644 index 0000000..9142e60 --- /dev/null +++ b/frontend/src/views/DigitalEmployeeFactory.vue @@ -0,0 +1,210 @@ + + + + + 数字员工工厂 + 创建目标,由 Main Agent 自主分解任务、调度执行、追踪进度 + + + + 创建目标 + + + + + + + 创建第一个目标 + + + + + + + + {{ statusLabel(goal.status) }} + P{{ goal.priority }} + + + {{ goal.title }} + {{ goal.description || '无描述' }} + + + 截止: {{ goal.deadline ? new Date(goal.deadline).toLocaleDateString() : '无期限' }} + 创建: {{ new Date(goal.created_at).toLocaleDateString() }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 创建并开始 + + + + + + + + diff --git a/frontend/src/views/GoalDetail.vue b/frontend/src/views/GoalDetail.vue new file mode 100644 index 0000000..53f2771 --- /dev/null +++ b/frontend/src/views/GoalDetail.vue @@ -0,0 +1,263 @@ + + + + + 返回 + + {{ goal?.title }} + {{ statusLabel(goal?.status) }} + + + 暂停 + 恢复 + 重新规划 + 删除 + + + + + + + + 优先级: P{{ goal?.priority }} + 截止: {{ goal?.deadline ? new Date(goal.deadline).toLocaleDateString() : '无期限' }} + 创建: {{ goal?.created_at ? new Date(goal.created_at).toLocaleDateString() : '-' }} + + + + + + 目标描述 + {{ goal.description }} + + + + + + + 任务列表 ({{ tasks.length }}) + 刷新 + + + + + + + + + + + + + {{ taskStatusLabel(task.status) }} + + {{ task.title }} + + + + {{ task.assigned_agent_name }} + + + 依赖 {{ task.depends_on.length }} 项 + + P{{ task.priority }} + + + {{ task.description }} + + {{ task.error_message }} + + + + + {{ formatResult(task.result) }} + + + + + + + + + + + +
创建目标,由 Main Agent 自主分解任务、调度执行、追踪进度
{{ goal.description || '无描述' }}
{{ goal.description }}
{{ formatResult(task.result) }}