feat: 路径约定体系 & 自优化管道落地

This commit is contained in:
oliver 2026-05-10 23:28:41 +08:00
parent 08df5d580a
commit df30b5902f
37 changed files with 239 additions and 324 deletions

View file

@ -42,7 +42,7 @@ from oclaw.runtime.session_auto_title import (
finalize_auto_title,
)
from oclaw.runtime.tools.base import ToolRegistry
from oclaw.runtime.tools.local_sdk import local_adapter_startup_self_check
from oclaw.runtime.tools.public.local_sdk import local_adapter_startup_self_check
logger = logging.getLogger(__name__)

View file

@ -154,6 +154,8 @@ def _run_dep_install(command: list[str], *, cwd: Path) -> tuple[bool, str]:
cwd=str(cwd),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=600,
check=False,
)

View file

@ -42,12 +42,13 @@ def _session_bootstrap_content_signature(*, skill_binding_role: str | None, work
targets: list[Path] = []
# session-bootstrap static source files
targets.append((repo / "runtime" / "skills" / "session-bootstrap" / "SOUL.md").resolve())
targets.append((repo / "runtime" / "skills" / "session-bootstrap" / "IDENTITY.md").resolve())
targets.append((repo / "skills" / "session-bootstrap" / "SOUL.md").resolve())
targets.append((repo / "skills" / "session-bootstrap" / "IDENTITY.md").resolve())
# wiki global memory/improvement sources
targets.append((repo / "data" / "wiki" / "improvement" / "learnings.md").resolve())
targets.append((repo / "data" / "wiki" / "improvement" / "errors.md").resolve())
targets.append((repo / "data" / "wiki" / "improvement" / "feature-requests.md").resolve())
targets.append((repo / "data" / "wiki" / "users" / "current.md").resolve())
core_dir = (repo / "data" / "wiki" / "core").resolve()
if core_dir.exists() and core_dir.is_dir():
@ -99,6 +100,7 @@ def _unified_skill_policy_guidance() -> str:
# Global policy for all agents (including dynamic/ephemeral) — appended to base_system in build_executor_system_prompt.
return (
"## 技能使用政策(Skill Usage Policy):\n"
"- 每次会话启动后,在进行文件/路径相关操作前,必须先读取 `skills/_workspace/public/path-convention/SKILL.md` 了解当前路径规范。\n"
"- 如果用户问你有哪些技能(skill/技能),请直接根据已注入的技能目录及其 description 回答。\n"
"- 不要为了“列出技能”而去读取 SKILL.md。只有在你确实需要某个技能的详细使用说明时,才读取对应 SKILL.md。\n"
"- 当你需要技能细节时,请按目录中给出的 path 读取对应的 SKILL.md。\n"

View file

@ -1,19 +0,0 @@
from __future__ import annotations
# Canonical memory expert tools live under this directory.
from .wiki_tools import (
memory_wiki_apply_tool,
memory_wiki_get_tool,
memory_wiki_lint_tool,
memory_wiki_search_tool,
memory_wiki_status_tool,
)
__all__ = [
"memory_wiki_apply_tool",
"memory_wiki_get_tool",
"memory_wiki_lint_tool",
"memory_wiki_search_tool",
"memory_wiki_status_tool",
]

View file

@ -1,5 +0,0 @@
from __future__ import annotations
from .adapter import LocalAdapter, LocalAdapterError, get_local_adapter, local_adapter_startup_self_check
__all__ = ["LocalAdapter", "LocalAdapterError", "get_local_adapter", "local_adapter_startup_self_check"]

View file

@ -1,30 +0,0 @@
from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
def cd_tool() -> ToolSpec:
def _handler(args: dict[str, Any]) -> dict[str, Any]:
cwd = str(args.get("cwd") or "").strip()
return get_local_adapter().cd(cwd=cwd)
return ToolSpec(
name="cd",
description="Change current working directory for local tools (best-effort, per-process).",
parameters={
"type": "object",
"properties": {"cwd": {"type": "string", "description": "Directory to change into."}},
"required": ["cwd"],
"additionalProperties": False,
},
handler=_handler,
tags=frozenset({"public", "local", "write"}),
risk_level="high",
timeout_s=5.0,
)
__all__ = ["cd_tool"]

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def delete_file_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def edit_file_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def get_cwd_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def get_env_tool() -> ToolSpec:

View file

@ -15,6 +15,8 @@ def _git(command: str, *, cwd: str) -> dict[str, Any]:
shell=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=60.0,
)
out = (cp.stdout or "") + (("\n" + cp.stderr) if cp.stderr else "")

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def kill_process_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def list_directory_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def list_processes_tool() -> ToolSpec:

View file

@ -30,8 +30,8 @@ class LocalAdapter:
def get_cwd(self) -> dict[str, Any]:
try:
p = resolve_workspace_path(self._cwd or ".")
return {"ok": True, "cwd": str(p)}
root = resolve_workspace_path(".")
return {"ok": True, "cwd": str(root)}
except Exception as exc:
return {"ok": False, "error_code": "local_execution_error", "error": f"{type(exc).__name__}: {exc}"}
@ -105,8 +105,8 @@ class LocalAdapter:
try:
timeout_s = max(1, min(int(timeout or 300), 600))
# run_command never follows adapter cd state.
# It only uses explicit cwd; otherwise defaults to data/workspace.
workdir = str(resolve_workspace_path(cwd or "data/workspace"))
# It only uses explicit cwd; otherwise defaults to workspace root.
workdir = str(resolve_workspace_path(cwd or "."))
Path(workdir).mkdir(parents=True, exist_ok=True)
# Use UTF-8 for decoded streams: Windows defaults (e.g. GBK) break on UTF-8-only bytes from
# curl/wttr.in, git, ripgrep, etc.; subprocess may leave stdout None after UnicodeDecodeError.
@ -370,6 +370,8 @@ class LocalAdapter:
shell=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=10.0,
)
out = str(cp.stdout or "")
@ -393,6 +395,8 @@ class LocalAdapter:
shell=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=10.0,
)
for line in str(cp.stdout or "").splitlines():
@ -418,7 +422,7 @@ class LocalAdapter:
try:
if os.name == "nt":
cmd = f"taskkill /PID {p} " + ("/F" if force else "")
cp = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10.0)
cp = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10.0)
ok = cp.returncode == 0
return {"ok": ok, "pid": p, "stdout": str(cp.stdout or ""), "stderr": str(cp.stderr or ""), "exit_code": int(cp.returncode)}
import signal
@ -462,5 +466,3 @@ def local_adapter_startup_self_check() -> dict[str, Any]:
"error": f"{type(exc).__name__}: {exc}",
}
__all__ = ["LocalAdapter", "LocalAdapterError", "get_local_adapter", "local_adapter_startup_self_check"]

View file

@ -31,8 +31,6 @@ def _wiki_handlers() -> dict[str, Callable[[dict[str, Any]], dict[str, Any]]]:
if spec is None or spec.loader is None:
return {}
mod = importlib.util.module_from_spec(spec)
# Register module before exec so decorators (e.g., @dataclass) can resolve
# cls.__module__ via sys.modules during import-time processing.
sys.modules[spec.name] = mod
spec.loader.exec_module(mod) # type: ignore[assignment]
fn = getattr(mod, "build_wiki_tool_specs", None)
@ -67,7 +65,8 @@ def _status_tool(public_name: str, desc: str) -> ToolSpec:
description=desc,
parameters={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
handler=lambda args: _delegate("wiki_status", args),
tags=frozenset({"memory", "wiki", "curator"}),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
@ -87,7 +86,8 @@ def _get_tool(public_name: str, desc: str) -> ToolSpec:
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_get", args),
tags=frozenset({"memory", "wiki", "curator"}),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
@ -108,7 +108,8 @@ def _search_tool(public_name: str, desc: str) -> ToolSpec:
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_search", args),
tags=frozenset({"memory", "wiki", "curator"}),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
@ -124,7 +125,8 @@ def _lint_tool(public_name: str, desc: str) -> ToolSpec:
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_lint", args),
tags=frozenset({"memory", "wiki", "curator"}),
tags=frozenset({"wiki", "curator"}),
risk_level="low",
read_only=True,
)
@ -144,8 +146,8 @@ def _apply_tool(public_name: str, desc: str) -> ToolSpec:
"additionalProperties": False,
},
handler=lambda args: _delegate("wiki_apply", args),
tags=frozenset({"memory", "wiki", "curator", "write"}),
risk_level="high",
tags=frozenset({"wiki", "curator", "write"}),
risk_level="low",
read_only=False,
)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def mkdir_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def move_file_tool() -> ToolSpec:

View file

@ -20,7 +20,7 @@ from oclaw.runtime.tools.base import ToolSpec
def _ffmpeg_exists() -> bool:
try:
p = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, timeout=3)
p = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3)
return p.returncode == 0
except Exception:
return False
@ -41,6 +41,8 @@ def _ffprobe_json(path: Path) -> dict[str, Any] | None:
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=8,
)
if p.returncode != 0:
@ -179,6 +181,8 @@ def query_video_attachment_tool() -> ToolSpec:
["ffmpeg", "-y", "-i", str(p), "-vn", "-ac", "1", "-ar", "16000", str(wav)],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=60,
)
if not wav.exists() or wav.stat().st_size <= 0:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def run_command_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def search_files_tool() -> ToolSpec:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from oclaw.runtime.tools.base import ToolSpec
from oclaw.runtime.tools.local_sdk import get_local_adapter
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
def set_env_tool() -> ToolSpec:

View file

@ -156,6 +156,8 @@ def _run(argv: list[str], *, cwd: Path, stdin_json: dict[str, Any], timeout_s: f
argv,
input=json.dumps(stdin_json, ensure_ascii=False),
text=True,
encoding="utf-8",
errors="replace",
capture_output=True,
cwd=str(cwd),
timeout=max(1, int(timeout_s)),