mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
Ship Cursor-only MCP admin and modular Admin/Chat UI.
Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2aff012b48
commit
f6f129931f
61 changed files with 18057 additions and 16327 deletions
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -12,6 +14,28 @@ from svc.config.log_paths import oclaw_log_root
|
|||
_CONFIGURED = False
|
||||
|
||||
|
||||
class SafeRotatingFileHandler(logging.handlers.RotatingFileHandler):
|
||||
"""RotatingFileHandler that tolerates Windows file locks during rename.
|
||||
|
||||
Production gateway.err showed thousands of ``PermissionError: [WinError 32]``
|
||||
when ``RotatingFileHandler.doRollover`` tried to rename ``oclaw.log`` while
|
||||
another handle still had the file open. Swallow that race and keep writing
|
||||
to the current stream instead of dumping a traceback storm to stderr.
|
||||
"""
|
||||
|
||||
def doRollover(self) -> None: # noqa: N802 — logging API
|
||||
try:
|
||||
super().doRollover()
|
||||
except PermissionError:
|
||||
return
|
||||
except OSError as e:
|
||||
winerr = getattr(e, "winerror", None)
|
||||
# 32 = sharing violation; 13/EACCES also seen under concurrent writers.
|
||||
if winerr == 32 or getattr(e, "errno", None) in (13, 11):
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
def _skip_file_handlers() -> bool:
|
||||
if str(os.getenv("AIA_LOG_TO_FILE") or "").strip().lower() in ("0", "false", "no", "off"):
|
||||
return True
|
||||
|
|
@ -74,7 +98,7 @@ def build_worker_logging_dict_config(
|
|||
},
|
||||
"handlers": {
|
||||
"oclaw_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"class": "svc.observability.logging_setup.SafeRotatingFileHandler",
|
||||
"formatter": "file",
|
||||
"filename": str(oclaw_path),
|
||||
"maxBytes": max(256_000, mb),
|
||||
|
|
@ -118,7 +142,7 @@ def build_uvicorn_logging_dict_config(
|
|||
},
|
||||
"handlers": {
|
||||
"default_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"class": "svc.observability.logging_setup.SafeRotatingFileHandler",
|
||||
"formatter": "default",
|
||||
"filename": str(oclaw_path),
|
||||
"maxBytes": max(256_000, mb),
|
||||
|
|
@ -126,7 +150,7 @@ def build_uvicorn_logging_dict_config(
|
|||
"encoding": "utf-8",
|
||||
},
|
||||
"access_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"class": "svc.observability.logging_setup.SafeRotatingFileHandler",
|
||||
"formatter": "access",
|
||||
"filename": str(access_path),
|
||||
"maxBytes": max(256_000, mb),
|
||||
|
|
@ -185,6 +209,7 @@ def reset_oclaw_logging_for_tests() -> None:
|
|||
|
||||
|
||||
__all__ = [
|
||||
"SafeRotatingFileHandler",
|
||||
"build_uvicorn_logging_dict_config",
|
||||
"build_worker_logging_dict_config",
|
||||
"configure_oclaw_logging",
|
||||
|
|
|
|||
|
|
@ -660,21 +660,101 @@ class ScheduledJobStoreMixin:
|
|||
delivery_status = raw
|
||||
except Exception:
|
||||
delivery_status = {}
|
||||
from runtime.scheduler.failure_class import enrich_scheduled_job_run_dict
|
||||
|
||||
return enrich_scheduled_job_run_dict(
|
||||
{
|
||||
"id": run.id,
|
||||
"job_id": run.job_id,
|
||||
"tenant_id": run.tenant_id,
|
||||
"status": run.status,
|
||||
"scheduled_at": run.scheduled_at,
|
||||
"started_at": run.started_at,
|
||||
"finished_at": run.finished_at,
|
||||
"session_id": run.session_id,
|
||||
"oclaw_task_id": run.oclaw_task_id,
|
||||
"run_id": run.run_id,
|
||||
"reply_text": run.reply_text,
|
||||
"delivery_status": delivery_status,
|
||||
"error": run.error,
|
||||
"created_at": run.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
def scheduled_job_failure_summary(
|
||||
self,
|
||||
*,
|
||||
tenant_id: str,
|
||||
recent_limit: int = 80,
|
||||
) -> dict[str, Any]:
|
||||
"""Aggregate recent run outcomes + jobs whose last_run_status is failed."""
|
||||
from collections import Counter
|
||||
|
||||
from runtime.scheduler.failure_class import classify_scheduled_job_error
|
||||
|
||||
tid = str(tenant_id or "").strip()
|
||||
lim = max(10, min(int(recent_limit or 80), 300))
|
||||
jobs = self.scheduled_job_list(tenant_id=tid, status=None, limit=200)
|
||||
by_status = Counter(str(j.status or "") for j in jobs)
|
||||
last_failed_jobs: list[dict[str, Any]] = []
|
||||
for j in jobs:
|
||||
if str(j.last_run_status or "").strip().lower() != "failed":
|
||||
continue
|
||||
d = self.scheduled_job_to_dict(j)
|
||||
last_failed_jobs.append(
|
||||
{
|
||||
"id": d.get("id"),
|
||||
"name": d.get("name"),
|
||||
"status": d.get("status"),
|
||||
"last_run_at": d.get("last_run_at"),
|
||||
"last_run_status": d.get("last_run_status"),
|
||||
"specialist": d.get("specialist"),
|
||||
}
|
||||
)
|
||||
with self._connect() as conn: # type: ignore[attr-defined]
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT r.id, r.job_id, r.tenant_id, r.status, r.scheduled_at, r.started_at, r.finished_at,
|
||||
r.session_id, r.oclaw_task_id, r.run_id, r.reply_text, r.delivery_status_json, r.error, r.created_at,
|
||||
j.name AS job_name
|
||||
FROM scheduled_job_run r
|
||||
LEFT JOIN scheduled_job j ON j.id = r.job_id
|
||||
WHERE r.tenant_id = ?
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(tid, lim),
|
||||
).fetchall()
|
||||
status_counts: Counter[str] = Counter()
|
||||
fail_classes: Counter[str] = Counter()
|
||||
recent_fails: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
st = str(row["status"] or "")
|
||||
status_counts[st] += 1
|
||||
err = str(row["error"] or "")
|
||||
fc = classify_scheduled_job_error(err, status=st)
|
||||
if fc:
|
||||
fail_classes[fc] += 1
|
||||
if st.lower() in {"failed", "error"}:
|
||||
item = self.scheduled_job_run_to_dict(_row_to_run(row))
|
||||
item["job_name"] = str(row["job_name"] or "") or None
|
||||
if len(recent_fails) < 25:
|
||||
recent_fails.append(item)
|
||||
finished = sum(int(v) for k, v in status_counts.items() if str(k).lower() not in {"queued", "running", ""})
|
||||
failed_n = int(status_counts.get("failed", 0)) + int(status_counts.get("error", 0))
|
||||
success_n = int(status_counts.get("success", 0))
|
||||
fail_rate = round(100.0 * failed_n / finished, 1) if finished else 0.0
|
||||
return {
|
||||
"id": run.id,
|
||||
"job_id": run.job_id,
|
||||
"tenant_id": run.tenant_id,
|
||||
"status": run.status,
|
||||
"scheduled_at": run.scheduled_at,
|
||||
"started_at": run.started_at,
|
||||
"finished_at": run.finished_at,
|
||||
"session_id": run.session_id,
|
||||
"oclaw_task_id": run.oclaw_task_id,
|
||||
"run_id": run.run_id,
|
||||
"reply_text": run.reply_text,
|
||||
"delivery_status": delivery_status,
|
||||
"error": run.error,
|
||||
"created_at": run.created_at,
|
||||
"jobs_total": len(jobs),
|
||||
"jobs_by_status": dict(by_status),
|
||||
"jobs_last_failed": last_failed_jobs,
|
||||
"recent_runs": lim,
|
||||
"recent_status_counts": dict(status_counts),
|
||||
"recent_fail_classes": dict(fail_classes.most_common()),
|
||||
"recent_fail_rate_pct": fail_rate,
|
||||
"recent_success": success_n,
|
||||
"recent_failed": failed_n,
|
||||
"recent_failures": recent_fails,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue