mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 08:30:45 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
|
|
@ -1,13 +1,31 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from .config import should_include_hook_compat
|
||||
from .hook_types import HookEligibilityContext, HookEntry, ensure_hook_entry
|
||||
from .internal_hooks import HookHandler, register_hook, unregister_hook
|
||||
from .policy import resolve_hook_enable_state, resolve_hook_config
|
||||
from .workspace import HookEntry, load_workspace_hook_entries
|
||||
from .script_handlers import build_script_hook_handler
|
||||
from .workspace import load_workspace_hook_entries
|
||||
|
||||
|
||||
_loaded_hook_registrations: list[tuple[str, HookHandler]] = []
|
||||
|
||||
|
||||
def _reset_loaded_internal_hooks() -> None:
|
||||
while _loaded_hook_registrations:
|
||||
event_key, handler = _loaded_hook_registrations.pop()
|
||||
unregister_hook(event_key, handler)
|
||||
|
||||
|
||||
def _is_within_base(handler_path: str, base_dir: str) -> bool:
|
||||
try:
|
||||
Path(handler_path).resolve().relative_to(Path(base_dir).resolve())
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _load_module_from_path(module_path: str, unique_key: str) -> Optional[object]:
|
||||
|
|
@ -26,10 +44,43 @@ def _load_module_from_path(module_path: str, unique_key: str) -> Optional[object
|
|||
return mod
|
||||
|
||||
|
||||
def _resolve_legacy_handlers(config: dict[str, Any]) -> list[dict[str, str]]:
|
||||
rows = (((config.get("hooks") or {}).get("internal") or {}).get("handlers"))
|
||||
if not isinstance(rows, list):
|
||||
return []
|
||||
out: list[dict[str, str]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
event = str(row.get("event") or "").strip()
|
||||
module = str(row.get("module") or "").strip()
|
||||
export = str(row.get("export") or "default").strip() or "default"
|
||||
if not event or not module:
|
||||
continue
|
||||
out.append({"event": event, "module": module, "export": export})
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_workspace_module_path(*, workspace_dir: str, raw_module: str) -> Optional[str]:
|
||||
if not raw_module or raw_module.startswith("/") or raw_module.startswith("\\"):
|
||||
return None
|
||||
ws = Path(workspace_dir).resolve()
|
||||
module_path = (ws / raw_module).resolve()
|
||||
try:
|
||||
module_path.relative_to(ws)
|
||||
except Exception:
|
||||
return None
|
||||
if not module_path.exists() or not module_path.is_file():
|
||||
return None
|
||||
return str(module_path)
|
||||
|
||||
|
||||
def _resolve_handler(mod: object, export_name: str) -> Optional[HookHandler]:
|
||||
handler = getattr(mod, export_name, None)
|
||||
if callable(handler):
|
||||
return handler # type: ignore[return-value]
|
||||
normalized = str(export_name or "").strip()
|
||||
if normalized and normalized != "default":
|
||||
handler = getattr(mod, normalized, None)
|
||||
if callable(handler):
|
||||
return handler # type: ignore[return-value]
|
||||
# fallback: common names
|
||||
for candidate in ("handle", "handler", "main"):
|
||||
h = getattr(mod, candidate, None)
|
||||
|
|
@ -39,29 +90,41 @@ def _resolve_handler(mod: object, export_name: str) -> Optional[HookHandler]:
|
|||
|
||||
|
||||
def load_internal_hooks(
|
||||
config: Dict[str, Any],
|
||||
config: dict[str, Any],
|
||||
workspace_dir: str,
|
||||
*,
|
||||
managed_hooks_dir: Optional[str] = None,
|
||||
bundled_hooks_dir: Optional[str] = None,
|
||||
eligibility: Optional[HookEligibilityContext] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Discover python hooks and register them into the in-process registry.
|
||||
Discover hooks and register them into the in-process registry.
|
||||
|
||||
Hook file layout:
|
||||
Handler files (first match wins), see ``workspace._handler_candidates`` for the full list.
|
||||
|
||||
- ``.py``: in-process import (``handle`` / ``handler`` / ``main`` or ``metadata.oclaw.export``)
|
||||
- ``.ts`` / ``.mts`` / ``.cts`` (default): ``tsx`` + ``ts_hook_runner.ts`` (import ``default`` / ``handle`` or ``export``)
|
||||
- ``.mjs`` / ``.cjs`` (default): ``node`` + ``js_hook_runner.mjs`` (``import`` / ``require`` + same exports)
|
||||
- ``.sh`` / ``.bash``: subprocess: JSON on stdin, optional JSON on stdout (``context`` merge)
|
||||
- ``metadata.oclaw.hookMode: "script"`` (or legacy ``nodeScript: true``): run ``.ts`` / ``.mjs`` / ``.cjs`` as
|
||||
**stdin/stdout scripts** (no import runner) — use ``node`` for JS, ``tsx`` for TS
|
||||
|
||||
Hook layout (unchanged):
|
||||
<hookDir>/HOOK.md (YAML frontmatter with oclaw metadata)
|
||||
<hookDir>/handler.py (or index.py)
|
||||
|
||||
Metadata fields used:
|
||||
metadata.oclaw.events: ["type", "type:action", ...]
|
||||
metadata.oclaw.export: handler function name (default: "default" -> we map to "handle")
|
||||
"""
|
||||
_reset_loaded_internal_hooks()
|
||||
|
||||
# Hook system is on by default; only skip when explicitly disabled.
|
||||
if (((config.get("hooks") or {}).get("internal") or {}).get("enabled")) is False:
|
||||
return 0
|
||||
|
||||
entries = load_workspace_hook_entries(
|
||||
workspace_dir,
|
||||
config=config,
|
||||
managed_hooks_dir=managed_hooks_dir,
|
||||
bundled_hooks_dir=bundled_hooks_dir,
|
||||
extra_dirs=(((config.get("hooks") or {}).get("internal") or {}).get("load") or {}).get("extraDirs"),
|
||||
|
|
@ -69,35 +132,74 @@ def load_internal_hooks(
|
|||
|
||||
loaded = 0
|
||||
for idx, entry in enumerate(entries):
|
||||
state = resolve_hook_enable_state(entry, config)
|
||||
if not state.get("enabled"):
|
||||
if not should_include_hook_compat(entry=entry, config=config, eligibility=eligibility):
|
||||
continue
|
||||
entry_obj = ensure_hook_entry(entry)
|
||||
|
||||
md = entry.get("metadata") or {}
|
||||
md = entry_obj.metadata or {}
|
||||
events = md.get("events") if isinstance(md, dict) else None
|
||||
if not isinstance(events, list) or not events:
|
||||
continue
|
||||
|
||||
hook = entry.get("hook") or {}
|
||||
handler_path = hook.get("handlerPath")
|
||||
handler_path = entry_obj.hook.handlerPath
|
||||
base_dir = entry_obj.hook.baseDir
|
||||
if not isinstance(handler_path, str) or not handler_path:
|
||||
continue
|
||||
if not isinstance(base_dir, str) or not base_dir.strip():
|
||||
continue
|
||||
if not _is_within_base(handler_path, base_dir):
|
||||
continue
|
||||
|
||||
export_name = md.get("export") if isinstance(md, dict) else None
|
||||
if not isinstance(export_name, str) or not export_name.strip():
|
||||
export_name = "handle"
|
||||
export_name = "default"
|
||||
|
||||
mod = _load_module_from_path(handler_path, unique_key=str(idx))
|
||||
if mod is None:
|
||||
continue
|
||||
|
||||
handler = _resolve_handler(mod, export_name)
|
||||
suffix = Path(handler_path).suffix.lower()
|
||||
handler: Optional[HookHandler] = None
|
||||
if suffix in {".py"}:
|
||||
mod = _load_module_from_path(handler_path, unique_key=str(idx))
|
||||
if mod is None:
|
||||
continue
|
||||
handler = _resolve_handler(mod, export_name)
|
||||
else:
|
||||
oclaw_dict: dict[str, Any] = {}
|
||||
if isinstance(md, dict):
|
||||
for key in ("hookMode", "nodeScript"):
|
||||
if key in md:
|
||||
oclaw_dict[key] = md[key]
|
||||
handler = build_script_hook_handler(
|
||||
handler_path=handler_path,
|
||||
base_dir=base_dir,
|
||||
suffix=suffix,
|
||||
export_name=export_name,
|
||||
oclaw=oclaw_dict,
|
||||
)
|
||||
if handler is None:
|
||||
continue
|
||||
|
||||
for event_key in events:
|
||||
if isinstance(event_key, str) and event_key.strip():
|
||||
register_hook(event_key.strip(), handler)
|
||||
ek = event_key.strip()
|
||||
register_hook(ek, handler)
|
||||
_loaded_hook_registrations.append((ek, handler))
|
||||
loaded += 1
|
||||
|
||||
# Legacy config handlers (hooks.internal.handlers)
|
||||
for j, row in enumerate(_resolve_legacy_handlers(config)):
|
||||
safe_path = _resolve_workspace_module_path(workspace_dir=workspace_dir, raw_module=row["module"])
|
||||
if not safe_path:
|
||||
continue
|
||||
mod = _load_module_from_path(safe_path, unique_key=f"legacy_{j}")
|
||||
if mod is None:
|
||||
continue
|
||||
handler = _resolve_handler(mod, row.get("export") or "default")
|
||||
if handler is None:
|
||||
continue
|
||||
ev = row.get("event") or ""
|
||||
if not ev:
|
||||
continue
|
||||
register_hook(ev, handler)
|
||||
_loaded_hook_registrations.append((ev, handler))
|
||||
loaded += 1
|
||||
|
||||
return loaded
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue