enh: automation
This commit is contained in:
@@ -1541,6 +1541,18 @@ ENABLE_CHANNELS = PersistentConfig(
|
|||||||
os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true',
|
os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTOMATION_MAX_COUNT = PersistentConfig(
|
||||||
|
'AUTOMATION_MAX_COUNT',
|
||||||
|
'automations.max_count',
|
||||||
|
os.environ.get('AUTOMATION_MAX_COUNT', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTOMATION_MIN_INTERVAL = PersistentConfig(
|
||||||
|
'AUTOMATION_MIN_INTERVAL',
|
||||||
|
'automations.min_interval',
|
||||||
|
os.environ.get('AUTOMATION_MIN_INTERVAL', ''),
|
||||||
|
)
|
||||||
|
|
||||||
ENABLE_NOTES = PersistentConfig(
|
ENABLE_NOTES = PersistentConfig(
|
||||||
'ENABLE_NOTES',
|
'ENABLE_NOTES',
|
||||||
'notes.enable',
|
'notes.enable',
|
||||||
|
|||||||
@@ -383,6 +383,8 @@ from open_webui.config import (
|
|||||||
API_KEYS_ALLOWED_ENDPOINTS,
|
API_KEYS_ALLOWED_ENDPOINTS,
|
||||||
ENABLE_FOLDERS,
|
ENABLE_FOLDERS,
|
||||||
FOLDER_MAX_FILE_COUNT,
|
FOLDER_MAX_FILE_COUNT,
|
||||||
|
AUTOMATION_MAX_COUNT,
|
||||||
|
AUTOMATION_MIN_INTERVAL,
|
||||||
ENABLE_CHANNELS,
|
ENABLE_CHANNELS,
|
||||||
ENABLE_NOTES,
|
ENABLE_NOTES,
|
||||||
ENABLE_USER_STATUS,
|
ENABLE_USER_STATUS,
|
||||||
@@ -874,6 +876,8 @@ app.state.config.BANNERS = WEBUI_BANNERS
|
|||||||
|
|
||||||
app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS
|
app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS
|
||||||
app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
|
app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT
|
||||||
|
app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT
|
||||||
|
app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL
|
||||||
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
|
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
|
||||||
app.state.config.ENABLE_NOTES = ENABLE_NOTES
|
app.state.config.ENABLE_NOTES = ENABLE_NOTES
|
||||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||||
|
|||||||
@@ -143,6 +143,10 @@ class AutomationTable:
|
|||||||
db.refresh(row)
|
db.refresh(row)
|
||||||
return AutomationModel.model_validate(row)
|
return AutomationModel.model_validate(row)
|
||||||
|
|
||||||
|
def count_by_user(self, user_id: str, db: Optional[Session] = None) -> int:
|
||||||
|
with get_db_context(db) as db:
|
||||||
|
return db.query(Automation).filter_by(user_id=user_id).count()
|
||||||
|
|
||||||
def get_by_id(self, id: str, db: Optional[Session] = None) -> Optional[AutomationModel]:
|
def get_by_id(self, id: str, db: Optional[Session] = None) -> Optional[AutomationModel]:
|
||||||
with get_db_context(db) as db:
|
with get_db_context(db) as db:
|
||||||
row = db.get(Automation, id)
|
row = db.get(Automation, id)
|
||||||
|
|||||||
@@ -950,6 +950,8 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
|
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||||
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
|
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
|
||||||
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
|
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
|
||||||
|
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
|
||||||
|
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
|
||||||
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
|
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
|
||||||
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
|
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
|
||||||
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
|
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
|
||||||
@@ -976,6 +978,8 @@ class AdminConfig(BaseModel):
|
|||||||
ENABLE_MESSAGE_RATING: bool
|
ENABLE_MESSAGE_RATING: bool
|
||||||
ENABLE_FOLDERS: bool
|
ENABLE_FOLDERS: bool
|
||||||
FOLDER_MAX_FILE_COUNT: Optional[int | str] = None
|
FOLDER_MAX_FILE_COUNT: Optional[int | str] = None
|
||||||
|
AUTOMATION_MAX_COUNT: Optional[int | str] = None
|
||||||
|
AUTOMATION_MIN_INTERVAL: Optional[int | str] = None
|
||||||
ENABLE_CHANNELS: bool
|
ENABLE_CHANNELS: bool
|
||||||
ENABLE_MEMORIES: bool
|
ENABLE_MEMORIES: bool
|
||||||
ENABLE_NOTES: bool
|
ENABLE_NOTES: bool
|
||||||
@@ -1001,6 +1005,12 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
|
|||||||
request.app.state.config.FOLDER_MAX_FILE_COUNT = (
|
request.app.state.config.FOLDER_MAX_FILE_COUNT = (
|
||||||
int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
|
int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
|
||||||
)
|
)
|
||||||
|
request.app.state.config.AUTOMATION_MAX_COUNT = (
|
||||||
|
int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''
|
||||||
|
)
|
||||||
|
request.app.state.config.AUTOMATION_MIN_INTERVAL = (
|
||||||
|
int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else ''
|
||||||
|
)
|
||||||
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
|
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
|
||||||
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
|
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
|
||||||
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
|
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
|
||||||
@@ -1042,6 +1052,8 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep
|
|||||||
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
|
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||||
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
|
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
|
||||||
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
|
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
|
||||||
|
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
|
||||||
|
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
|
||||||
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
|
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
|
||||||
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
|
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
|
||||||
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
|
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from open_webui.utils.automations import (
|
|||||||
next_run_ns,
|
next_run_ns,
|
||||||
next_n_runs_ns,
|
next_n_runs_ns,
|
||||||
execute_automation,
|
execute_automation,
|
||||||
|
rrule_interval_seconds,
|
||||||
)
|
)
|
||||||
from open_webui.utils.auth import get_verified_user, get_admin_user
|
from open_webui.utils.auth import get_verified_user, get_admin_user
|
||||||
from open_webui.utils.access_control import has_permission
|
from open_webui.utils.access_control import has_permission
|
||||||
@@ -60,6 +61,35 @@ def check_automation_access(automation, user):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_automation_limits(request, user, rrule_str: str, db, is_create: bool = False):
|
||||||
|
"""Enforce global automation limits. Admins bypass all checks."""
|
||||||
|
if user.role == 'admin':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Max count (create only)
|
||||||
|
if is_create:
|
||||||
|
max_count = request.app.state.config.AUTOMATION_MAX_COUNT
|
||||||
|
if max_count:
|
||||||
|
max_count = int(max_count)
|
||||||
|
if max_count > 0 and Automations.count_by_user(user.id, db=db) >= max_count:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=f'Automation limit reached ({max_count})',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Min interval (create + update)
|
||||||
|
min_interval = request.app.state.config.AUTOMATION_MIN_INTERVAL
|
||||||
|
if min_interval:
|
||||||
|
min_interval = int(min_interval)
|
||||||
|
if min_interval > 0:
|
||||||
|
interval = rrule_interval_seconds(rrule_str)
|
||||||
|
if interval is not None and interval < min_interval:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f'Schedule too frequent. Minimum interval is {min_interval} seconds.',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def enrich_automation(automation: AutomationModel, db: Session, tz: str = None) -> AutomationResponse:
|
def enrich_automation(automation: AutomationModel, db: Session, tz: str = None) -> AutomationResponse:
|
||||||
"""Full enrichment for single-item views (includes next_runs computation)."""
|
"""Full enrichment for single-item views (includes next_runs computation)."""
|
||||||
last_run = AutomationRuns.get_latest(automation.id, db=db)
|
last_run = AutomationRuns.get_latest(automation.id, db=db)
|
||||||
@@ -135,6 +165,8 @@ async def create_new_automation(
|
|||||||
detail=str(e),
|
detail=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
check_automation_limits(request, user, form_data.data.rrule, db, is_create=True)
|
||||||
|
|
||||||
# Validate terminal server exists if linked
|
# Validate terminal server exists if linked
|
||||||
if form_data.data.terminal and form_data.data.terminal.server_id:
|
if form_data.data.terminal and form_data.data.terminal.server_id:
|
||||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||||
@@ -192,6 +224,8 @@ async def update_automation_by_id(
|
|||||||
detail=str(e),
|
detail=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
check_automation_limits(request, user, form_data.data.rrule, db, is_create=False)
|
||||||
|
|
||||||
# Validate terminal server exists if linked
|
# Validate terminal server exists if linked
|
||||||
if form_data.data.terminal and form_data.data.terminal.server_id:
|
if form_data.data.terminal and form_data.data.terminal.server_id:
|
||||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||||
|
|||||||
@@ -92,6 +92,25 @@ def next_n_runs_ns(s: str, n: int = 5, tz: str = None) -> list[int]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def rrule_interval_seconds(s: str) -> Optional[int]:
|
||||||
|
"""Approximate interval between recurrences in seconds.
|
||||||
|
|
||||||
|
Returns None for one-shot (COUNT=1) schedules or rules
|
||||||
|
with fewer than two future occurrences.
|
||||||
|
"""
|
||||||
|
if 'COUNT=1' in s:
|
||||||
|
return None
|
||||||
|
rule = _parse_rule(s)
|
||||||
|
now = datetime.now()
|
||||||
|
first = rule.after(now)
|
||||||
|
if first is None:
|
||||||
|
return None
|
||||||
|
second = rule.after(first)
|
||||||
|
if second is None:
|
||||||
|
return None
|
||||||
|
return int((second - first).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Worker Loop
|
# Worker Loop
|
||||||
############################
|
############################
|
||||||
|
|||||||
Reference in New Issue
Block a user