mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 07:20:44 +08:00
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。 Made-with: Cursor
146 lines
4 KiB
Python
146 lines
4 KiB
Python
from __future__ import annotations
|
|
|
|
import fnmatch
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Iterable, List, Sequence
|
|
|
|
|
|
HOOK_KEY = "bootstrap-extra-files"
|
|
_ALLOWED_BASENAMES = {
|
|
"AGENTS.md",
|
|
"SOUL.md",
|
|
"TOOLS.md",
|
|
"IDENTITY.md",
|
|
"USER.md",
|
|
"HEARTBEAT.md",
|
|
"BOOTSTRAP.md",
|
|
"MEMORY.md",
|
|
"memory.md",
|
|
}
|
|
|
|
|
|
def _resolve_hook_cfg(cfg: Any) -> Dict[str, Any]:
|
|
if not isinstance(cfg, dict):
|
|
return {}
|
|
hooks = cfg.get("hooks") if isinstance(cfg.get("hooks"), dict) else {}
|
|
internal = hooks.get("internal") if isinstance(hooks.get("internal"), dict) else {}
|
|
entries = internal.get("entries") if isinstance(internal.get("entries"), dict) else {}
|
|
row = entries.get(HOOK_KEY)
|
|
return row if isinstance(row, dict) else {}
|
|
|
|
|
|
def _string_list(v: Any) -> List[str]:
|
|
if isinstance(v, list):
|
|
out = []
|
|
for x in v:
|
|
s = str(x or "").strip()
|
|
if s:
|
|
out.append(s)
|
|
return out
|
|
if isinstance(v, str):
|
|
s = v.strip()
|
|
return [s] if s else []
|
|
return []
|
|
|
|
|
|
def _patterns(hook_cfg: Dict[str, Any]) -> List[str]:
|
|
for k in ("paths", "patterns", "files"):
|
|
got = _string_list(hook_cfg.get(k))
|
|
if got:
|
|
return got
|
|
return []
|
|
|
|
|
|
def _is_within(root: Path, candidate: Path) -> bool:
|
|
try:
|
|
candidate.resolve().relative_to(root.resolve())
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _glob_paths(root: Path, patterns: Sequence[str]) -> List[Path]:
|
|
"""
|
|
We intentionally do not use Path.glob on arbitrary patterns that might escape roots via '..'.
|
|
Instead: enumerate candidates by rglob and fnmatch on posix-style relative paths.
|
|
"""
|
|
if not root.exists() or not root.is_dir():
|
|
return []
|
|
|
|
# Pre-normalize patterns to forward-slash for fnmatch
|
|
raw_pats = [p.replace("\\", "/").lstrip("/") for p in patterns if str(p or "").strip()]
|
|
pats: list[str] = []
|
|
for pat in raw_pats:
|
|
pats.append(pat)
|
|
# Python's fnmatch doesn't treat "**/" as "zero-or-more directories".
|
|
# Add a compatibility variant so "**/AGENTS.md" matches "AGENTS.md" as well.
|
|
if pat.startswith("**/") and len(pat) > 3:
|
|
pats.append(pat[3:])
|
|
if not pats:
|
|
return []
|
|
|
|
out: List[Path] = []
|
|
try:
|
|
for p in root.rglob("*"):
|
|
if not p.is_file():
|
|
continue
|
|
if p.name not in _ALLOWED_BASENAMES:
|
|
continue
|
|
rel = p.relative_to(root).as_posix()
|
|
if any(fnmatch.fnmatch(rel, pat) for pat in pats):
|
|
out.append(p)
|
|
except Exception:
|
|
return out
|
|
|
|
# deterministic order
|
|
out.sort(key=lambda x: x.as_posix())
|
|
return out
|
|
|
|
|
|
def handle(event: Any) -> None:
|
|
if getattr(event, "type", None) != "agent" or getattr(event, "action", None) != "bootstrap":
|
|
return
|
|
|
|
ctx = getattr(event, "context", None)
|
|
if not isinstance(ctx, dict):
|
|
return
|
|
|
|
hook_cfg = _resolve_hook_cfg(ctx.get("cfg"))
|
|
if hook_cfg.get("enabled") is False:
|
|
return
|
|
|
|
patterns = _patterns(hook_cfg)
|
|
if not patterns:
|
|
return
|
|
|
|
ws = ctx.get("workspaceDir")
|
|
if not isinstance(ws, str) or not ws.strip():
|
|
return
|
|
ws_root = Path(ws).expanduser()
|
|
if not ws_root.exists() or not ws_root.is_dir():
|
|
return
|
|
|
|
matches = _glob_paths(ws_root, patterns)
|
|
if not matches:
|
|
return
|
|
|
|
# Mutate context.bootstrapFiles (Oclaw-style).
|
|
boot = ctx.get("bootstrapFiles")
|
|
if not isinstance(boot, list):
|
|
boot = []
|
|
ctx["bootstrapFiles"] = boot
|
|
|
|
existing_paths = set()
|
|
for it in list(boot):
|
|
if isinstance(it, dict) and isinstance(it.get("path"), str):
|
|
existing_paths.add(it["path"])
|
|
elif isinstance(it, str):
|
|
existing_paths.add(it)
|
|
|
|
for p in matches:
|
|
ap = str(p.resolve())
|
|
if ap in existing_paths:
|
|
continue
|
|
boot.append({"path": ap, "name": p.name})
|
|
existing_paths.add(ap)
|
|
|