mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 07:03:15 +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
|
|
@ -2,15 +2,13 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Literal, Optional, Sequence, Tuple
|
||||
from typing import Any, List, Optional, Sequence, Tuple
|
||||
|
||||
from .frontmatter import parse_frontmatter, resolve_oclaw_metadata
|
||||
from .policy import HookSource, resolve_hook_entries
|
||||
|
||||
|
||||
HookEntry = Dict[str, Any]
|
||||
from .frontmatter import parse_frontmatter
|
||||
from .hook_manifest_core import parse_hook_manifest
|
||||
from .hook_types import HookEntry, HookInvocation, HookRef, HookSource
|
||||
from .policy import resolve_hook_entries
|
||||
|
||||
|
||||
def _read_text(path: Path) -> Optional[str]:
|
||||
|
|
@ -28,7 +26,132 @@ def _safe_is_dir(p: Path) -> bool:
|
|||
|
||||
|
||||
def _handler_candidates() -> Tuple[str, ...]:
|
||||
return ("handler.py", "index.py")
|
||||
# One file per hook dir. Order: py → TypeScript (runner) → JS (node runner) → shell.
|
||||
return (
|
||||
"handler.py",
|
||||
"index.py",
|
||||
"handler.ts",
|
||||
"index.ts",
|
||||
"handler.mts",
|
||||
"index.mts",
|
||||
"handler.cts",
|
||||
"index.cts",
|
||||
"handler.mjs",
|
||||
"index.mjs",
|
||||
"handler.cjs",
|
||||
"index.cjs",
|
||||
"handler.sh",
|
||||
"index.sh",
|
||||
"handler.bash",
|
||||
"index.bash",
|
||||
)
|
||||
|
||||
|
||||
def _resolve_contained_dir(base_dir: Path, target_dir: str) -> Optional[Path]:
|
||||
try:
|
||||
resolved = (base_dir / target_dir).resolve()
|
||||
resolved.relative_to(base_dir.resolve())
|
||||
return resolved
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_package_hook_paths(package_dir: Path) -> List[str]:
|
||||
"""
|
||||
Support package.json manifest declarations:
|
||||
- { "openclaw": { "hooks": [...] } }
|
||||
- { "oclaw": { "hooks": [...] } }
|
||||
"""
|
||||
manifest = package_dir / "package.json"
|
||||
raw = _read_text(manifest)
|
||||
if raw is None:
|
||||
return []
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
except Exception:
|
||||
return []
|
||||
if not isinstance(obj, dict):
|
||||
return []
|
||||
out: List[str] = []
|
||||
for key in ("openclaw", "oclaw"):
|
||||
row = obj.get(key)
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
hooks = row.get("hooks")
|
||||
if not isinstance(hooks, list):
|
||||
continue
|
||||
for it in hooks:
|
||||
s = str(it or "").strip()
|
||||
if s:
|
||||
out.append(s)
|
||||
if out:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_plugin_hook_dirs(*, workspace_dir: str, config: Optional[dict[str, Any]]) -> List[tuple[str, str]]:
|
||||
"""
|
||||
Lightweight Python parity for OpenClaw plugin hook discovery.
|
||||
|
||||
Discover plugin bundles under:
|
||||
<workspace>/.openclaw/extensions/<plugin-id>/.codex-plugin/plugin.json
|
||||
and read `hooks` from that manifest (string path or list[str]).
|
||||
"""
|
||||
ws = Path(workspace_dir).resolve()
|
||||
ext_root = ws / ".openclaw" / "extensions"
|
||||
if not ext_root.exists() or not ext_root.is_dir():
|
||||
return []
|
||||
|
||||
enabled_entries = (
|
||||
(((config or {}).get("plugins") or {}).get("entries") or {})
|
||||
if isinstance((((config or {}).get("plugins") or {}).get("entries") or {}), dict)
|
||||
else {}
|
||||
)
|
||||
|
||||
out: List[tuple[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
for plugin_root in ext_root.iterdir():
|
||||
if not plugin_root.is_dir():
|
||||
continue
|
||||
plugin_id = plugin_root.name
|
||||
state = enabled_entries.get(plugin_id) if isinstance(enabled_entries, dict) else None
|
||||
if isinstance(state, dict) and state.get("enabled") is False:
|
||||
continue
|
||||
|
||||
manifest_path = plugin_root / ".codex-plugin" / "plugin.json"
|
||||
raw = _read_text(manifest_path)
|
||||
if raw is None:
|
||||
continue
|
||||
try:
|
||||
manifest = json.loads(raw)
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(manifest, dict):
|
||||
continue
|
||||
hooks_value = manifest.get("hooks")
|
||||
hook_paths: List[str] = []
|
||||
if isinstance(hooks_value, str):
|
||||
s = hooks_value.strip()
|
||||
if s:
|
||||
hook_paths.append(s)
|
||||
elif isinstance(hooks_value, list):
|
||||
for row in hooks_value:
|
||||
s = str(row or "").strip()
|
||||
if s:
|
||||
hook_paths.append(s)
|
||||
if not hook_paths:
|
||||
continue
|
||||
|
||||
for rel in hook_paths:
|
||||
resolved = _resolve_contained_dir(plugin_root, rel)
|
||||
if resolved is None:
|
||||
continue
|
||||
key = str(resolved)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append((key, plugin_id))
|
||||
return out
|
||||
|
||||
|
||||
def _load_hook_from_dir(hook_dir: Path, source: HookSource, plugin_id: Optional[str] = None) -> Optional[HookEntry]:
|
||||
|
|
@ -39,12 +162,9 @@ def _load_hook_from_dir(hook_dir: Path, source: HookSource, plugin_id: Optional[
|
|||
|
||||
parsed = parse_frontmatter(content)
|
||||
fm = parsed.frontmatter
|
||||
name = fm.get("name") or hook_dir.name
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
name = hook_dir.name
|
||||
description = fm.get("description") or ""
|
||||
if not isinstance(description, str):
|
||||
description = ""
|
||||
manifest = parse_hook_manifest(frontmatter=fm, default_name=hook_dir.name)
|
||||
name = manifest.name
|
||||
description = manifest.description
|
||||
|
||||
handler_path: Optional[Path] = None
|
||||
for candidate in _handler_candidates():
|
||||
|
|
@ -55,21 +175,20 @@ def _load_hook_from_dir(hook_dir: Path, source: HookSource, plugin_id: Optional[
|
|||
if handler_path is None:
|
||||
return None
|
||||
|
||||
metadata = resolve_oclaw_metadata(fm) or {}
|
||||
|
||||
return {
|
||||
"hook": {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"source": source,
|
||||
"pluginId": plugin_id,
|
||||
"filePath": str(hook_md),
|
||||
"baseDir": str(hook_dir.resolve()),
|
||||
"handlerPath": str(handler_path.resolve()),
|
||||
},
|
||||
"frontmatter": fm,
|
||||
"metadata": metadata,
|
||||
}
|
||||
return HookEntry(
|
||||
hook=HookRef(
|
||||
name=name,
|
||||
description=description,
|
||||
source=source,
|
||||
pluginId=plugin_id,
|
||||
filePath=str(hook_md),
|
||||
baseDir=str(hook_dir.resolve()),
|
||||
handlerPath=str(handler_path.resolve()),
|
||||
),
|
||||
frontmatter=dict(fm),
|
||||
metadata=manifest.metadata.as_dict(),
|
||||
invocation=HookInvocation(enabled=bool(manifest.invocation_enabled)),
|
||||
)
|
||||
|
||||
|
||||
def load_hook_entries_from_dir(dir_path: str, source: HookSource, plugin_id: Optional[str] = None) -> List[HookEntry]:
|
||||
|
|
@ -81,6 +200,17 @@ def load_hook_entries_from_dir(dir_path: str, source: HookSource, plugin_id: Opt
|
|||
for child in base.iterdir():
|
||||
if not child.is_dir():
|
||||
continue
|
||||
package_hook_paths = _parse_package_hook_paths(child)
|
||||
if package_hook_paths:
|
||||
for rel in package_hook_paths:
|
||||
hook_dir = _resolve_contained_dir(child, rel)
|
||||
if hook_dir is None:
|
||||
continue
|
||||
entry = _load_hook_from_dir(hook_dir, source=source, plugin_id=plugin_id)
|
||||
if entry:
|
||||
out.append(entry)
|
||||
continue
|
||||
|
||||
entry = _load_hook_from_dir(child, source=source, plugin_id=plugin_id)
|
||||
if entry:
|
||||
out.append(entry)
|
||||
|
|
@ -92,6 +222,7 @@ def load_hook_entries_from_dir(dir_path: str, source: HookSource, plugin_id: Opt
|
|||
def discover_workspace_hook_entries(
|
||||
workspace_dir: str,
|
||||
*,
|
||||
config: Optional[dict[str, Any]] = None,
|
||||
managed_hooks_dir: Optional[str] = None,
|
||||
bundled_hooks_dir: Optional[str] = None,
|
||||
extra_dirs: Optional[Sequence[str]] = None,
|
||||
|
|
@ -118,6 +249,9 @@ def discover_workspace_hook_entries(
|
|||
if bundled_hooks_dir:
|
||||
entries.extend(load_hook_entries_from_dir(bundled_hooks_dir, source="oclaw-bundled"))
|
||||
|
||||
for plugin_dir, plugin_id in _resolve_plugin_hook_dirs(workspace_dir=workspace_dir, config=config):
|
||||
entries.extend(load_hook_entries_from_dir(plugin_dir, source="oclaw-plugin", plugin_id=plugin_id))
|
||||
|
||||
entries.extend(load_hook_entries_from_dir(str(managed), source="oclaw-managed"))
|
||||
entries.extend(load_hook_entries_from_dir(str(workspace_hooks), source="oclaw-workspace"))
|
||||
return entries
|
||||
|
|
@ -126,12 +260,14 @@ def discover_workspace_hook_entries(
|
|||
def load_workspace_hook_entries(
|
||||
workspace_dir: str,
|
||||
*,
|
||||
config: Optional[dict[str, Any]] = None,
|
||||
managed_hooks_dir: Optional[str] = None,
|
||||
bundled_hooks_dir: Optional[str] = None,
|
||||
extra_dirs: Optional[Sequence[str]] = None,
|
||||
) -> List[HookEntry]:
|
||||
discovered = discover_workspace_hook_entries(
|
||||
workspace_dir,
|
||||
config=config,
|
||||
managed_hooks_dir=managed_hooks_dir,
|
||||
bundled_hooks_dir=bundled_hooks_dir,
|
||||
extra_dirs=extra_dirs,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue