feat: enhance agent orchestration, knowledge flow and UI refinements
This commit is contained in:
@@ -6,6 +6,9 @@ from app.routers.forum import router as forum_router
|
||||
from app.routers.graph import router as graph_router
|
||||
from app.routers.agent import router as agent_router
|
||||
from app.routers.todo import router as todo_router
|
||||
from app.routers.reminder import router as reminder_router
|
||||
from app.routers.goal import router as goal_router
|
||||
from app.routers.schedule_center import router as schedule_center_router
|
||||
from app.routers.settings import router as settings_router
|
||||
from app.routers.folder import router as folder_router
|
||||
from app.routers.skill import router as skill_router
|
||||
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.database import get_db
|
||||
from app.models.agent import Agent
|
||||
from app.models.skill import Skill
|
||||
from app.models.user import User
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas.agent import AgentCreate, AgentOut, AgentStats, AgentConfigUpdate, AgentConfigOut
|
||||
@@ -13,9 +14,9 @@ _agent_call_counts: dict[str, int] = {}
|
||||
_agent_current_tasks: dict[str, str | None] = {}
|
||||
_agent_statuses: dict[str, str] = {}
|
||||
|
||||
DEFAULT_AGENT_ROLES = ["master", "planner", "executor", "librarian", "analyst"]
|
||||
DEFAULT_AGENT_ROLES = ["master", "schedule_planner", "executor", "librarian", "analyst"]
|
||||
SUB_COMMANDERS_BY_ROLE = {
|
||||
"planner": ["planner_scope", "planner_steps"],
|
||||
"schedule_planner": ["schedule_analysis", "schedule_planning"],
|
||||
"executor": ["executor_tasks", "executor_forum"],
|
||||
"librarian": ["librarian_retrieval", "librarian_graph"],
|
||||
"analyst": ["analyst_progress", "analyst_insights"],
|
||||
@@ -88,10 +89,10 @@ async def get_agent_config(
|
||||
agent = result.scalar_one_or_none()
|
||||
|
||||
if not agent:
|
||||
from app.agents.prompts import MASTER_SYSTEM_PROMPT, PLANNER_SYSTEM_PROMPT, EXECUTOR_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT, ANALYST_SYSTEM_PROMPT
|
||||
from app.agents.prompts import MASTER_SYSTEM_PROMPT, SCHEDULE_PLANNER_SYSTEM_PROMPT, EXECUTOR_SYSTEM_PROMPT, LIBRARIAN_SYSTEM_PROMPT, ANALYST_SYSTEM_PROMPT
|
||||
defaults = {
|
||||
"master": ("JARVIS", "主控制核心", MASTER_SYSTEM_PROMPT),
|
||||
"planner": ("PLANNER", "规划专家", PLANNER_SYSTEM_PROMPT),
|
||||
"schedule_planner": ("SCHEDULE PLANNER", "日程规划师", SCHEDULE_PLANNER_SYSTEM_PROMPT),
|
||||
"executor": ("EXECUTOR", "执行专家", EXECUTOR_SYSTEM_PROMPT),
|
||||
"librarian": ("LIBRARIAN", "知识管理员", LIBRARIAN_SYSTEM_PROMPT),
|
||||
"analyst": ("ANALYST", "数据分析师", ANALYST_SYSTEM_PROMPT),
|
||||
@@ -107,6 +108,7 @@ async def get_agent_config(
|
||||
system_prompt=prompt,
|
||||
enabled=True,
|
||||
is_active=True,
|
||||
selected_skill_ids=[],
|
||||
)
|
||||
return AgentConfigOut(
|
||||
id=agent.role,
|
||||
@@ -116,6 +118,7 @@ async def get_agent_config(
|
||||
system_prompt=agent.system_prompt,
|
||||
enabled=agent.is_active,
|
||||
is_active=agent.is_active,
|
||||
selected_skill_ids=agent.selected_skill_ids or [],
|
||||
)
|
||||
|
||||
|
||||
@@ -141,6 +144,19 @@ async def update_agent_config(
|
||||
if data.enabled is not None:
|
||||
agent.is_active = data.enabled
|
||||
_agent_statuses[agent_id] = "disabled" if not data.enabled else "idle"
|
||||
if data.selected_skill_ids is not None:
|
||||
if data.selected_skill_ids:
|
||||
result = await db.execute(
|
||||
select(Skill.id).where(
|
||||
Skill.id.in_(data.selected_skill_ids),
|
||||
Skill.owner_id == current_user.id,
|
||||
)
|
||||
)
|
||||
allowed_skill_ids = set(result.scalars().all())
|
||||
invalid_skill_ids = [skill_id for skill_id in data.selected_skill_ids if skill_id not in allowed_skill_ids]
|
||||
if invalid_skill_ids:
|
||||
raise HTTPException(status_code=400, detail="存在无效的技能绑定")
|
||||
agent.selected_skill_ids = data.selected_skill_ids
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(agent)
|
||||
@@ -152,6 +168,7 @@ async def update_agent_config(
|
||||
system_prompt=agent.system_prompt,
|
||||
enabled=agent.is_active,
|
||||
is_active=agent.is_active,
|
||||
selected_skill_ids=agent.selected_skill_ids or [],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import UserCreate, UserOut, Token
|
||||
from app.services.admin_bootstrap_service import ensure_builtin_skills
|
||||
from app.services.auth_service import verify_password, get_password_hash, create_access_token, decode_token
|
||||
from app.config import settings
|
||||
|
||||
@@ -58,6 +59,7 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名或邮箱已被注册")
|
||||
await db.refresh(user)
|
||||
await ensure_builtin_skills(db)
|
||||
return user
|
||||
|
||||
|
||||
@@ -97,10 +99,12 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSessi
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名、邮箱或密码错误")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="用户已被禁用")
|
||||
await ensure_builtin_skills(db)
|
||||
access_token = create_access_token(data={"sub": user.id})
|
||||
return Token(access_token=access_token)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def get_me(current_user: User = Depends(get_current_user)):
|
||||
async def get_me(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
await ensure_builtin_skills(db)
|
||||
return current_user
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
@@ -92,13 +93,16 @@ async def chat(
|
||||
):
|
||||
"""简单版对话(非流式)"""
|
||||
agent_svc = AgentService(db)
|
||||
conv_id, msg_id, content, model_name = await agent_svc.chat_simple(
|
||||
user_id=current_user.id,
|
||||
message=data.message,
|
||||
conversation_id=data.conversation_id,
|
||||
file_ids=data.file_ids,
|
||||
model_name=data.model_name,
|
||||
)
|
||||
try:
|
||||
conv_id, msg_id, content, model_name = await agent_svc.chat_simple(
|
||||
user_id=current_user.id,
|
||||
message=data.message,
|
||||
conversation_id=data.conversation_id,
|
||||
file_ids=data.file_ids,
|
||||
model_name=data.model_name,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
# 更新对话消息计数
|
||||
result = await db.execute(select(Conversation).where(Conversation.id == conv_id))
|
||||
@@ -126,13 +130,17 @@ async def chat_stream(
|
||||
agent_svc = AgentService(db)
|
||||
|
||||
async def stream_generator():
|
||||
conv_id, msg_id, stream = await agent_svc.chat(
|
||||
user_id=current_user.id,
|
||||
message=data.message,
|
||||
conversation_id=data.conversation_id,
|
||||
file_ids=data.file_ids,
|
||||
model_name=data.model_name,
|
||||
)
|
||||
try:
|
||||
conv_id, msg_id, stream = await agent_svc.chat(
|
||||
user_id=current_user.id,
|
||||
message=data.message,
|
||||
conversation_id=data.conversation_id,
|
||||
file_ids=data.file_ids,
|
||||
model_name=data.model_name,
|
||||
)
|
||||
except ValueError as exc:
|
||||
yield f"event: error\ndata: {json.dumps({'error': str(exc)}, ensure_ascii=False)}\n\n"
|
||||
return
|
||||
|
||||
yield f"event: metadata\ndata: {json.dumps({'conversation_id': conv_id, 'message_id': msg_id})}\n\n"
|
||||
|
||||
|
||||
92
backend/app/routers/goal.py
Normal file
92
backend/app/routers/goal.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.goal import Goal
|
||||
from app.models.user import User
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas.goal import GoalCreate, GoalListOut, GoalOut, GoalUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/goals", tags=["目标"])
|
||||
|
||||
|
||||
@router.get("", response_model=GoalListOut)
|
||||
async def list_goals(
|
||||
date_str: str = Query(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
target_date = date.fromisoformat(date_str).isoformat()
|
||||
query = (
|
||||
select(Goal)
|
||||
.where(Goal.user_id == current_user.id)
|
||||
.where(Goal.goal_date == target_date)
|
||||
.order_by(Goal.created_at.desc())
|
||||
)
|
||||
items = (await db.execute(query)).scalars().all()
|
||||
return GoalListOut(items=items)
|
||||
|
||||
|
||||
@router.post("", response_model=GoalOut, status_code=201)
|
||||
async def create_goal(
|
||||
data: GoalCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
goal = Goal(
|
||||
user_id=current_user.id,
|
||||
title=data.title,
|
||||
note=data.note,
|
||||
goal_date=data.goal_date.isoformat(),
|
||||
status=data.status,
|
||||
)
|
||||
db.add(goal)
|
||||
await db.commit()
|
||||
await db.refresh(goal)
|
||||
return goal
|
||||
|
||||
|
||||
@router.patch("/{goal_id}", response_model=GoalOut)
|
||||
async def update_goal(
|
||||
goal_id: str,
|
||||
data: GoalUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Goal).where(Goal.id == goal_id, Goal.user_id == current_user.id)
|
||||
)
|
||||
goal = result.scalar_one_or_none()
|
||||
if not goal:
|
||||
raise HTTPException(status_code=404, detail="目标不存在")
|
||||
|
||||
payload = data.model_dump(exclude_none=True)
|
||||
if "goal_date" in payload:
|
||||
payload["goal_date"] = payload["goal_date"].isoformat()
|
||||
|
||||
for field, value in payload.items():
|
||||
setattr(goal, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(goal)
|
||||
return goal
|
||||
|
||||
|
||||
@router.delete("/{goal_id}", status_code=204)
|
||||
async def delete_goal(
|
||||
goal_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Goal).where(Goal.id == goal_id, Goal.user_id == current_user.id)
|
||||
)
|
||||
goal = result.scalar_one_or_none()
|
||||
if not goal:
|
||||
raise HTTPException(status_code=404, detail="目标不存在")
|
||||
|
||||
await db.delete(goal)
|
||||
await db.commit()
|
||||
90
backend/app/routers/reminder.py
Normal file
90
backend/app/routers/reminder.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.reminder import Reminder
|
||||
from app.models.user import User
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas.reminder import ReminderCreate, ReminderListOut, ReminderOut, ReminderUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/reminders", tags=["提醒"])
|
||||
|
||||
|
||||
@router.get("", response_model=ReminderListOut)
|
||||
async def list_reminders(
|
||||
date_str: str = Query(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
target_date = date.fromisoformat(date_str)
|
||||
start = datetime.combine(target_date, datetime.min.time())
|
||||
end = datetime.combine(target_date, datetime.max.time())
|
||||
query = (
|
||||
select(Reminder)
|
||||
.where(Reminder.user_id == current_user.id)
|
||||
.where(Reminder.reminder_at >= start)
|
||||
.where(Reminder.reminder_at <= end)
|
||||
.order_by(Reminder.reminder_at.asc(), Reminder.created_at.asc())
|
||||
)
|
||||
items = (await db.execute(query)).scalars().all()
|
||||
return ReminderListOut(items=items)
|
||||
|
||||
|
||||
@router.post("", response_model=ReminderOut, status_code=201)
|
||||
async def create_reminder(
|
||||
data: ReminderCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
reminder = Reminder(
|
||||
user_id=current_user.id,
|
||||
title=data.title,
|
||||
note=data.note,
|
||||
reminder_at=data.reminder_at,
|
||||
)
|
||||
db.add(reminder)
|
||||
await db.commit()
|
||||
await db.refresh(reminder)
|
||||
return reminder
|
||||
|
||||
|
||||
@router.patch("/{reminder_id}", response_model=ReminderOut)
|
||||
async def update_reminder(
|
||||
reminder_id: str,
|
||||
data: ReminderUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Reminder).where(Reminder.id == reminder_id, Reminder.user_id == current_user.id)
|
||||
)
|
||||
reminder = result.scalar_one_or_none()
|
||||
if not reminder:
|
||||
raise HTTPException(status_code=404, detail="提醒不存在")
|
||||
|
||||
for field, value in data.model_dump(exclude_none=True).items():
|
||||
setattr(reminder, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(reminder)
|
||||
return reminder
|
||||
|
||||
|
||||
@router.delete("/{reminder_id}", status_code=204)
|
||||
async def delete_reminder(
|
||||
reminder_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Reminder).where(Reminder.id == reminder_id, Reminder.user_id == current_user.id)
|
||||
)
|
||||
reminder = result.scalar_one_or_none()
|
||||
if not reminder:
|
||||
raise HTTPException(status_code=404, detail="提醒不存在")
|
||||
|
||||
await db.delete(reminder)
|
||||
await db.commit()
|
||||
160
backend/app/routers/schedule_center.py
Normal file
160
backend/app/routers/schedule_center.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from calendar import monthrange
|
||||
from datetime import UTC, date, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.goal import Goal
|
||||
from app.models.reminder import Reminder
|
||||
from app.models.task import Task, TaskPriority
|
||||
from app.models.todo import DailyTodo
|
||||
from app.models.user import User
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas.schedule_center import (
|
||||
ScheduleCenterDateOut,
|
||||
ScheduleCenterDaySummary,
|
||||
ScheduleCenterMonthOut,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/schedule-center", tags=["调度中心"])
|
||||
|
||||
|
||||
def _build_summary(
|
||||
target_date: str,
|
||||
todos: list[DailyTodo],
|
||||
tasks: list[Task],
|
||||
reminders: list[Reminder],
|
||||
goals: list[Goal],
|
||||
) -> ScheduleCenterDaySummary:
|
||||
return ScheduleCenterDaySummary(
|
||||
date=target_date,
|
||||
todo_total=len(todos),
|
||||
todo_completed=sum(1 for item in todos if item.is_completed),
|
||||
task_due_total=len(tasks),
|
||||
high_priority_total=sum(1 for item in tasks if item.priority in {TaskPriority.HIGH, TaskPriority.URGENT}),
|
||||
reminder_total=len(reminders),
|
||||
goal_total=len(goals),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/month", response_model=ScheduleCenterMonthOut)
|
||||
async def get_month_schedule(
|
||||
year: int = Query(..., ge=2000, le=2100),
|
||||
month: int = Query(..., ge=1, le=12),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
month_start = date(year, month, 1)
|
||||
days_in_month = monthrange(month_start.year, month_start.month)[1]
|
||||
start_key = month_start.isoformat()
|
||||
end_key = month_start.replace(day=days_in_month).isoformat()
|
||||
start_dt = datetime.combine(month_start, datetime.min.time())
|
||||
end_dt = datetime.combine(month_start.replace(day=days_in_month), datetime.max.time())
|
||||
|
||||
todos = (await db.execute(
|
||||
select(DailyTodo).where(DailyTodo.user_id == current_user.id, DailyTodo.todo_date >= start_key, DailyTodo.todo_date <= end_key)
|
||||
)).scalars().all()
|
||||
tasks = (await db.execute(
|
||||
select(Task).where(
|
||||
Task.user_id == current_user.id,
|
||||
Task.due_date.is_not(None),
|
||||
Task.due_date >= start_dt,
|
||||
Task.due_date <= end_dt,
|
||||
)
|
||||
)).scalars().all()
|
||||
reminders = (await db.execute(
|
||||
select(Reminder).where(
|
||||
Reminder.user_id == current_user.id,
|
||||
Reminder.reminder_at >= start_dt,
|
||||
Reminder.reminder_at <= end_dt,
|
||||
)
|
||||
)).scalars().all()
|
||||
goals = (await db.execute(
|
||||
select(Goal).where(Goal.user_id == current_user.id, Goal.goal_date >= start_key, Goal.goal_date <= end_key)
|
||||
)).scalars().all()
|
||||
|
||||
todo_map: dict[str, list[DailyTodo]] = {}
|
||||
for item in todos:
|
||||
todo_map.setdefault(item.todo_date, []).append(item)
|
||||
|
||||
task_map: dict[str, list[Task]] = {}
|
||||
for item in tasks:
|
||||
key = item.due_date.date().isoformat()
|
||||
task_map.setdefault(key, []).append(item)
|
||||
|
||||
reminder_map: dict[str, list[Reminder]] = {}
|
||||
for item in reminders:
|
||||
key = item.reminder_at.date().isoformat()
|
||||
reminder_map.setdefault(key, []).append(item)
|
||||
|
||||
goal_map: dict[str, list[Goal]] = {}
|
||||
for item in goals:
|
||||
goal_map.setdefault(item.goal_date, []).append(item)
|
||||
|
||||
days = []
|
||||
for day in range(1, days_in_month + 1):
|
||||
date_key = month_start.replace(day=day).isoformat()
|
||||
days.append(_build_summary(
|
||||
date_key,
|
||||
todo_map.get(date_key, []),
|
||||
task_map.get(date_key, []),
|
||||
reminder_map.get(date_key, []),
|
||||
goal_map.get(date_key, []),
|
||||
))
|
||||
|
||||
return ScheduleCenterMonthOut(month=f"{year:04d}-{month:02d}", days=days)
|
||||
|
||||
|
||||
@router.get("/date", response_model=ScheduleCenterDateOut)
|
||||
async def get_date_schedule(
|
||||
date_str: date = Query(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
target_date = date_str
|
||||
start_dt = datetime.combine(target_date, datetime.min.time())
|
||||
end_dt = datetime.combine(target_date, datetime.max.time())
|
||||
date_key = target_date.isoformat()
|
||||
|
||||
todos = (await db.execute(
|
||||
select(DailyTodo)
|
||||
.where(DailyTodo.user_id == current_user.id, DailyTodo.todo_date == date_key)
|
||||
.order_by(DailyTodo.created_at.desc())
|
||||
)).scalars().all()
|
||||
tasks = (await db.execute(
|
||||
select(Task)
|
||||
.where(
|
||||
Task.user_id == current_user.id,
|
||||
Task.due_date.is_not(None),
|
||||
Task.due_date >= start_dt,
|
||||
Task.due_date <= end_dt,
|
||||
)
|
||||
.order_by(Task.created_at.desc())
|
||||
)).scalars().all()
|
||||
reminders = (await db.execute(
|
||||
select(Reminder)
|
||||
.where(
|
||||
Reminder.user_id == current_user.id,
|
||||
Reminder.reminder_at >= start_dt,
|
||||
Reminder.reminder_at <= end_dt,
|
||||
)
|
||||
.order_by(Reminder.reminder_at.asc(), Reminder.created_at.asc())
|
||||
)).scalars().all()
|
||||
goals = (await db.execute(
|
||||
select(Goal)
|
||||
.where(Goal.user_id == current_user.id, Goal.goal_date == date_key)
|
||||
.order_by(Goal.created_at.desc())
|
||||
)).scalars().all()
|
||||
|
||||
summary = _build_summary(date_key, todos, tasks, reminders, goals)
|
||||
return ScheduleCenterDateOut(
|
||||
date=date_key,
|
||||
todos=todos,
|
||||
tasks=tasks,
|
||||
reminders=reminders,
|
||||
goals=goals,
|
||||
summary=summary,
|
||||
generated_at=datetime.now(UTC),
|
||||
)
|
||||
@@ -1,11 +1,13 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.models.skill import Skill
|
||||
from app.models.user import User
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas.skill import SkillCreate, SkillOut, SkillUpdate
|
||||
from app.services.admin_bootstrap_service import ensure_builtin_skills
|
||||
from app.services.skill_service import SkillService
|
||||
|
||||
router = APIRouter(prefix="/api/skills", tags=["Skill"])
|
||||
|
||||
@@ -37,13 +39,23 @@ async def create_skill(
|
||||
|
||||
@router.get("", response_model=list[SkillOut])
|
||||
async def list_skills(
|
||||
agent_type: str | None = Query(default=None),
|
||||
visibility: str | None = Query(default=None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(Skill).where(Skill.owner_id == current_user.id).order_by(Skill.created_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
service = SkillService(db)
|
||||
return await service.list_for_user(current_user.id, agent_type=agent_type, visibility=visibility)
|
||||
|
||||
|
||||
@router.post("/bootstrap-builtin", response_model=list[SkillOut])
|
||||
async def bootstrap_builtin_skills(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await ensure_builtin_skills(db, preferred_owner_id=current_user.id)
|
||||
service = SkillService(db)
|
||||
return await service.list_for_user(current_user.id)
|
||||
|
||||
|
||||
@router.get("/{skill_id}", response_model=SkillOut)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from datetime import UTC, date, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import desc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from app.database import get_db
|
||||
from app.models.task import Task, TaskStatus
|
||||
from app.models.user import User
|
||||
@@ -13,12 +15,28 @@ router = APIRouter(prefix="/api/tasks", tags=["看板"])
|
||||
@router.get("", response_model=list[TaskOut])
|
||||
async def list_tasks(
|
||||
status: TaskStatus | None = None,
|
||||
due_date: date | None = Query(default=None),
|
||||
date_from: date | None = Query(default=None),
|
||||
date_to: date | None = Query(default=None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = select(Task).where(Task.user_id == current_user.id)
|
||||
if status:
|
||||
query = query.where(Task.status == status)
|
||||
if due_date:
|
||||
start = datetime.combine(due_date, datetime.min.time())
|
||||
end = datetime.combine(due_date, datetime.max.time())
|
||||
query = query.where(Task.due_date.is_not(None), Task.due_date >= start, Task.due_date <= end)
|
||||
else:
|
||||
start = datetime.combine(date_from, datetime.min.time()) if date_from else None
|
||||
end = datetime.combine(date_to, datetime.max.time()) if date_to else None
|
||||
if start and end and start > end:
|
||||
raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期")
|
||||
if start is not None:
|
||||
query = query.where(Task.due_date.is_not(None), Task.due_date >= start)
|
||||
if end is not None:
|
||||
query = query.where(Task.due_date.is_not(None), Task.due_date <= end)
|
||||
query = query.order_by(desc(Task.created_at))
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
@@ -64,10 +82,10 @@ async def update_task(
|
||||
if field == "tags":
|
||||
setattr(task, field, json.dumps(value))
|
||||
elif field == "status" and value == TaskStatus.DONE:
|
||||
from datetime import UTC, datetime
|
||||
task.completed_at = datetime.now(UTC)
|
||||
setattr(task, field, value)
|
||||
else:
|
||||
elif field == "status":
|
||||
task.completed_at = None
|
||||
setattr(task, field, value)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from datetime import UTC, date, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from datetime import date
|
||||
from app.database import get_db
|
||||
from app.models.todo import DailyTodo, TodoSource
|
||||
from app.models.user import User
|
||||
@@ -52,7 +53,7 @@ async def create_todo(
|
||||
user_id=current_user.id,
|
||||
title=data.title,
|
||||
source=TodoSource.MANUAL,
|
||||
todo_date=date.today().isoformat(),
|
||||
todo_date=(data.todo_date or date.today()).isoformat(),
|
||||
)
|
||||
db.add(todo)
|
||||
await db.commit()
|
||||
@@ -74,14 +75,11 @@ async def update_todo(
|
||||
if not todo:
|
||||
raise HTTPException(status_code=404, detail="待办不存在")
|
||||
|
||||
# 历史日期不允许修改
|
||||
if todo.todo_date != date.today().isoformat():
|
||||
raise HTTPException(status_code=403, detail="历史待办不可修改")
|
||||
|
||||
if data.title is not None:
|
||||
todo.title = data.title
|
||||
if data.todo_date is not None:
|
||||
todo.todo_date = data.todo_date.isoformat()
|
||||
if data.is_completed is not None:
|
||||
from datetime import UTC, datetime
|
||||
todo.is_completed = data.is_completed
|
||||
todo.completed_at = datetime.now(UTC) if data.is_completed else None
|
||||
|
||||
@@ -102,9 +100,6 @@ async def delete_todo(
|
||||
todo = result.scalar_one_or_none()
|
||||
if not todo:
|
||||
raise HTTPException(status_code=404, detail="待办不存在")
|
||||
if todo.todo_date != date.today().isoformat():
|
||||
raise HTTPException(status_code=403, detail="历史待办不可删除")
|
||||
|
||||
await db.delete(todo)
|
||||
await db.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user