Settings: add dedicated DAV/Pushover tabs, fix CalDAV/CardDAV bugs
- Add admin DAV tab (rename from CalDAV/CardDAV) and Pushover tab
- Add per-user Pushover tab (User Key only; App Token stays admin-managed)
- Remove system-wide CalDAV/CardDAV fallback — per-user config only
- Rewrite contacts_tool.py using httpx directly (caldav 2.x dropped AddressBook)
- Fix CardDAV REPORT/PROPFIND using SOGo URL pattern
- Fix CalDAV/CardDAV test endpoints (POST method, URL scheme normalization)
- Fix Show Password button — API now returns actual credential values
- Convert Credentials tab to generic key-value store; dedicated keys
(CalDAV, Pushover, trusted_proxy) excluded via _DEDICATED_CRED_KEYS
This commit is contained in:
@@ -21,17 +21,50 @@ from . import tasks as agent_store
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Priority levels for the run queue (lower number = higher priority)
|
||||
PRIORITY_HIGH = 0 # User-initiated chat runs
|
||||
PRIORITY_NORMAL = 1 # Webhook / inbox / Telegram triggers
|
||||
PRIORITY_LOW = 2 # Background monitors
|
||||
|
||||
_DEFAULT_MAX_CONCURRENT = 3
|
||||
|
||||
|
||||
class AgentRunner:
|
||||
def __init__(self) -> None:
|
||||
self._agent: Agent | None = None
|
||||
self._scheduler = AsyncIOScheduler(timezone=settings.timezone)
|
||||
self._running: dict[str, asyncio.Task] = {} # run_id → asyncio.Task
|
||||
# Concurrency semaphore — initialised in start() once event loop is running
|
||||
self._semaphore: asyncio.Semaphore | None = None
|
||||
self._max_concurrent: int = _DEFAULT_MAX_CONCURRENT
|
||||
|
||||
@property
|
||||
def scheduler(self) -> AsyncIOScheduler:
|
||||
return self._scheduler
|
||||
|
||||
@property
|
||||
def queue_status(self) -> dict:
|
||||
running = sum(1 for t in self._running.values() if not t.done())
|
||||
# Tasks waiting for the semaphore are counted as "queued"
|
||||
queued = max(0, running - self._max_concurrent)
|
||||
return {"running": min(running, self._max_concurrent), "queued": queued, "max_concurrent": self._max_concurrent}
|
||||
|
||||
def init(self, agent: Agent) -> None:
|
||||
self._agent = agent
|
||||
|
||||
async def _load_max_concurrent(self) -> int:
|
||||
val = await credential_store.get("system:max_concurrent_runs")
|
||||
try:
|
||||
return max(1, int(val)) if val else _DEFAULT_MAX_CONCURRENT
|
||||
except (ValueError, TypeError):
|
||||
return _DEFAULT_MAX_CONCURRENT
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Load all enabled agents with schedules into APScheduler and start it."""
|
||||
# Initialise concurrency semaphore (must happen inside a running event loop)
|
||||
self._max_concurrent = await self._load_max_concurrent()
|
||||
self._semaphore = asyncio.Semaphore(self._max_concurrent)
|
||||
|
||||
for agent in await agent_store.list_agents():
|
||||
if agent["enabled"] and agent["schedule"]:
|
||||
self._add_job(agent)
|
||||
@@ -44,7 +77,9 @@ class AgentRunner:
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
self._scheduler.start()
|
||||
logger.info("[agent-runner] Scheduler started, loaded scheduled agents")
|
||||
logger.info(
|
||||
"[agent-runner] Scheduler started, max_concurrent=%d", self._max_concurrent
|
||||
)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self._scheduler.running:
|
||||
@@ -261,7 +296,14 @@ class AgentRunner:
|
||||
finally:
|
||||
self._running.pop(run_id, None)
|
||||
|
||||
task = asyncio.create_task(_execute())
|
||||
async def _execute_with_semaphore():
|
||||
if self._semaphore:
|
||||
async with self._semaphore:
|
||||
await _execute()
|
||||
else:
|
||||
await _execute()
|
||||
|
||||
task = asyncio.create_task(_execute_with_semaphore())
|
||||
self._running[run_id] = task
|
||||
return await agent_store.get_run(run_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user