mirror of https://github.com/langgenius/dify.git
add ENABLE_COLLABORATION_MODE
This commit is contained in:
parent
8e3b412ff6
commit
eaf1ae37dd
|
|
@ -30,6 +30,9 @@ INTERNAL_FILES_URL=http://127.0.0.1:5001
|
|||
# The time in seconds after the signature is rejected
|
||||
FILES_ACCESS_TIMEOUT=300
|
||||
|
||||
# Collaboration mode toggle
|
||||
ENABLE_COLLABORATION_MODE=false
|
||||
|
||||
# Access token expiration time in minutes
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
|
||||
|
|
|
|||
|
|
@ -1048,6 +1048,13 @@ class PositionConfig(BaseSettings):
|
|||
return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""}
|
||||
|
||||
|
||||
class CollaborationConfig(BaseSettings):
|
||||
ENABLE_COLLABORATION_MODE: bool = Field(
|
||||
description="Whether to enable collaboration mode features across the workspace",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class LoginConfig(BaseSettings):
|
||||
ENABLE_EMAIL_CODE_LOGIN: bool = Field(
|
||||
description="whether to enable email code login",
|
||||
|
|
@ -1136,6 +1143,7 @@ class FeatureConfig(
|
|||
WorkflowConfig,
|
||||
WorkflowNodeExecutionConfig,
|
||||
WorkspaceConfig,
|
||||
CollaborationConfig,
|
||||
LoginConfig,
|
||||
AccountConfig,
|
||||
SwaggerUIConfig,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import json
|
||||
import time
|
||||
|
||||
from werkzeug.wrappers import Request as WerkzeugRequest
|
||||
|
||||
from extensions.ext_redis import redis_client
|
||||
from extensions.ext_socketio import sio
|
||||
from libs.token import extract_access_token
|
||||
from libs.passport import PassportService
|
||||
from libs.token import extract_access_token
|
||||
from services.account_service import AccountService
|
||||
from werkzeug.wrappers import Request as WerkzeugRequest
|
||||
|
||||
|
||||
@sio.on("connect")
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class SystemFeatureModel(BaseModel):
|
|||
enable_email_code_login: bool = False
|
||||
enable_email_password_login: bool = True
|
||||
enable_social_oauth_login: bool = False
|
||||
enable_collaboration_mode: bool = False
|
||||
is_allow_register: bool = False
|
||||
is_allow_create_workspace: bool = False
|
||||
is_email_setup: bool = False
|
||||
|
|
@ -211,6 +212,7 @@ class FeatureService:
|
|||
system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN
|
||||
system_features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN
|
||||
system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN
|
||||
system_features.enable_collaboration_mode = dify_config.ENABLE_COLLABORATION_MODE
|
||||
system_features.is_allow_register = dify_config.ALLOW_REGISTER
|
||||
system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE
|
||||
system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != ""
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ class TestFeatureService:
|
|||
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
|
||||
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
|
||||
mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = False
|
||||
mock_config.ENABLE_COLLABORATION_MODE = True
|
||||
mock_config.ALLOW_REGISTER = False
|
||||
mock_config.ALLOW_CREATE_WORKSPACE = False
|
||||
mock_config.MAIL_TYPE = "smtp"
|
||||
|
|
@ -291,6 +292,7 @@ class TestFeatureService:
|
|||
# Verify authentication settings
|
||||
assert result.enable_email_code_login is True
|
||||
assert result.enable_email_password_login is False
|
||||
assert result.enable_collaboration_mode is True
|
||||
assert result.is_allow_register is False
|
||||
assert result.is_allow_create_workspace is False
|
||||
|
||||
|
|
@ -340,6 +342,7 @@ class TestFeatureService:
|
|||
mock_config.ENABLE_EMAIL_CODE_LOGIN = True
|
||||
mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True
|
||||
mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = False
|
||||
mock_config.ENABLE_COLLABORATION_MODE = False
|
||||
mock_config.ALLOW_REGISTER = True
|
||||
mock_config.ALLOW_CREATE_WORKSPACE = True
|
||||
mock_config.MAIL_TYPE = "smtp"
|
||||
|
|
@ -361,6 +364,7 @@ class TestFeatureService:
|
|||
assert result.enable_email_code_login is True
|
||||
assert result.enable_email_password_login is True
|
||||
assert result.enable_social_oauth_login is False
|
||||
assert result.enable_collaboration_mode is False
|
||||
assert result.is_allow_register is True
|
||||
assert result.is_allow_create_workspace is True
|
||||
assert result.is_email_setup is True
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@ MIGRATION_ENABLED=true
|
|||
# The default value is 300 seconds.
|
||||
FILES_ACCESS_TIMEOUT=300
|
||||
|
||||
# Collaboration mode toggle
|
||||
# To open collaboration features, you also need to set SERVER_WORKER_CLASS=geventwebsocket.gunicorn.workers.GeventWebSocketWorker
|
||||
ENABLE_COLLABORATION_MODE=false
|
||||
|
||||
# Access token expiration time in minutes
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||
|
||||
|
|
@ -149,6 +153,7 @@ DIFY_PORT=5001
|
|||
SERVER_WORKER_AMOUNT=1
|
||||
|
||||
# Defaults to gevent. If using windows, it can be switched to sync or solo.
|
||||
# If enable collaboration mode, it must be set to geventwebsocket.gunicorn.workers.GeventWebSocketWorker
|
||||
SERVER_WORKER_CLASS=gevent
|
||||
|
||||
# Default number of worker connections, the default is 10.
|
||||
|
|
|
|||
|
|
@ -45,26 +45,38 @@ const WorkflowMain = ({
|
|||
const reactFlow = useReactFlow()
|
||||
|
||||
const store = useStoreApi()
|
||||
const { startCursorTracking, stopCursorTracking, onlineUsers, cursors, isConnected } = useCollaboration(appId || '', store)
|
||||
const {
|
||||
startCursorTracking,
|
||||
stopCursorTracking,
|
||||
onlineUsers,
|
||||
cursors,
|
||||
isConnected,
|
||||
isEnabled: isCollaborationEnabled,
|
||||
} = useCollaboration(appId || '', store)
|
||||
const [myUserId, setMyUserId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected)
|
||||
if (isCollaborationEnabled && isConnected)
|
||||
setMyUserId('current-user')
|
||||
}, [isConnected])
|
||||
else
|
||||
setMyUserId(null)
|
||||
}, [isCollaborationEnabled, isConnected])
|
||||
|
||||
const filteredCursors = Object.fromEntries(
|
||||
Object.entries(cursors).filter(([userId]) => userId !== myUserId),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCollaborationEnabled)
|
||||
return
|
||||
|
||||
if (containerRef.current)
|
||||
startCursorTracking(containerRef as React.RefObject<HTMLElement>, reactFlow)
|
||||
|
||||
return () => {
|
||||
stopCursorTracking()
|
||||
}
|
||||
}, [startCursorTracking, stopCursorTracking, reactFlow])
|
||||
}, [startCursorTracking, stopCursorTracking, reactFlow, isCollaborationEnabled])
|
||||
|
||||
const handleWorkflowDataUpdate = useCallback((payload: any) => {
|
||||
const {
|
||||
|
|
@ -128,7 +140,7 @@ const WorkflowMain = ({
|
|||
} = useWorkflowRun()
|
||||
|
||||
useEffect(() => {
|
||||
if (!appId) return
|
||||
if (!appId || !isCollaborationEnabled) return
|
||||
|
||||
const unsubscribe = collaborationManager.onVarsAndFeaturesUpdate(async (update: any) => {
|
||||
try {
|
||||
|
|
@ -141,11 +153,11 @@ const WorkflowMain = ({
|
|||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [appId, handleWorkflowDataUpdate])
|
||||
}, [appId, handleWorkflowDataUpdate, isCollaborationEnabled])
|
||||
|
||||
// Listen for workflow updates from other users
|
||||
useEffect(() => {
|
||||
if (!appId) return
|
||||
if (!appId || !isCollaborationEnabled) return
|
||||
|
||||
const unsubscribe = collaborationManager.onWorkflowUpdate(async () => {
|
||||
console.log('Received workflow update from collaborator, fetching latest workflow data')
|
||||
|
|
@ -170,11 +182,11 @@ const WorkflowMain = ({
|
|||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [appId, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas])
|
||||
}, [appId, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled])
|
||||
|
||||
// Listen for sync requests from other users (only processed by leader)
|
||||
useEffect(() => {
|
||||
if (!appId) return
|
||||
if (!appId || !isCollaborationEnabled) return
|
||||
|
||||
const unsubscribe = collaborationManager.onSyncRequest(() => {
|
||||
console.log('Leader received sync request, performing sync')
|
||||
|
|
@ -182,7 +194,7 @@ const WorkflowMain = ({
|
|||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [appId, doSyncWorkflowDraft])
|
||||
}, [appId, doSyncWorkflowDraft, isCollaborationEnabled])
|
||||
const {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
|||
import { API_PREFIX } from '@/config'
|
||||
import { useWorkflowRefreshDraft } from '.'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const store = useStoreApi()
|
||||
|
|
@ -22,6 +23,7 @@ export const useNodesSyncDraft = () => {
|
|||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
|
||||
const params = useParams()
|
||||
const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
|
||||
|
||||
const getPostParams = useCallback(() => {
|
||||
const {
|
||||
|
|
@ -86,21 +88,21 @@ export const useNodesSyncDraft = () => {
|
|||
environment_variables: environmentVariables,
|
||||
conversation_variables: conversationVariables,
|
||||
hash: syncWorkflowDraftHash,
|
||||
_is_collaborative: true,
|
||||
_is_collaborative: isCollaborationEnabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
}, [store, featuresStore, workflowStore])
|
||||
}, [store, featuresStore, workflowStore, isCollaborationEnabled])
|
||||
|
||||
const syncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
// Check leader status at sync time
|
||||
const currentIsLeader = collaborationManager.getIsLeader()
|
||||
const currentIsLeader = isCollaborationEnabled ? collaborationManager.getIsLeader() : true
|
||||
|
||||
// Only allow leader to sync data
|
||||
if (!currentIsLeader) {
|
||||
if (isCollaborationEnabled && !currentIsLeader) {
|
||||
console.log('Not leader, skipping sync on page close')
|
||||
return
|
||||
}
|
||||
|
|
@ -114,7 +116,7 @@ export const useNodesSyncDraft = () => {
|
|||
JSON.stringify(postParams.params),
|
||||
)
|
||||
}
|
||||
}, [getPostParams, params.appId, getNodesReadOnly])
|
||||
}, [getPostParams, params.appId, getNodesReadOnly, isCollaborationEnabled])
|
||||
|
||||
const doSyncWorkflowDraft = useCallback(async (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
|
|
@ -129,12 +131,13 @@ export const useNodesSyncDraft = () => {
|
|||
return
|
||||
|
||||
// Check leader status at sync time
|
||||
const currentIsLeader = collaborationManager.getIsLeader()
|
||||
const currentIsLeader = isCollaborationEnabled ? collaborationManager.getIsLeader() : true
|
||||
|
||||
// If not leader and not forcing upload, request the leader to sync
|
||||
if (!currentIsLeader && !forceUpload) {
|
||||
if (isCollaborationEnabled && !currentIsLeader && !forceUpload) {
|
||||
console.log('Not leader, requesting leader to sync workflow draft')
|
||||
collaborationManager.emitSyncRequest()
|
||||
if (isCollaborationEnabled)
|
||||
collaborationManager.emitSyncRequest()
|
||||
callback?.onSettled?.()
|
||||
return
|
||||
}
|
||||
|
|
@ -180,7 +183,7 @@ export const useNodesSyncDraft = () => {
|
|||
callback?.onSettled?.()
|
||||
}
|
||||
}
|
||||
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft])
|
||||
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft, isCollaborationEnabled])
|
||||
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ReactFlowInstance } from 'reactflow'
|
|||
import { collaborationManager } from '../core/collaboration-manager'
|
||||
import { CursorService } from '../services/cursor-service'
|
||||
import type { CollaborationState } from '../types/collaboration'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export function useCollaboration(appId: string, reactFlowStore?: any) {
|
||||
const [state, setState] = useState<Partial<CollaborationState & { isLeader: boolean }>>({
|
||||
|
|
@ -14,9 +15,19 @@ export function useCollaboration(appId: string, reactFlowStore?: any) {
|
|||
})
|
||||
|
||||
const cursorServiceRef = useRef<CursorService | null>(null)
|
||||
const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
|
||||
|
||||
useEffect(() => {
|
||||
if (!appId) return
|
||||
if (!appId || !isCollaborationEnabled) {
|
||||
setState({
|
||||
isConnected: false,
|
||||
onlineUsers: [],
|
||||
cursors: {},
|
||||
nodePanelPresence: {},
|
||||
isLeader: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let connectionId: string | null = null
|
||||
let isUnmounted = false
|
||||
|
|
@ -75,9 +86,12 @@ export function useCollaboration(appId: string, reactFlowStore?: any) {
|
|||
if (connectionId)
|
||||
collaborationManager.disconnect(connectionId)
|
||||
}
|
||||
}, [appId, reactFlowStore])
|
||||
}, [appId, reactFlowStore, isCollaborationEnabled])
|
||||
|
||||
const startCursorTracking = (containerRef: React.RefObject<HTMLElement>, reactFlowInstance?: ReactFlowInstance) => {
|
||||
if (!isCollaborationEnabled || !cursorServiceRef.current)
|
||||
return
|
||||
|
||||
if (cursorServiceRef.current) {
|
||||
cursorServiceRef.current.startTracking(containerRef, (position) => {
|
||||
collaborationManager.emitCursorMove(position)
|
||||
|
|
@ -96,6 +110,7 @@ export function useCollaboration(appId: string, reactFlowStore?: any) {
|
|||
nodePanelPresence: state.nodePanelPresence || {},
|
||||
isLeader: state.isLeader || false,
|
||||
leaderId: collaborationManager.getLeaderId(),
|
||||
isEnabled: isCollaborationEnabled,
|
||||
startCursorTracking,
|
||||
stopCursorTracking,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const useAvatarUrls = (users: any[]) => {
|
|||
|
||||
const OnlineUsers = () => {
|
||||
const appId = useStore(s => s.appId)
|
||||
const { onlineUsers, cursors } = useCollaboration(appId as string)
|
||||
const { onlineUsers, cursors, isEnabled: isCollaborationEnabled } = useCollaboration(appId as string)
|
||||
const { userProfile } = useAppContext()
|
||||
const reactFlow = useReactFlow()
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||
|
|
@ -87,7 +87,7 @@ const OnlineUsers = () => {
|
|||
reactFlow.setCenter(cursor.x, cursor.y, { zoom: 1, duration: 800 })
|
||||
}
|
||||
|
||||
if (!onlineUsers || onlineUsers.length === 0)
|
||||
if (!isCollaborationEnabled || !onlineUsers || onlineUsers.length === 0)
|
||||
return null
|
||||
|
||||
// Display logic:
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export type SystemFeatures = {
|
|||
enable_email_code_login: boolean
|
||||
enable_email_password_login: boolean
|
||||
enable_social_oauth_login: boolean
|
||||
enable_collaboration_mode: boolean
|
||||
is_allow_create_workspace: boolean
|
||||
is_allow_register: boolean
|
||||
is_email_setup: boolean
|
||||
|
|
@ -75,6 +76,7 @@ export const defaultSystemFeatures: SystemFeatures = {
|
|||
enable_email_code_login: false,
|
||||
enable_email_password_login: false,
|
||||
enable_social_oauth_login: false,
|
||||
enable_collaboration_mode: false,
|
||||
is_allow_create_workspace: false,
|
||||
is_allow_register: false,
|
||||
is_email_setup: false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue