1414from app .core .config import settings
1515from app .models .subtask import Subtask , SubtaskRole , SubtaskStatus
1616from app .models .user import User
17+ from app .schemas .shared_task import (
18+ JoinSharedTaskRequest ,
19+ JoinSharedTaskResponse ,
20+ PublicSharedTaskResponse ,
21+ TaskShareInfo ,
22+ TaskShareResponse ,
23+ )
1724from app .schemas .task import (
1825 TaskCreate ,
1926 TaskDetail ,
2330 TaskUpdate ,
2431)
2532from app .services .adapters .task_kinds import task_kinds_service
33+ from app .services .shared_task import shared_task_service
2634
2735router = APIRouter ()
2836logger = logging .getLogger (__name__ )
@@ -49,7 +57,7 @@ async def call_chat_shell_cancel(subtask_id: int, partial_content: str = ""):
4957 """Background task to cancel Chat Shell streaming via session manager"""
5058 try :
5159 from app .services .chat .session_manager import session_manager
52-
60+
5361 success = await session_manager .cancel_stream (subtask_id )
5462 if success :
5563 logger .info (f"Chat Shell stream cancelled successfully for subtask { subtask_id } " )
@@ -190,7 +198,7 @@ async def cancel_task(
190198 """Cancel a running task by calling executor_manager or Chat Shell cancel"""
191199 from app .models .kind import Kind
192200 from app .schemas .kind import Task
193-
201+
194202 # Verify user owns this task
195203 task = task_kinds_service .get_task_detail (
196204 db = db , task_id = task_id , user_id = current_user .id
@@ -228,13 +236,13 @@ async def cancel_task(
228236 )
229237 .first ()
230238 )
231-
239+
232240 if task_kind and task_kind .json :
233241 task_crd = Task .model_validate (task_kind .json )
234242 if task_crd .metadata .labels :
235243 source = task_crd .metadata .labels .get ("source" , "" )
236244 is_chat_shell = source == "chat_shell"
237-
245+
238246 logger .info (f"Task { task_id } is_chat_shell={ is_chat_shell } " )
239247
240248 if is_chat_shell :
@@ -249,11 +257,11 @@ async def cancel_task(
249257 )
250258 .first ()
251259 )
252-
260+
253261 if running_subtask :
254262 # Cancel the Chat Shell stream
255263 background_tasks .add_task (call_chat_shell_cancel , running_subtask .id )
256-
264+
257265 # Update subtask status to COMPLETED (not CANCELLED, to show partial content)
258266 from datetime import datetime
259267 running_subtask .status = SubtaskStatus .COMPLETED
@@ -262,7 +270,7 @@ async def cancel_task(
262270 running_subtask .updated_at = datetime .now ()
263271 running_subtask .error_message = ""
264272 db .commit ()
265-
273+
266274 # Update task status to COMPLETED (not CANCELLING, for Chat Shell)
267275 try :
268276 task_kinds_service .update_task (
@@ -276,7 +284,7 @@ async def cancel_task(
276284 )
277285 except Exception as e :
278286 logger .error (f"Failed to update Chat Shell task { task_id } status: { str (e )} " )
279-
287+
280288 return {"message" : "Chat stopped successfully" , "status" : "COMPLETED" }
281289 else :
282290 # No running subtask found, just mark task as completed
@@ -289,7 +297,7 @@ async def cancel_task(
289297 )
290298 except Exception as e :
291299 logger .error (f"Failed to update task { task_id } status: { str (e )} " )
292-
300+
293301 return {"message" : "No running stream to cancel" , "status" : "COMPLETED" }
294302 else :
295303 # For non-Chat Shell tasks, use executor_manager
@@ -314,3 +322,110 @@ async def cancel_task(
314322 background_tasks .add_task (call_executor_cancel , task_id )
315323
316324 return {"message" : "Cancel request accepted" , "status" : "CANCELLING" }
325+
326+
327+ @router .post ("/{task_id}/share" , response_model = TaskShareResponse )
328+ def share_task (
329+ task_id : int ,
330+ current_user : User = Depends (security .get_current_user ),
331+ db : Session = Depends (get_db ),
332+ ):
333+ """
334+ Generate a share link for a task.
335+ The share link allows others to view the task history and copy it to their task list.
336+ """
337+ # Validate that the task belongs to the current user
338+ if not shared_task_service .validate_task_exists (
339+ db = db , task_id = task_id , user_id = current_user .id
340+ ):
341+ raise HTTPException (
342+ status_code = 404 , detail = "Task not found or you don't have permission"
343+ )
344+
345+ return shared_task_service .share_task (
346+ db = db , task_id = task_id , user_id = current_user .id
347+ )
348+
349+
350+ @router .get ("/share/info" , response_model = TaskShareInfo )
351+ def get_task_share_info (
352+ share_token : str = Query (..., description = "Share token from URL" ),
353+ db : Session = Depends (get_db ),
354+ ):
355+ """
356+ Get task share information from share token.
357+ This endpoint doesn't require authentication, so anyone with the link can view.
358+ """
359+ return shared_task_service .get_share_info (db = db , share_token = share_token )
360+
361+
362+ @router .get ("/share/public" , response_model = PublicSharedTaskResponse )
363+ def get_public_shared_task (
364+ token : str = Query (..., description = "Share token from URL" ),
365+ db : Session = Depends (get_db ),
366+ ):
367+ """
368+ Get public shared task data for read-only viewing.
369+ This endpoint doesn't require authentication - anyone with the link can view.
370+ Only returns public data (no sensitive information like team config, bot details, etc.)
371+ """
372+ return shared_task_service .get_public_shared_task (db = db , share_token = token )
373+
374+
375+ @router .post ("/share/join" , response_model = JoinSharedTaskResponse )
376+ def join_shared_task (
377+ request : JoinSharedTaskRequest ,
378+ current_user : User = Depends (security .get_current_user ),
379+ db : Session = Depends (get_db ),
380+ ):
381+ """
382+ Copy a shared task to the current user's task list.
383+ This creates a new task with all the subtasks (messages) from the shared task.
384+ """
385+ from app .models .kind import Kind
386+
387+ # If team_id is provided, validate it belongs to the user
388+ if request .team_id :
389+ user_team = (
390+ db .query (Kind )
391+ .filter (
392+ Kind .user_id == current_user .id ,
393+ Kind .kind == "Team" ,
394+ Kind .id == request .team_id ,
395+ Kind .is_active == True ,
396+ )
397+ .first ()
398+ )
399+
400+ if not user_team :
401+ raise HTTPException (
402+ status_code = 400 ,
403+ detail = "Invalid team_id or team does not belong to you" ,
404+ )
405+ else :
406+ # Get user's first active team if not specified
407+ user_team = (
408+ db .query (Kind )
409+ .filter (
410+ Kind .user_id == current_user .id ,
411+ Kind .kind == "Team" ,
412+ Kind .is_active == True ,
413+ )
414+ .first ()
415+ )
416+
417+ if not user_team :
418+ raise HTTPException (
419+ status_code = 400 ,
420+ detail = "You need to have at least one team to copy a shared task" ,
421+ )
422+
423+ return shared_task_service .join_shared_task (
424+ db = db ,
425+ share_token = request .share_token ,
426+ user_id = current_user .id ,
427+ team_id = user_team .id ,
428+ model_id = request .model_id ,
429+ force_override_bot_model = request .force_override_bot_model or False ,
430+ )
431+
0 commit comments