oclaw/runtime/hooks/hook_manifest_core.py
oliver dbbe3add6a 重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
2026-04-26 08:34:33 +08:00

195 lines
6 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True)
class HookInstallSpec:
kind: str
id: str | None = None
label: str | None = None
package: str | None = None
repository: str | None = None
bins: tuple[str, ...] = ()
def as_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {"kind": self.kind}
if self.id:
out["id"] = self.id
if self.label:
out["label"] = self.label
if self.package:
out["package"] = self.package
if self.repository:
out["repository"] = self.repository
if self.bins:
out["bins"] = list(self.bins)
return out
@dataclass(frozen=True)
class HookRequiresSpec:
bins: tuple[str, ...] = ()
any_bins: tuple[str, ...] = ()
env: tuple[str, ...] = ()
config: tuple[str, ...] = ()
def as_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {}
if self.bins:
out["bins"] = list(self.bins)
if self.any_bins:
out["anyBins"] = list(self.any_bins)
if self.env:
out["env"] = list(self.env)
if self.config:
out["config"] = list(self.config)
return out
@dataclass(frozen=True)
class HookMetadataSpec:
events: tuple[str, ...] = ()
always: bool | None = None
emoji: str | None = None
homepage: str | None = None
hook_key: str | None = None
export: str | None = None
os: tuple[str, ...] = ()
requires: HookRequiresSpec | None = None
install: tuple[HookInstallSpec, ...] = ()
hook_mode: str | None = None
node_script: bool | None = None
def as_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {"events": list(self.events)}
if self.always is not None:
out["always"] = bool(self.always)
if self.emoji:
out["emoji"] = self.emoji
if self.homepage:
out["homepage"] = self.homepage
if self.hook_key:
out["hookKey"] = self.hook_key
if self.export:
out["export"] = self.export
if self.os:
out["os"] = list(self.os)
if self.requires:
req = self.requires.as_dict()
if req:
out["requires"] = req
if self.install:
out["install"] = [row.as_dict() for row in self.install]
if self.hook_mode is not None:
out["hookMode"] = str(self.hook_mode)
if self.node_script is not None:
out["nodeScript"] = bool(self.node_script)
return out
@dataclass(frozen=True)
class ParsedHookManifest:
name: str
description: str
invocation_enabled: bool
metadata: HookMetadataSpec = field(default_factory=HookMetadataSpec)
def _read_str(value: Any) -> str | None:
if isinstance(value, str):
s = value.strip()
return s if s else None
return None
def _normalize_str_list(value: Any) -> tuple[str, ...]:
if not isinstance(value, list):
return ()
out: list[str] = []
for row in value:
s = _read_str(row)
if s:
out.append(s)
return tuple(out)
def _parse_install_spec(row: Any) -> HookInstallSpec | None:
if not isinstance(row, dict):
return None
kind = (_read_str(row.get("kind")) or "").lower()
if kind not in {"bundled", "npm", "git"}:
return None
return HookInstallSpec(
kind=kind,
id=_read_str(row.get("id")),
label=_read_str(row.get("label")),
package=_read_str(row.get("package")),
repository=_read_str(row.get("repository")),
bins=_normalize_str_list(row.get("bins")),
)
def _resolve_raw_oclaw_metadata(frontmatter: dict[str, Any]) -> dict[str, Any]:
meta = frontmatter.get("metadata")
if isinstance(meta, str):
try:
parsed = json.loads(meta)
meta = parsed if isinstance(parsed, dict) else None
except Exception:
meta = None
if not isinstance(meta, dict):
return {}
oc = meta.get("oclaw")
return oc if isinstance(oc, dict) else {}
def parse_hook_manifest(*, frontmatter: dict[str, Any], default_name: str) -> ParsedHookManifest:
name = _read_str(frontmatter.get("name")) or default_name
description = _read_str(frontmatter.get("description")) or ""
enabled_raw = frontmatter.get("enabled")
invocation_enabled = bool(enabled_raw) if isinstance(enabled_raw, bool) else True
oc = _resolve_raw_oclaw_metadata(frontmatter)
requires_obj = oc.get("requires") if isinstance(oc.get("requires"), dict) else {}
requires = HookRequiresSpec(
bins=_normalize_str_list(requires_obj.get("bins")),
any_bins=_normalize_str_list(requires_obj.get("anyBins")),
env=_normalize_str_list(requires_obj.get("env")),
config=_normalize_str_list(requires_obj.get("config")),
)
install_rows: list[HookInstallSpec] = []
if isinstance(oc.get("install"), list):
for row in oc["install"]:
parsed = _parse_install_spec(row)
if parsed:
install_rows.append(parsed)
node_script: bool | None
if "nodeScript" in oc:
node_script = bool(oc.get("nodeScript"))
else:
node_script = None
metadata = HookMetadataSpec(
events=_normalize_str_list(oc.get("events")),
always=bool(oc["always"]) if isinstance(oc.get("always"), bool) else None,
emoji=_read_str(oc.get("emoji")),
homepage=_read_str(oc.get("homepage")),
hook_key=_read_str(oc.get("hookKey")),
export=_read_str(oc.get("export")),
os=_normalize_str_list(oc.get("os")),
requires=requires if requires.as_dict() else None,
install=tuple(install_rows),
hook_mode=_read_str(oc.get("hookMode")),
node_script=node_script,
)
return ParsedHookManifest(
name=name,
description=description,
invocation_enabled=invocation_enabled,
metadata=metadata,
)