oclaw/interfaces/gateway/server_methods/config.py
oliver 9d2900db02 完成运行时与启动链路的全面相对路径整改,统一以仓库根为路径基准并移除 oclaw 双层硬编码。
同时补齐路径回归测试与开源自检文档,确保从非仓库 cwd 启动时 workspace 根、配置和 wiki 路径仍稳定可用。

Made-with: Cursor
2026-04-26 09:22:42 +08:00

169 lines
5.8 KiB
Python

from __future__ import annotations
import json
import os
from dataclasses import dataclass
from typing import Any
from .shared_types import GatewayRequestHandlers
from .validation import assert_valid_params, error_shape
@dataclass(frozen=True)
class _ConfigSnapshot:
exists: bool
valid: bool
config: dict[str, Any] | None
raw: str | None = None
def _read_config_file_snapshot() -> _ConfigSnapshot:
"""Staging adapter.
The upstream TS version reads JSON5 from Oclaw config root.
Here we keep a minimal in-memory placeholder until full config I/O is ported.
"""
return _ConfigSnapshot(exists=False, valid=True, config={})
def _load_schema_with_plugins() -> dict[str, Any]:
# Placeholder for `loadGatewayRuntimeConfigSchema`.
return {"schema": "stub", "uiHints": {}}
def _validate_config_get_params(params: Any) -> bool:
return isinstance(params, dict) or params is None
def _validate_config_schema_params(params: Any) -> bool:
return isinstance(params, dict) or params is None
def _validate_config_schema_lookup_params(params: Any) -> bool:
return isinstance(params, dict) and isinstance(params.get("path"), str) and bool(params["path"].strip())
def _validate_config_set_params(params: Any) -> bool:
return isinstance(params, dict) and isinstance(params.get("raw"), str)
def _validate_config_patch_params(params: Any) -> bool:
return isinstance(params, dict) and isinstance(params.get("raw"), str)
def _validate_config_apply_params(params: Any) -> bool:
return isinstance(params, dict) and isinstance(params.get("raw"), str)
def _config_get_handler(opts: dict[str, Any]) -> Any:
respond = opts.get("respond")
params = opts.get("params")
if not callable(respond):
return None
if not assert_valid_params(params, _validate_config_get_params, "config.get", respond):
return None
snapshot = _read_config_file_snapshot()
schema = _load_schema_with_plugins()
respond(True, {"snapshot": snapshot.config, "schema": schema}, None, None)
return None
def _config_schema_handler(opts: dict[str, Any]) -> Any:
respond = opts.get("respond")
params = opts.get("params")
if not callable(respond):
return None
if not assert_valid_params(params, _validate_config_schema_params, "config.schema", respond):
return None
respond(True, _load_schema_with_plugins(), None, None)
return None
def _config_schema_lookup_handler(opts: dict[str, Any]) -> Any:
respond = opts.get("respond")
context = opts.get("context") or {}
params = opts.get("params")
if not callable(respond):
return None
if not assert_valid_params(params, _validate_config_schema_lookup_params, "config.schema.lookup", respond):
return None
path = str((params or {}).get("path") or "").strip()
schema = _load_schema_with_plugins()
# Minimal lookup: dotted path fetch from schema dict.
cur: Any = schema
for seg in path.split("."):
if not isinstance(cur, dict) or seg not in cur:
respond(False, None, error_shape("INVALID_REQUEST", "config schema path not found"), None)
return None
cur = cur[seg]
respond(True, {"path": path, "value": cur}, None, None)
_ = context
return None
def _parse_raw_json_or_error(raw: str) -> tuple[bool, dict[str, Any] | None, str | None]:
try:
obj = json.loads(raw)
except Exception as exc:
return (False, None, f"invalid json: {exc}")
if not isinstance(obj, dict):
return (False, None, "raw must be a json object")
return (True, obj, None)
def _config_set_like_handler(opts: dict[str, Any], method: str) -> Any:
respond = opts.get("respond")
params = opts.get("params")
if not callable(respond):
return None
validator = _validate_config_set_params if method == "config.set" else _validate_config_apply_params
if not assert_valid_params(params, validator, method, respond):
return None
raw = str((params or {}).get("raw") or "")
ok, obj, err = _parse_raw_json_or_error(raw)
if not ok or obj is None:
respond(False, None, error_shape("INVALID_REQUEST", err or "invalid config"), None)
return None
respond(True, {"ok": True, "config": obj}, None, None)
return None
def _config_patch_handler(opts: dict[str, Any]) -> Any:
respond = opts.get("respond")
params = opts.get("params")
if not callable(respond):
return None
if not assert_valid_params(params, _validate_config_patch_params, "config.patch", respond):
return None
# Staging patch: treat raw as full next config.
raw = str((params or {}).get("raw") or "")
ok, obj, err = _parse_raw_json_or_error(raw)
if not ok or obj is None:
respond(False, None, error_shape("INVALID_REQUEST", err or "invalid config"), None)
return None
respond(True, {"ok": True, "noop": False, "config": obj}, None, None)
return None
def _config_open_file_handler(opts: dict[str, Any]) -> Any:
respond = opts.get("respond")
params = opts.get("params")
if not callable(respond):
return None
if not assert_valid_params(params, _validate_config_get_params, "config.openFile", respond):
return None
config_path = os.getenv("OCLAW_CONFIG_PATH") or "oclaw.json"
respond(True, {"ok": True, "path": config_path}, None, None)
return None
config_handlers: GatewayRequestHandlers = {
"config.get": _config_get_handler,
"config.schema": _config_schema_handler,
"config.schema.lookup": _config_schema_lookup_handler,
"config.set": lambda opts: _config_set_like_handler(opts, "config.set"),
"config.patch": _config_patch_handler,
"config.apply": lambda opts: _config_set_like_handler(opts, "config.apply"),
"config.openFile": _config_open_file_handler,
}