77from flask_restx import Resource , fields , marshal , marshal_with
88from graphon .enums import NodeType
99from graphon .file import File
10+ from graphon .file import helpers as file_helpers
1011from graphon .graph_engine .manager import GraphEngineManager
1112from graphon .model_runtime .utils .encoders import jsonable_encoder
1213from pydantic import BaseModel , Field , ValidationError , field_validator
3940from extensions .ext_redis import redis_client
4041from factories import file_factory , variable_factory
4142from fields .member_fields import simple_account_fields
43+ from fields .online_user_fields import online_user_list_fields
4244from fields .workflow_fields import workflow_fields , workflow_pagination_fields
4345from libs import helper
4446from libs .datetime_utils import naive_utc_now
4749from models import App
4850from models .model import AppMode
4951from models .workflow import Workflow
52+ from repositories .workflow_collaboration_repository import WORKFLOW_ONLINE_USERS_PREFIX
5053from services .app_generate_service import AppGenerateService
5154from services .errors .app import IsDraftWorkflowError , WorkflowHashNotEqualError , WorkflowNotFoundError
5255from services .errors .llm import InvokeRateLimitError
5760LISTENING_RETRY_IN = 2000
5861DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
5962RESTORE_SOURCE_WORKFLOW_MUST_BE_PUBLISHED_MESSAGE = "source workflow must be published"
63+ MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS = 50
6064
6165# Register models for flask_restx to avoid dict type issues in Swagger
6266# Register in dependency order: base models first, then dependent models
@@ -150,6 +154,14 @@ class ConvertToWorkflowPayload(BaseModel):
150154 icon_background : str | None = None
151155
152156
157+ class WorkflowFeaturesPayload (BaseModel ):
158+ features : dict [str , Any ] = Field (..., description = "Workflow feature configuration" )
159+
160+
161+ class WorkflowOnlineUsersQuery (BaseModel ):
162+ app_ids : str = Field (..., description = "Comma-separated app IDs" )
163+
164+
153165class DraftWorkflowTriggerRunPayload (BaseModel ):
154166 node_id : str
155167
@@ -173,6 +185,8 @@ def reg(cls: type[BaseModel]):
173185reg (ConvertToWorkflowPayload )
174186reg (WorkflowListQuery )
175187reg (WorkflowUpdatePayload )
188+ reg (WorkflowFeaturesPayload )
189+ reg (WorkflowOnlineUsersQuery )
176190reg (DraftWorkflowTriggerRunPayload )
177191reg (DraftWorkflowTriggerRunAllPayload )
178192
@@ -931,6 +945,32 @@ def post(self, app_model: App):
931945 }
932946
933947
948+ @console_ns .route ("/apps/<uuid:app_id>/workflows/draft/features" )
949+ class WorkflowFeaturesApi (Resource ):
950+ """Update draft workflow features."""
951+
952+ @console_ns .expect (console_ns .models [WorkflowFeaturesPayload .__name__ ])
953+ @console_ns .doc ("update_workflow_features" )
954+ @console_ns .doc (description = "Update draft workflow features" )
955+ @console_ns .doc (params = {"app_id" : "Application ID" })
956+ @console_ns .response (200 , "Workflow features updated successfully" )
957+ @setup_required
958+ @login_required
959+ @account_initialization_required
960+ @get_app_model (mode = [AppMode .ADVANCED_CHAT , AppMode .WORKFLOW ])
961+ @edit_permission_required
962+ def post (self , app_model : App ):
963+ current_user , _ = current_account_with_tenant ()
964+
965+ args = WorkflowFeaturesPayload .model_validate (console_ns .payload or {})
966+ features = args .features
967+
968+ workflow_service = WorkflowService ()
969+ workflow_service .update_draft_workflow_features (app_model = app_model , features = features , account = current_user )
970+
971+ return {"result" : "success" }
972+
973+
934974@console_ns .route ("/apps/<uuid:app_id>/workflows" )
935975class PublishedAllWorkflowApi (Resource ):
936976 @console_ns .expect (console_ns .models [WorkflowListQuery .__name__ ])
@@ -1340,3 +1380,62 @@ def post(self, app_model: App):
13401380 "status" : "error" ,
13411381 }
13421382 ), 400
1383+
1384+
1385+ @console_ns .route ("/apps/workflows/online-users" )
1386+ class WorkflowOnlineUsersApi (Resource ):
1387+ @console_ns .expect (console_ns .models [WorkflowOnlineUsersQuery .__name__ ])
1388+ @console_ns .doc ("get_workflow_online_users" )
1389+ @console_ns .doc (description = "Get workflow online users" )
1390+ @setup_required
1391+ @login_required
1392+ @account_initialization_required
1393+ @marshal_with (online_user_list_fields )
1394+ def get (self ):
1395+ args = WorkflowOnlineUsersQuery .model_validate (request .args .to_dict (flat = True )) # type: ignore
1396+
1397+ app_ids = list (dict .fromkeys (app_id .strip () for app_id in args .app_ids .split ("," ) if app_id .strip ()))
1398+ if len (app_ids ) > MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS :
1399+ raise BadRequest (f"Maximum { MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS } app_ids are allowed per request." )
1400+
1401+ if not app_ids :
1402+ return {"data" : []}
1403+
1404+ _ , current_tenant_id = current_account_with_tenant ()
1405+ workflow_service = WorkflowService ()
1406+ accessible_app_ids = workflow_service .get_accessible_app_ids (app_ids , current_tenant_id )
1407+
1408+ results = []
1409+ for app_id in app_ids :
1410+ if app_id not in accessible_app_ids :
1411+ continue
1412+
1413+ users_json = redis_client .hgetall (f"{ WORKFLOW_ONLINE_USERS_PREFIX } { app_id } " )
1414+
1415+ users = []
1416+ for _ , user_info_json in users_json .items ():
1417+ try :
1418+ user_info = json .loads (user_info_json )
1419+ except Exception :
1420+ continue
1421+
1422+ if not isinstance (user_info , dict ):
1423+ continue
1424+
1425+ avatar = user_info .get ("avatar" )
1426+ if isinstance (avatar , str ) and avatar and not avatar .startswith (("http://" , "https://" )):
1427+ try :
1428+ user_info ["avatar" ] = file_helpers .get_signed_file_url (avatar )
1429+ except Exception as exc :
1430+ logger .warning (
1431+ "Failed to sign workflow online user avatar; using original value. "
1432+ "app_id=%s avatar=%s error=%s" ,
1433+ app_id ,
1434+ avatar ,
1435+ exc ,
1436+ )
1437+
1438+ users .append (user_info )
1439+ results .append ({"app_id" : app_id , "users" : users })
1440+
1441+ return {"data" : results }
0 commit comments