重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。

同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 08:34:33 +08:00
parent 4a23b715a2
commit dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions

View file

@ -0,0 +1,340 @@
from __future__ import annotations
import argparse
import copy
import json
import os
import sys
from typing import Any
_DEPREC_INSTALL = (
"oclaw: `hooks install` is deprecated and not implemented in the Python CLI.\n"
" • Copy a hook directory to ~/.oclaw/hooks/<name>/ or <workspace>/hooks/<name>/ (HOOK.md + handler).\n"
" • See runtime/hooks/README.md for layout and handler priority.\n"
" • If you use the OpenClaw app: use `openclaw plugins install` for packaged hook bundles.\n"
)
_DEPREC_UPDATE = (
"oclaw: `hooks update` is deprecated and not implemented in the Python CLI.\n"
" • Update hook sources in place under ~/.oclaw/hooks/ or your workspace hooks/ tree.\n"
" • If you use the OpenClaw app: use `openclaw plugins update`.\n"
)
from oclaw.platform.config.runtime_paths import runtime_hooks_bundled_root
from oclaw.runtime.hooks.config import should_include_hook
from oclaw.runtime.hooks.frontmatter import resolve_hook_key
from oclaw.runtime.hooks.hooks_status import build_workspace_hook_status
from oclaw.runtime.hooks.hook_types import HookEntry
from oclaw.runtime.hooks.merge_skill_hook_dirs import merge_skill_hook_extra_dirs_into_config
from oclaw.runtime.hooks.user_config_hooks import (
apply_hook_entry_enabled,
load_storage_config_document,
resolve_hooks_config_storage_path,
save_storage_config_document,
)
from oclaw.runtime.hooks.workspace import load_workspace_hook_entries
from oclaw.runtime.hooks_runtime import resolve_runtime_config
def _resolve_cli_workspace(ns: argparse.Namespace) -> str:
w = getattr(ns, "workspace", None)
if isinstance(w, str) and w.strip():
return w.strip()
return str(os.getenv("OCLAW_WORKSPACE") or os.getcwd()).strip() or "."
def prepare_hooks_cli_config(cfg: dict[str, Any] | None) -> dict[str, Any]:
base = dict(cfg or resolve_runtime_config() or {})
return merge_skill_hook_extra_dirs_into_config(base)
def build_hooks_status_report(workspace_dir: str, *, config: dict[str, Any] | None = None) -> dict[str, Any]:
cfg = prepare_hooks_cli_config(config)
extra = (((cfg.get("hooks") or {}).get("internal") or {}).get("load") or {}).get("extraDirs")
extra_dirs = [str(x) for x in extra] if isinstance(extra, list) else None
return build_workspace_hook_status(
workspace_dir,
config=cfg,
bundled_hooks_dir=str(runtime_hooks_bundled_root()),
extra_dirs=extra_dirs,
)
def _load_hook_entries_for_workspace(workspace_dir: str, *, config: dict[str, Any] | None = None) -> list[HookEntry]:
cfg = prepare_hooks_cli_config(config)
extra = (((cfg.get("hooks") or {}).get("internal") or {}).get("load") or {}).get("extraDirs")
extra_dirs = [str(x) for x in extra] if isinstance(extra, list) else None
return load_workspace_hook_entries(
workspace_dir,
config=cfg,
bundled_hooks_dir=str(runtime_hooks_bundled_root()),
extra_dirs=extra_dirs,
)
def find_hook_entry_for_name(workspace_dir: str, query: str, *, config: dict[str, Any] | None = None) -> HookEntry | None:
q = str(query or "").strip()
if not q:
return None
for e in _load_hook_entries_for_workspace(workspace_dir, config=config):
hook_key = resolve_hook_key(e.hook.name, e.as_dict())
if e.hook.name == q or hook_key == q:
return e
return None
def _status_row_for_entry(workspace_dir: str, entry: HookEntry, *, config: dict[str, Any] | None = None) -> dict[str, Any] | None:
hook_key = resolve_hook_key(entry.hook.name, entry.as_dict())
report = build_hooks_status_report(workspace_dir, config=config)
for h in report.get("hooks") or []:
if h.get("hook_key") == hook_key or h.get("name") == entry.hook.name:
return h if isinstance(h, dict) else None
return None
def _requirements_satisfied_if_entry_enabled(entry: HookEntry, *, config: dict[str, Any] | None = None) -> bool:
"""
Same gate as OpenClaw ``enableHook`` (``requireEligible`` → ``requirementsSatisfied``):
would this hook pass eligibility if its config entry were toggled on?
"""
cfg = copy.deepcopy(prepare_hooks_cli_config(config))
hook_key = resolve_hook_key(entry.hook.name, entry.as_dict())
hooks = cfg.setdefault("hooks", {})
internal = hooks.setdefault("internal", {})
if not isinstance(internal, dict):
hooks["internal"] = {}
internal = hooks["internal"]
internal["enabled"] = True
entries = internal.setdefault("entries", {})
if not isinstance(entries, dict):
internal["entries"] = {}
entries = internal["entries"]
prev = entries.get(hook_key)
base = dict(prev) if isinstance(prev, dict) else {}
base["enabled"] = True
entries[hook_key] = base
return bool(should_include_hook(entry=entry, config=cfg, eligibility=None))
def _cmd_hooks_list(args: argparse.Namespace) -> int:
report = build_hooks_status_report(_resolve_cli_workspace(args), config=None)
hooks = list(report.get("hooks") or [])
if getattr(args, "eligible", False):
hooks = [h for h in hooks if h.get("loadable")]
if args.json:
out = {
"workspace_dir": report.get("workspace_dir"),
"summary": report.get("summary"),
"hooks": hooks,
}
print(json.dumps(out, indent=2, default=str))
return 0
summary = report.get("summary") or {}
ready = int(summary.get("loadable_total") or 0)
total = int(summary.get("discovered_total") or 0)
print(f"Hooks ({ready}/{total} ready) workspace={report.get('workspace_dir')}")
for h in hooks:
if h.get("loadable"):
st = "ready"
elif not h.get("enabled_by_config"):
st = "disabled"
else:
st = "missing"
ev = ", ".join(str(x) for x in (h.get("events") or [])[:8])
print(f" [{st:8}] {h.get('name','')} source={h.get('source','')} events={ev}")
if getattr(args, "verbose", False):
br = h.get("blocked_reason") or ""
mb = h.get("missing_bins") or []
if br or mb:
print(f" blocked={br!r} missing_bins={mb}")
return 0
def _cmd_hooks_check(args: argparse.Namespace) -> int:
report = build_hooks_status_report(_resolve_cli_workspace(args), config=None)
hooks = list(report.get("hooks") or [])
eligible = [h for h in hooks if h.get("loadable")]
bad = [h for h in hooks if not h.get("loadable")]
if args.json:
print(
json.dumps(
{
"workspace_dir": report.get("workspace_dir"),
"summary": report.get("summary"),
"eligible": [h.get("name") for h in eligible],
"not_eligible": [
{"name": h.get("name"), "blocked_reason": h.get("blocked_reason"), "missing_bins": h.get("missing_bins")}
for h in bad
],
},
indent=2,
default=str,
)
)
return 0
s = report.get("summary") or {}
print("Hooks status")
print(f" total: {s.get('discovered_total', 0)}")
print(f" loadable: {s.get('loadable_total', 0)}")
print(f" blocked: {len(bad)}")
if bad:
print("Not loadable:")
for h in bad:
print(f" - {h.get('name')}: {h.get('blocked_reason') or 'missing requirements'} missing_bins={h.get('missing_bins')}")
return 0
def _cmd_hooks_info(args: argparse.Namespace) -> int:
report = build_hooks_status_report(_resolve_cli_workspace(args), config=None)
name = str(getattr(args, "name", "") or "").strip()
hooks = list(report.get("hooks") or [])
row = next((h for h in hooks if h.get("name") == name or h.get("hook_key") == name), None)
if row is None:
if args.json:
print(json.dumps({"error": "not_found", "hook": name}, indent=2))
else:
print(f'Hook "{name}" not found. Try: python -m oclaw.runtime.operations hooks list')
return 1
if args.json:
print(json.dumps(row, indent=2, default=str))
return 0
print(f"{row.get('name')} loadable={row.get('loadable')} source={row.get('source')}")
if row.get("plugin_id"):
print(f" plugin_id: {row.get('plugin_id')}")
print(f" events: {', '.join(str(x) for x in (row.get('events') or []))}")
print(f" enabled_by_config: {row.get('enabled_by_config')}")
print(f" eligible: {row.get('eligible')}")
if not row.get("loadable"):
print(f" blocked_reason: {row.get('blocked_reason')}")
print(f" missing_bins: {row.get('missing_bins')}")
opts = row.get("install_options") or []
if opts:
print(" install_options:")
for o in opts:
print(f" - {o.get('id')}: {o.get('kind')} {o.get('label')}")
return 0
def _cmd_hooks_enable(args: argparse.Namespace) -> int:
ws = _resolve_cli_workspace(args)
name = str(getattr(args, "name", "") or "").strip()
entry = find_hook_entry_for_name(ws, name, config=None)
if entry is None:
print(f'Hook "{name}" not found. Try: python -m oclaw.runtime.operations hooks list --workspace ...')
return 1
if entry.hook.source == "oclaw-plugin":
print("Plugin-managed hooks cannot be enabled via this CLI; enable or configure the plugin instead.")
return 1
hook_key = resolve_hook_key(entry.hook.name, entry.as_dict())
if not _requirements_satisfied_if_entry_enabled(entry, config=None):
row = _status_row_for_entry(ws, entry, config=None)
reason = (row or {}).get("blocked_reason") or "missing requirements"
print(f'Hook "{hook_key}" cannot be enabled: {reason}')
return 1
doc = load_storage_config_document()
apply_hook_entry_enabled(doc, hook_key, True, ensure_internal_hooks_enabled=True)
save_storage_config_document(doc)
print(f"Enabled hook {hook_key!r} (config: {resolve_hooks_config_storage_path()})")
return 0
def _cmd_hooks_disable(args: argparse.Namespace) -> int:
ws = _resolve_cli_workspace(args)
name = str(getattr(args, "name", "") or "").strip()
entry = find_hook_entry_for_name(ws, name, config=None)
if entry is None:
print(f'Hook "{name}" not found.')
return 1
if entry.hook.source == "oclaw-plugin":
print("Plugin-managed hooks cannot be disabled via this CLI; disable or configure the plugin instead.")
return 1
hook_key = resolve_hook_key(entry.hook.name, entry.as_dict())
doc = load_storage_config_document()
apply_hook_entry_enabled(doc, hook_key, False, ensure_internal_hooks_enabled=False)
save_storage_config_document(doc)
print(f"Disabled hook {hook_key!r} (config: {resolve_hooks_config_storage_path()})")
return 0
def _cmd_hooks_install(args: argparse.Namespace) -> int:
print(_DEPREC_INSTALL, file=sys.stderr, end="")
spec = str(getattr(args, "spec", "") or "").strip()
if not spec:
print("error: missing path or package spec", file=sys.stderr)
return 1
print(f"note: spec was not installed: {spec!r}", file=sys.stderr)
return 2
def _cmd_hooks_update(args: argparse.Namespace) -> int:
print(_DEPREC_UPDATE, file=sys.stderr, end="")
hid = getattr(args, "hook_id", None)
parts = []
if getattr(args, "dry_run", False):
parts.append("dry_run")
if getattr(args, "all", False):
parts.append("all")
if hid:
parts.append(f"id={hid!r}")
if parts:
print(f"note: no update performed ({', '.join(parts)})", file=sys.stderr)
else:
print("note: no update performed (pass hook id or --all)", file=sys.stderr)
return 2
def register_hooks_parser(root_sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
hooks = root_sub.add_parser("hooks", help="List and inspect internal hooks (discovery + eligibility)")
hooks_sub = hooks.add_subparsers(dest="hooks_cmd", required=True)
def _add_workspace(p: argparse.ArgumentParser) -> None:
p.add_argument(
"--workspace",
default=None,
help="Workspace root for discovery (default: OCLAW_WORKSPACE or current directory)",
)
p_list = hooks_sub.add_parser("list", help="List discovered hooks")
_add_workspace(p_list)
p_list.add_argument("--json", action="store_true", help="JSON output")
p_list.add_argument("--eligible", action="store_true", help="Only hooks that are loadable")
p_list.add_argument("-v", "--verbose", action="store_true", help="Include blocked_reason / missing_bins")
p_list.set_defaults(func=_cmd_hooks_list)
p_check = hooks_sub.add_parser("check", help="Summary of hook loadability")
_add_workspace(p_check)
p_check.add_argument("--json", action="store_true")
p_check.set_defaults(func=_cmd_hooks_check)
p_info = hooks_sub.add_parser("info", help="Show details for one hook by name or hook_key")
_add_workspace(p_info)
p_info.add_argument("name", help="Hook name (HOOK.md name) or hookKey")
p_info.add_argument("--json", action="store_true")
p_info.set_defaults(func=_cmd_hooks_info)
p_en = hooks_sub.add_parser("enable", help="Enable a hook (writes hooks.internal.entries.<key>.enabled)")
_add_workspace(p_en)
p_en.add_argument("name", help="Hook name or hookKey (must currently be loadable)")
p_en.set_defaults(func=_cmd_hooks_enable)
p_dis = hooks_sub.add_parser("disable", help="Disable a hook in config")
_add_workspace(p_dis)
p_dis.add_argument("name", help="Hook name or hookKey")
p_dis.set_defaults(func=_cmd_hooks_disable)
p_inst = hooks_sub.add_parser(
"install",
help="[deprecated] Install a hook pack (not implemented; prints migration hints)",
)
p_inst.add_argument("spec", help="npm spec, archive path, or directory (not processed)")
p_inst.add_argument("-l", "--link", action="store_true", help="Ignored (OpenClaw CLI compat)")
p_inst.add_argument("--pin", action="store_true", help="Ignored (OpenClaw CLI compat)")
p_inst.set_defaults(func=_cmd_hooks_install)
p_up = hooks_sub.add_parser(
"update",
help="[deprecated] Update hook packs (not implemented; prints migration hints)",
)
p_up.add_argument("hook_id", nargs="?", default=None, help="Hook pack id (optional if --all)")
p_up.add_argument("--all", action="store_true", help="Ignored except for message context")
p_up.add_argument("--dry-run", action="store_true", help="No-op; only prints deprecation")
p_up.set_defaults(func=_cmd_hooks_update)

View file

@ -4,6 +4,7 @@ import argparse
import os
import sys
from oclaw.runtime.operations.hooks_cmd import register_hooks_parser
from oclaw.runtime.operations.memory import register_memory_parser
from oclaw.runtime.operations.providers.registry import build_channel_registry
from oclaw.runtime.operations.stack import register_stack_parser
@ -43,6 +44,7 @@ def _build_parser() -> argparse.ArgumentParser:
register_memory_parser(root_sub)
register_stack_parser(root_sub)
register_hooks_parser(root_sub)
return p

View file

@ -65,6 +65,10 @@ def _is_running(pid: int) -> bool:
return False
def is_pid_running(pid: int) -> bool:
return _is_running(int(pid or 0))
def _python_bin() -> str:
return sys.executable or "python"
@ -176,10 +180,99 @@ def detect_orphan_service_processes(name: str, *, keep_pids: set[int] | None = N
return out
def list_service_process_pids(name: str) -> list[int]:
sig = _service_signature(name)
if not sig:
return []
return sorted(int(x) for x in _find_pids_by_signature(sig) if int(x) > 0)
def list_listen_ports_for_pid(pid: int) -> list[int]:
p = int(pid or 0)
if p <= 0:
return []
ports: set[int] = set()
if os.name == "nt":
try:
raw = subprocess.check_output(
["netstat", "-ano", "-p", "tcp"],
stderr=subprocess.DEVNULL,
text=True,
encoding="utf-8",
errors="replace",
)
except Exception:
return []
for line in (raw or "").splitlines():
txt = " ".join(str(line or "").strip().split())
if not txt or not txt.lower().startswith("tcp "):
continue
parts = txt.split(" ")
if len(parts) < 5:
continue
local_addr = parts[1]
state = parts[3].strip().upper()
pid_s = parts[4].strip()
if pid_s != str(p):
continue
if state != "LISTENING":
continue
sep = local_addr.rfind(":")
if sep < 0:
continue
port_s = local_addr[sep + 1 :].strip().strip("]")
if port_s.isdigit():
ports.add(int(port_s))
return sorted(ports)
try:
raw = subprocess.check_output(
["ss", "-ltnp"],
stderr=subprocess.DEVNULL,
text=True,
encoding="utf-8",
errors="replace",
)
except Exception:
return []
for line in (raw or "").splitlines():
txt = " ".join(str(line or "").strip().split())
if not txt or "LISTEN" not in txt:
continue
if f"pid={p}," not in txt and f"pid={p})" not in txt:
continue
parts = txt.split(" ")
if len(parts) < 4:
continue
local_addr = parts[3]
sep = local_addr.rfind(":")
if sep < 0:
continue
port_s = local_addr[sep + 1 :].strip().strip("]")
if port_s.isdigit():
ports.add(int(port_s))
return sorted(ports)
def cleanup_orphan_service_processes(name: str, *, keep_pids: set[int] | None = None) -> list[int]:
return _cleanup_orphan_service_processes(name, keep_pids=keep_pids)
def cleanup_service_processes_by_pid(name: str, pids: list[int] | tuple[int, ...] | set[int]) -> list[int]:
sig = _service_signature(name)
if not sig:
return []
target = {int(x) for x in (pids or []) if int(x) > 0}
if not target:
return []
live = set(_find_pids_by_signature(sig))
victims = sorted([x for x in target if x in live])
killed: list[int] = []
for pid in victims:
if _kill_pid_force(pid):
killed.append(int(pid))
return killed
@dataclass(frozen=True)
class ServiceState:
name: str
@ -191,12 +284,15 @@ class ServiceState:
def start_service(*, name: str, command: list[str], env: dict[str, str] | None = None, cwd: str | None = None) -> int:
state = _read_state()
services = state.setdefault("services", {})
_cleanup_orphan_service_processes(name)
existing = services.get(name)
if isinstance(existing, dict):
pid = int(existing.get("pid") or 0)
if _is_running(pid):
# Keep recorded primary pid, kill only duplicate peers.
_cleanup_orphan_service_processes(name, keep_pids={pid})
return pid
# No stable primary pid: kill all matching peers and restart cleanly.
_cleanup_orphan_service_processes(name)
merged_env = os.environ.copy()
if env:
merged_env.update({str(k): str(v) for k, v in env.items()})
@ -251,14 +347,47 @@ def status_services() -> list[ServiceState]:
state = _read_state()
services = state.get("services")
if not isinstance(services, dict):
return []
services = {}
state["services"] = services
# Reconcile state-file PIDs with live processes by command signature.
# This prevents false "missing service" reports when cwd/env drift starts
# services successfully but runtime state was written from another context.
changed = False
known_names: list[str] = []
for n in services.keys():
nn = str(n or "").strip()
if nn and nn not in known_names:
known_names.append(nn)
for n in ("gateway", "channel:wecom"):
if n not in known_names:
known_names.append(n)
out: list[ServiceState] = []
for name, meta in services.items():
for name in known_names:
meta = services.get(name)
if not isinstance(meta, dict):
continue
meta = {}
pid = int(meta.get("pid") or 0)
running = _is_running(pid)
cmd = meta.get("command") if isinstance(meta.get("command"), list) else []
out.append(ServiceState(name=str(name), pid=pid, command=[str(x) for x in cmd], running=_is_running(pid)))
if not running:
sig = _service_signature(str(name))
pids = _find_pids_by_signature(sig) if sig else []
# Safe reconcile only when exactly one candidate exists.
# If multiple workers exist, avoid guessing a primary pid.
if len(pids) == 1:
pid = int(sorted(pids)[0])
running = _is_running(pid)
if running and int(meta.get("pid") or 0) != pid:
services[str(name)] = {
"pid": int(pid),
"command": cmd,
"stdout_log": str(meta.get("stdout_log") or ""),
"stderr_log": str(meta.get("stderr_log") or ""),
}
changed = True
out.append(ServiceState(name=str(name), pid=pid, command=[str(x) for x in cmd], running=running))
if changed:
_write_state(state)
return out

View file

@ -1,4 +1,4 @@
param(
param(
[string]$Python = "python",
[switch]$Recreate = $false
)
@ -14,10 +14,10 @@ function Fail([string]$msg) {
exit 1
}
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
Set-Location $repoRoot
$venvDir = Join-Path $repoRoot "oclaw/.venv"
$venvDir = Join-Path $repoRoot ".venv"
$venvPython = Join-Path $venvDir "Scripts/python.exe"
$py = Get-Command $Python -ErrorAction SilentlyContinue
@ -49,3 +49,5 @@ Write-Host ""
Write-Host "OK. Next:" -ForegroundColor Green
Write-Host " powershell -ExecutionPolicy Bypass -File .\\oclaw\\scripts\\start_gateway.ps1"
Write-Host ""

View file

@ -9,6 +9,7 @@ _PS1_FORWARDERS: dict[str, str] = {
"bootstrap_venv.ps1": "& $real @args",
"start_web.ps1": "& $real @args",
"status_all.ps1": "& $real @args",
"status_desktop.ps1": "& $real @args",
"status_ops.ps1": "& $real @args",
"status_wiki_worker.ps1": "& $real @args",
"stop_all.ps1": "& $real @args",

View file

@ -1,10 +1,10 @@
param(
param(
[string]$BindHost = "127.0.0.1",
[int]$Port = 8787,
[switch]$SkipInstall = $false,
[switch]$Background = $false,
[switch]$WithWeixin = $false,
[switch]$WithWikiWorker = $false,
[bool]$WithWikiWorker = $true,
[string]$WeixinChannelId = "oclaw-weixin",
[string]$WeixinGatewayBaseUrl = ""
)
@ -15,12 +15,13 @@ function Write-Step([string]$msg) {
Write-Host "==> $msg" -ForegroundColor Cyan
}
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location $repoRoot
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$repoParent = Split-Path -Parent $repoRoot
Set-Location $repoParent
Write-Step "Starting gateway + desktop"
& "$PSScriptRoot/start_gateway.ps1" -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background
& "$PSScriptRoot/start_gateway.ps1" -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background -WithWikiWorker:$WithWikiWorker
# When gateway is started in foreground, the call above blocks; desktop won't start.
if (-not $Background) {
@ -29,7 +30,7 @@ if (-not $Background) {
exit 0
}
& "$PSScriptRoot/start_desktop.ps1" -SkipInstall:$SkipInstall -Background
& "$PSScriptRoot/start_desktop.ps1" -SkipInstall:$SkipInstall -Background -WithWikiWorker:$WithWikiWorker
if ($WithWeixin) {
$gwBase = ($WeixinGatewayBaseUrl | ForEach-Object { "$_".Trim() })
@ -39,11 +40,7 @@ if ($WithWeixin) {
Write-Step "Starting weixin sidecar"
& "$PSScriptRoot/weixin_start.ps1" -ChannelId $WeixinChannelId -GatewayBaseUrl $gwBase
}
if ($WithWikiWorker) {
Write-Step "Starting wiki worker"
& "$PSScriptRoot/start_wiki_worker.ps1" -Background
}
Write-Host ""
Write-Host "All started." -ForegroundColor Green

View file

@ -1,12 +1,14 @@
param(
param(
[switch]$SkipInstall = $false,
[switch]$Background = $false,
[switch]$KeepExistingGateway = $false
[switch]$KeepExistingGateway = $false,
[bool]$WithWikiWorker = $true
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$desktopDir = Join-Path $repoRoot "oclaw\\desktop"
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$repoParent = Split-Path -Parent $repoRoot
$desktopDir = Join-Path $repoRoot "desktop"
$runDir = Join-Path $PSScriptRoot ".run"
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
$pidFile = Join-Path $runDir "desktop.pid"
@ -19,6 +21,7 @@ if (-not (Test-Path $desktopDir)) {
}
Set-Location $desktopDir
$env:PYTHONPATH = $repoParent
if (-not $KeepExistingGateway) {
Write-Host "==> Cleaning previous gateway listener" -ForegroundColor Cyan
@ -34,6 +37,11 @@ if (-not $SkipInstall) {
npm install
}
if ($WithWikiWorker) {
Write-Host "==> Ensuring wiki worker is running" -ForegroundColor Cyan
& "$PSScriptRoot/start_wiki_worker.ps1" -Background
}
Write-Host "==> Launching desktop app" -ForegroundColor Cyan
if ($Background) {
# Avoid launching npm.ps1 directly (can be file-associated and open in Notepad on some setups).
@ -47,3 +55,5 @@ if ($Background) {
exit 0
}
& "npm.cmd" "run" "dev"

View file

@ -1,8 +1,9 @@
param(
param(
[string]$BindHost = "127.0.0.1",
[int]$Port = 8787,
[switch]$SkipInstall = $false,
[switch]$Background = $false
[switch]$Background = $false,
[bool]$WithWikiWorker = $true
)
$ErrorActionPreference = "Stop"
@ -16,16 +17,20 @@ function Fail([string]$msg) {
exit 1
}
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location $repoRoot
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$repoParent = Split-Path -Parent $repoRoot
Set-Location $repoParent
$runDir = Join-Path $PSScriptRoot ".run"
New-Item -ItemType Directory -Force -Path $runDir | Out-Null
$pidFile = Join-Path $runDir "gateway.pid"
Write-Step "Project root: $repoRoot"
Write-Step "Working directory: $repoParent"
$venvPython = Join-Path $repoRoot "oclaw/.venv/Scripts/python.exe"
$env:PYTHONPATH = $repoParent
$venvPython = Join-Path $repoRoot ".venv/Scripts/python.exe"
if (Test-Path $venvPython) {
Write-Step "Using venv python: $venvPython"
$pythonExe = $venvPython
@ -40,7 +45,7 @@ if (Test-Path $venvPython) {
if (-not $SkipInstall) {
Write-Step "Installing dependencies (pip install -r requirements.txt)"
& $pythonExe -m pip install -r "requirements.txt"
& $pythonExe -m pip install -r (Join-Path $repoRoot "requirements.txt")
} else {
Write-Step "Skip dependency install"
}
@ -52,15 +57,20 @@ Write-Host "Chat: http://$BindHost`:$Port/chat"
Write-Host "WS: ws://$BindHost`:$Port/ws"
Write-Host ""
$configPath = Join-Path $repoRoot "oclaw\oclaw.json"
$configPath = Join-Path $repoRoot "oclaw.json"
if (Test-Path $configPath) {
$env:OCLAW_CONFIG_PATH = $configPath
Write-Step "Using OCLAW_CONFIG_PATH: $configPath"
}
if ($WithWikiWorker) {
Write-Step "Ensuring wiki worker is running"
& "$PSScriptRoot/start_wiki_worker.ps1" -Background
}
if ($Background) {
Write-Step "Starting gateway in background"
$p = Start-Process -FilePath $pythonExe -ArgumentList @("-m","oclaw.runtime.operations","gateway","start","--host",$BindHost,"--port",$Port) -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden
$p = Start-Process -FilePath $pythonExe -ArgumentList @("-m","oclaw.runtime.operations","gateway","start","--host",$BindHost,"--port",$Port) -WorkingDirectory $repoParent -PassThru -WindowStyle Hidden
Set-Content -Path $pidFile -Value "$($p.Id)" -Encoding ascii
Write-Host "gateway.pid = $pidFile" -ForegroundColor DarkGray
Write-Host "PID = $($p.Id)" -ForegroundColor Green
@ -70,3 +80,5 @@ if ($Background) {
Write-Step "Starting gateway (foreground)"
& $pythonExe -m oclaw.runtime.operations gateway start --host $BindHost --port $Port

View file

@ -15,12 +15,16 @@ function Fail([string]$msg) {
exit 1
}
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location $repoRoot
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$repoParent = Split-Path -Parent $repoRoot
Set-Location $repoParent
Write-Step "Project root: $repoRoot"
Write-Step "Working directory: $repoParent"
$venvPython = Join-Path $repoRoot "oclaw/.venv/Scripts/python.exe"
$env:PYTHONPATH = $repoParent
$venvPython = Join-Path $repoRoot ".venv/Scripts/python.exe"
if (Test-Path $venvPython) {
Write-Step "Using venv python: $venvPython"
$pythonExe = $venvPython
@ -35,7 +39,7 @@ if (Test-Path $venvPython) {
if (-not $SkipInstall) {
Write-Step "Installing dependencies (pip install -r requirements.txt)"
& $pythonExe -m pip install -r "requirements.txt"
& $pythonExe -m pip install -r (Join-Path $repoRoot "requirements.txt")
} else {
Write-Step "Skip dependency install"
}
@ -43,7 +47,7 @@ if (-not $SkipInstall) {
if (-not $SkipConfigHint) {
Write-Step "Current WeCom status"
try {
python -m oclaw.runtime.operations channel wecom status
& $pythonExe -m oclaw.runtime.operations channel wecom status
} catch {
Write-Host "[WARN] Unable to read WeCom status yet." -ForegroundColor Yellow
}
@ -68,3 +72,5 @@ Write-Host "Useful commands:"
Write-Host " python -m oclaw.runtime.operations stack status"
Write-Host " python -m oclaw.runtime.operations stack down"

View file

@ -1,4 +1,4 @@
param(
param(
[switch]$Background = $true
)
@ -16,7 +16,16 @@ $pidFile = Join-Path $runDir "wiki_worker.pid"
$outLog = Join-Path $runDir "wiki_worker.out.log"
$errLog = Join-Path $runDir "wiki_worker.err.log"
$venvPython = Join-Path $repoRoot "oclaw/.venv/Scripts/python.exe"
function Test-AlivePid([int]$procId) {
try {
$p = Get-Process -Id $procId -ErrorAction Stop
return $null -ne $p
} catch {
return $false
}
}
$venvPython = Join-Path $repoRoot ".venv/Scripts/python.exe"
if (Test-Path $venvPython) {
$pythonExe = $venvPython
} else {
@ -32,6 +41,17 @@ if (-not $Background) {
exit 0
}
if (Test-Path $pidFile) {
$raw = (Get-Content $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
$existing = 0
[void][int]::TryParse([string]$raw, [ref]$existing)
if ($existing -gt 0 -and (Test-AlivePid $existing)) {
Write-Host "[ok] wiki worker already running pid=$existing"
exit 0
}
}
$p = Start-Process -FilePath $pythonExe -ArgumentList @("-m", "oclaw.runtime.workers.wiki.main") -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden -RedirectStandardOutput $outLog -RedirectStandardError $errLog
Set-Content -Path $pidFile -Value $p.Id -Encoding ascii
Write-Host "[ok] started wiki worker pid=$($p.Id) out=$outLog err=$errLog"

View file

@ -0,0 +1,39 @@
param()
$ErrorActionPreference = "Stop"
function Warn([string]$msg) {
Write-Host "[WARN] $msg" -ForegroundColor Yellow
}
$runDir = Join-Path $PSScriptRoot ".run"
$pidFile = Join-Path $runDir "desktop.pid"
$outLog = Join-Path $runDir "desktop.out.log"
$errLog = Join-Path $runDir "desktop.err.log"
if (-not (Test-Path $pidFile)) {
Warn "desktop.pid not found: $pidFile"
exit 1
}
$raw = (Get-Content $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
$procId = 0
[void][int]::TryParse([string]$raw, [ref]$procId)
if ($procId -le 0) {
Warn "Invalid PID in $pidFile"
exit 1
}
try {
$p = Get-Process -Id $procId -ErrorAction Stop
Write-Host "desktop_running=1" -ForegroundColor Green
Write-Host "pid=$procId"
Write-Host "name=$($p.ProcessName)"
if (Test-Path $outLog) { Write-Host "out_log=$outLog" }
if (Test-Path $errLog) { Write-Host "err_log=$errLog" }
exit 0
} catch {
Warn "desktop process not found: PID=$procId"
exit 1
}

View file

@ -4,18 +4,29 @@ function Write-Step([string]$msg) {
Write-Host "==> $msg" -ForegroundColor Cyan
}
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location $repoRoot
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$repoParent = Split-Path -Parent $repoRoot
Set-Location $repoParent
$env:PYTHONPATH = $repoParent
$venvPython = Join-Path $repoRoot ".venv/Scripts/python.exe"
if (Test-Path $venvPython) {
$pythonExe = $venvPython
} else {
$pythonExe = "python"
}
Write-Step "Project root: $repoRoot"
Write-Step "Working directory: $repoParent"
Write-Step "Stack status"
python -m oclaw.runtime.operations stack status
& $pythonExe -m oclaw.runtime.operations stack status
Write-Host ""
Write-Step "WeCom status"
python -m oclaw.runtime.operations channel wecom status
& $pythonExe -m oclaw.runtime.operations channel wecom status
Write-Host ""
Write-Host "Admin: http://127.0.0.1:8787/admin"
Write-Host "Chat: http://127.0.0.1:8787/chat"

View file

@ -8,14 +8,24 @@ function Write-Step([string]$msg) {
Write-Host "==> $msg" -ForegroundColor Cyan
}
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location $repoRoot
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$repoParent = Split-Path -Parent $repoRoot
Set-Location $repoParent
$env:PYTHONPATH = $repoParent
$venvPython = Join-Path $repoRoot ".venv/Scripts/python.exe"
if (Test-Path $venvPython) {
$pythonExe = $venvPython
} else {
$pythonExe = "python"
}
Write-Step "Project root: $repoRoot"
Write-Step "Working directory: $repoParent"
Write-Step "Stopping stack services..."
try {
python -m oclaw.runtime.operations stack down
& $pythonExe -m oclaw.runtime.operations stack down
Write-Host "Stack stopped." -ForegroundColor Green
} catch {
Write-Host "[WARN] stack down failed: $($_.Exception.Message)" -ForegroundColor Yellow
@ -26,8 +36,9 @@ try {
Write-Step "Current status"
try {
python -m oclaw.runtime.operations stack status
& $pythonExe -m oclaw.runtime.operations stack status
} catch {
Write-Host "[WARN] Unable to read status after stop." -ForegroundColor Yellow
}