-
Notifications
You must be signed in to change notification settings - Fork 33
feat: Added a shared task model and updated user relationships and co… #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """add shared_tasks table for task sharing | ||
|
|
||
| Revision ID: 2b3c4d5e6f7g | ||
| Revises: add_subtask_attachments | ||
| Create Date: 2025-12-04 12:00:00.000000+08:00 | ||
|
|
||
| """ | ||
| from typing import Sequence, Union | ||
|
|
||
| from alembic import op | ||
| import sqlalchemy as sa | ||
|
|
||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision: str = '2b3c4d5e6f7g' | ||
| down_revision: Union[str, Sequence[str], None] = 'add_subtask_attachments' | ||
| branch_labels: Union[str, Sequence[str], None] = None | ||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| """Add shared_tasks table for task sharing functionality.""" | ||
|
|
||
| # Create shared_tasks table | ||
| op.execute(""" | ||
| CREATE TABLE IF NOT EXISTS shared_tasks ( | ||
| id INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', | ||
| user_id INT NOT NULL DEFAULT 0 COMMENT '当前用户ID', | ||
| original_user_id INT NOT NULL DEFAULT 0 COMMENT '原始任务所有者用户ID', | ||
| original_task_id INT NOT NULL DEFAULT 0 COMMENT '原始任务ID', | ||
| copied_task_id INT NOT NULL DEFAULT 0 COMMENT '复制后的任务ID', | ||
| is_active BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否激活', | ||
| created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| PRIMARY KEY (id), | ||
| KEY idx_shared_tasks_id (id), | ||
| KEY idx_shared_tasks_user_id (user_id), | ||
| KEY idx_shared_tasks_original_user_id (original_user_id), | ||
| KEY idx_shared_tasks_original_task_id (original_task_id), | ||
| KEY idx_shared_tasks_copied_task_id (copied_task_id), | ||
| UNIQUE KEY uniq_user_original_task (user_id, original_task_id) | ||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | ||
| """) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| """Remove shared_tasks table.""" | ||
| op.execute("DROP TABLE IF EXISTS shared_tasks") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,13 @@ | |
| from app.core.config import settings | ||
| from app.models.subtask import Subtask, SubtaskRole, SubtaskStatus | ||
| from app.models.user import User | ||
| from app.schemas.shared_task import ( | ||
| JoinSharedTaskRequest, | ||
| JoinSharedTaskResponse, | ||
| PublicSharedTaskResponse, | ||
| TaskShareInfo, | ||
| TaskShareResponse, | ||
| ) | ||
| from app.schemas.task import ( | ||
| TaskCreate, | ||
| TaskDetail, | ||
|
|
@@ -23,6 +30,7 @@ | |
| TaskUpdate, | ||
| ) | ||
| from app.services.adapters.task_kinds import task_kinds_service | ||
| from app.services.shared_task import shared_task_service | ||
|
|
||
| router = APIRouter() | ||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -282,8 +290,7 @@ async def cancel_task( | |
| except Exception as e: | ||
| logger.error( | ||
| f"Failed to update Chat Shell task {task_id} status: {str(e)}" | ||
| ) | ||
|
|
||
| ) | ||
| return {"message": "Chat stopped successfully", "status": "COMPLETED"} | ||
| else: | ||
| # No running subtask found, just mark task as completed | ||
|
|
@@ -323,3 +330,110 @@ async def cancel_task( | |
| background_tasks.add_task(call_executor_cancel, task_id) | ||
|
|
||
| return {"message": "Cancel request accepted", "status": "CANCELLING"} | ||
|
|
||
|
|
||
| @router.post("/{task_id}/share", response_model=TaskShareResponse) | ||
| def share_task( | ||
| task_id: int, | ||
| current_user: User = Depends(security.get_current_user), | ||
| db: Session = Depends(get_db), | ||
| ): | ||
| """ | ||
| Generate a share link for a task. | ||
| The share link allows others to view the task history and copy it to their task list. | ||
| """ | ||
| # Validate that the task belongs to the current user | ||
| if not shared_task_service.validate_task_exists( | ||
| db=db, task_id=task_id, user_id=current_user.id | ||
| ): | ||
| raise HTTPException( | ||
| status_code=404, detail="Task not found or you don't have permission" | ||
| ) | ||
|
|
||
| return shared_task_service.share_task( | ||
| db=db, task_id=task_id, user_id=current_user.id | ||
| ) | ||
|
|
||
|
|
||
| @router.get("/share/info", response_model=TaskShareInfo) | ||
| def get_task_share_info( | ||
| share_token: str = Query(..., description="Share token from URL"), | ||
| db: Session = Depends(get_db), | ||
| ): | ||
| """ | ||
| Get task share information from share token. | ||
| This endpoint doesn't require authentication, so anyone with the link can view. | ||
| """ | ||
| return shared_task_service.get_share_info(db=db, share_token=share_token) | ||
|
|
||
|
|
||
| @router.get("/share/public", response_model=PublicSharedTaskResponse) | ||
| def get_public_shared_task( | ||
| token: str = Query(..., description="Share token from URL"), | ||
| db: Session = Depends(get_db), | ||
| ): | ||
| """ | ||
| Get public shared task data for read-only viewing. | ||
| This endpoint doesn't require authentication - anyone with the link can view. | ||
| Only returns public data (no sensitive information like team config, bot details, etc.) | ||
| """ | ||
| return shared_task_service.get_public_shared_task(db=db, share_token=token) | ||
|
|
||
|
|
||
| @router.post("/share/join", response_model=JoinSharedTaskResponse) | ||
| def join_shared_task( | ||
| request: JoinSharedTaskRequest, | ||
| current_user: User = Depends(security.get_current_user), | ||
| db: Session = Depends(get_db), | ||
| ): | ||
| """ | ||
| Copy a shared task to the current user's task list. | ||
| This creates a new task with all the subtasks (messages) from the shared task. | ||
| """ | ||
| from app.models.kind import Kind | ||
|
|
||
| # If team_id is provided, validate it belongs to the user | ||
| if request.team_id: | ||
| user_team = ( | ||
| db.query(Kind) | ||
| .filter( | ||
| Kind.user_id == current_user.id, | ||
| Kind.kind == "Team", | ||
| Kind.id == request.team_id, | ||
| Kind.is_active == True, | ||
| ) | ||
| .first() | ||
| ) | ||
|
|
||
| if not user_team: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="Invalid team_id or team does not belong to you", | ||
| ) | ||
| else: | ||
| # Get user's first active team if not specified | ||
| user_team = ( | ||
| db.query(Kind) | ||
| .filter( | ||
| Kind.user_id == current_user.id, | ||
| Kind.kind == "Team", | ||
| Kind.is_active == True, | ||
| ) | ||
| .first() | ||
| ) | ||
|
|
||
| if not user_team: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="You need to have at least one team to copy a shared task", | ||
| ) | ||
|
|
||
| return shared_task_service.join_shared_task( | ||
| db=db, | ||
| share_token=request.share_token, | ||
| user_id=current_user.id, | ||
| team_id=user_team.id, | ||
| model_id=request.model_id, | ||
| force_override_bot_model=request.force_override_bot_model or False, | ||
| ) | ||
|
Comment on lines
+335
to
+438
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add return type hints and avoid magic HTTP status numbers on new endpoints The new sharing endpoints look logically correct and delegate cleanly to To align with the Python coding guidelines (type hints + avoiding magic numbers), consider updating all four endpoints like this (pattern shown; apply similarly to the others): -@router.post("/{task_id}/share", response_model=TaskShareResponse)
-def share_task(
+@router.post("/{task_id}/share", response_model=TaskShareResponse)
+def share_task(
task_id: int,
current_user: User = Depends(security.get_current_user),
db: Session = Depends(get_db),
-):
+ ) -> TaskShareResponse:
@@
- if not shared_task_service.validate_task_exists(
- db=db, task_id=task_id, user_id=current_user.id
- ):
- raise HTTPException(
- status_code=404, detail="Task not found or you don't have permission"
- )
+ if not shared_task_service.validate_task_exists(
+ db=db, task_id=task_id, user_id=current_user.id
+ ):
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Task not found or you don't have permission",
+ )
@@
-@router.get("/share/info", response_model=TaskShareInfo)
-def get_task_share_info(
+@router.get("/share/info", response_model=TaskShareInfo)
+def get_task_share_info(
share_token: str = Query(..., description="Share token from URL"),
db: Session = Depends(get_db),
-):
+ ) -> TaskShareInfo:
@@
-@router.get("/share/public", response_model=PublicSharedTaskResponse)
-def get_public_shared_task(
- token: str = Query(..., description="Share token from URL"),
- db: Session = Depends(get_db),
-):
+@router.get("/share/public", response_model=PublicSharedTaskResponse)
+def get_public_shared_task(
+ token: str = Query(..., description="Share token from URL"),
+ db: Session = Depends(get_db),
+) -> PublicSharedTaskResponse:
@@
-@router.post("/share/join", response_model=JoinSharedTaskResponse)
-def join_shared_task(
+@router.post("/share/join", response_model=JoinSharedTaskResponse)
+def join_shared_task(
request: JoinSharedTaskRequest,
current_user: User = Depends(security.get_current_user),
db: Session = Depends(get_db),
-):
+ ) -> JoinSharedTaskResponse:
@@
- raise HTTPException(
- status_code=400,
- detail="Invalid team_id or team does not belong to you",
- )
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Invalid team_id or team does not belong to you",
+ )
@@
- raise HTTPException(
- status_code=400,
- detail="You need to have at least one team to copy a shared task",
- )
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="You need to have at least one team to copy a shared task",
+ )As per coding guidelines, this keeps the API signatures explicit and removes magic status-code literals. 🧰 Tools🪛 Ruff (0.14.7)338-338: Do not perform function call (B008) 339-339: Do not perform function call (B008) 361-361: Do not perform function call (B008) 373-373: Do not perform function call (B008) 386-386: Do not perform function call (B008) 387-387: Do not perform function call (B008) 403-403: Avoid equality comparisons to Replace with (E712) 420-420: Avoid equality comparisons to Replace with (E712) 🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||||||||||||||||||
| from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, UniqueConstraint | ||||||||||||||||||||||||||
| from sqlalchemy.orm import relationship | ||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||
| from app.db.base import Base | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| class SharedTask(Base): | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| Shared Task model - Records task sharing relationships | ||||||||||||||||||||||||||
| When a user shares a task, the task's content (including subtasks/messages) | ||||||||||||||||||||||||||
| can be copied to another user's task list. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| __tablename__ = "shared_tasks" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| id = Column(Integer, primary_key=True, index=True, autoincrement=True) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # User who joined/copied the shared task | ||||||||||||||||||||||||||
| user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Original user who created/shared the task | ||||||||||||||||||||||||||
| original_user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Original task ID that was shared | ||||||||||||||||||||||||||
| original_task_id = Column(Integer, ForeignKey("kinds.id", ondelete="CASCADE"), nullable=False, index=True) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # New task ID created for the user who joined (copied task) | ||||||||||||||||||||||||||
| copied_task_id = Column(Integer, ForeignKey("kinds.id", ondelete="CASCADE"), nullable=True, index=True) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Whether this share relationship is active | ||||||||||||||||||||||||||
| is_active = Column(Boolean, default=True, nullable=False) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Timestamps | ||||||||||||||||||||||||||
| created_at = Column(DateTime, default=datetime.utcnow, nullable=False) | ||||||||||||||||||||||||||
| updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) | ||||||||||||||||||||||||||
|
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chain🌐 Web query:
💡 Result: Yes — in Python 3.12 datetime.datetime.utcnow() (and utcfromtimestamp()) is deprecated. Use timezone-aware UTC datetimes instead, e.g.: from datetime import datetime, timezone The deprecation is documented in Python 3.12 “What’s New” / deprecations notes. [1][2] Sources: 🏁 Script executed: # First, let's examine the shared_task.py file to understand the current state
find . -path "*/backend/app/models/shared_task.py" -type fRepository: wecode-ai/Wegent Length of output: 95 🏁 Script executed: # Check if the file exists and view its content
cat -n backend/app/models/shared_task.py 2>/dev/null || echo "File not found at expected path, searching..."Repository: wecode-ai/Wegent Length of output: 2529 🏁 Script executed: # Search for shared_task.py file
fd -t f "shared_task.py" backend/Repository: wecode-ai/Wegent Length of output: 164 🏁 Script executed: # Search for datetime.utcnow usage in backend
rg "utcnow" backend/ --type py -B 2 -A 2Repository: wecode-ai/Wegent Length of output: 4168 Replace deprecated
-from datetime import datetime
+from datetime import datetime, timezone
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.base import Base
class SharedTask(Base):
# Timestamps
- created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
- updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
+ created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)
+ updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Relationships | ||||||||||||||||||||||||||
| user = relationship("User", foreign_keys=[user_id], back_populates="shared_tasks") | ||||||||||||||||||||||||||
| original_user = relationship("User", foreign_keys=[original_user_id]) | ||||||||||||||||||||||||||
| original_task = relationship("Kind", foreign_keys=[original_task_id]) | ||||||||||||||||||||||||||
| copied_task = relationship("Kind", foreign_keys=[copied_task_id]) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Unique constraint: one user can only copy the same original task once | ||||||||||||||||||||||||||
| __table_args__ = ( | ||||||||||||||||||||||||||
| UniqueConstraint('user_id', 'original_task_id', name='uq_user_original_task'), | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def __repr__(self): | ||||||||||||||||||||||||||
| return f"<SharedTask(id={self.id}, user_id={self.user_id}, original_task_id={self.original_task_id})>" | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 828
🌐 Web query:
FastAPI route matching order path parameter vs literal routes💡 Result:
Short answer: FastAPI (via Starlette) matches routes in the order they are added. More specific literal routes (e.g. /users/me) must be declared before general path-parameter routes (e.g. /users/{user_id}), otherwise the parameter route will capture the literal. The same applies to catch‑all patterns — place them last. [1][2]
Example: declare @app.get("/users/me") before @app.get("/users/{user_id}"). [1]
Sources: FastAPI Path Parameters docs and Starlette routing notes. [1][2]
🏁 Script executed:
Repository: wecode-ai/Wegent
Length of output: 371
Move
/share/*routes before/{task_id}route.The routes
/share/info(line 350),/share/public(line 362), and/share/join(line 375) must be defined before the/{task_id}route (line 155). FastAPI matches routes in definition order, and the parameterized/{task_id}route will capture "share" as a task_id before the literal/share/*routes are evaluated, preventing those endpoints from being reached.🧰 Tools
🪛 Ruff (0.14.7)
353-353: Do not perform function call
Dependsin argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable(B008)
365-365: Do not perform function call
Dependsin argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable(B008)
🤖 Prompt for AI Agents