mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-16 20:31:33 +08:00
156 lines
5.8 KiB
Python
Executable File
156 lines
5.8 KiB
Python
Executable File
"""打开 MemeMeow 专用 OpenCode runtime,供开发者检查历史图片分析会话。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from backend.config import Settings # noqa: E402
|
|
from backend.opencode import OpenCodeError, OpenCodeRunner # noqa: E402
|
|
|
|
|
|
COMPOSE_SERVICE = "mememeow-agent-runtime"
|
|
EXECUTOR_WORKSPACE = "/runtime/workspace"
|
|
EXECUTOR_ENVIRONMENT = {
|
|
"OPENCODE_DB": "/runtime/opencode.db",
|
|
"OPENCODE_CONFIG": "/runtime/workspace/opencode.json",
|
|
"OPENCODE_CONFIG_DIR": "/runtime/workspace/.opencode",
|
|
"OPENCODE_DISABLE_PROJECT_CONFIG": "1",
|
|
}
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
"""创建启动器参数解析器;未知参数原样交给 OpenCode TUI。"""
|
|
parser = argparse.ArgumentParser(description="打开 MemeMeow OpenCode 会话检查界面")
|
|
parser.add_argument("--list", action="store_true", help="在终端列出历史 session 后退出")
|
|
return parser
|
|
|
|
|
|
def _compose_file() -> Path:
|
|
"""返回 Compose 配置绝对路径,供任意工作目录下的诊断调用复用。"""
|
|
configured = Path(os.environ.get("MEMEMEOW_COMPOSE_FILE", "docker-compose.yml")).expanduser()
|
|
if not configured.is_absolute():
|
|
configured = PROJECT_ROOT / configured
|
|
return configured.resolve()
|
|
|
|
|
|
def _docker_command_for_execution(command: list[str]) -> list[str]:
|
|
"""选择直接 Docker 或 ``sg docker``,兼容尚未刷新组权限的宿主会话。"""
|
|
try:
|
|
probe = subprocess.run(
|
|
["docker", "info"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=3,
|
|
check=False,
|
|
)
|
|
if probe.returncode == 0:
|
|
return command
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
pass
|
|
return ["sg", "docker", "-c", shlex.join(command)]
|
|
|
|
|
|
def _compose_command(*arguments: str) -> list[str]:
|
|
"""构造固定 Compose 项目的命令,避免依赖调用者当前目录。"""
|
|
return ["docker", "compose", "-f", str(_compose_file()), *arguments]
|
|
|
|
|
|
def _compose_runtime_running() -> bool:
|
|
"""检查权威 Agent 服务是否运行;调用场景为选择 session 数据源。"""
|
|
if not _compose_file().is_file():
|
|
return False
|
|
command = _docker_command_for_execution(
|
|
_compose_command("ps", "--status", "running", "--services", COMPOSE_SERVICE)
|
|
)
|
|
try:
|
|
result = subprocess.run(command, capture_output=True, text=True, timeout=8, check=False)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return False
|
|
return result.returncode == 0 and COMPOSE_SERVICE in result.stdout.splitlines()
|
|
|
|
|
|
def _compose_runtime_selected(runner: OpenCodeRunner) -> bool:
|
|
"""判断 executor 模式是否应使用当前 Compose project 的 service 诊断入口。"""
|
|
return runner.executor_mode and runner.executor.configured and _compose_file().is_file()
|
|
|
|
|
|
def _compose_launcher_command(*, session_list: bool, passthrough: list[str]) -> list[str]:
|
|
"""构造容器内 OpenCode 入口,连接 named volume 中的共享配置和数据库。"""
|
|
arguments = ["exec"]
|
|
if session_list:
|
|
# 非交互查询关闭 TTY,便于重定向、测试和脚本消费 JSON。
|
|
arguments.append("-T")
|
|
arguments.extend(("--workdir", EXECUTOR_WORKSPACE))
|
|
for key, value in EXECUTOR_ENVIRONMENT.items():
|
|
arguments.extend(("--env", f"{key}={value}"))
|
|
arguments.append(COMPOSE_SERVICE)
|
|
if session_list:
|
|
arguments.extend(("opencode", "session", "list", *passthrough))
|
|
else:
|
|
# 模型配置只存在于容器环境;用固定 shell 程序读取,透传参数仍保持独立 argv。
|
|
arguments.extend(
|
|
(
|
|
"sh",
|
|
"-lc",
|
|
'exec opencode /runtime/workspace --model "$MEMEMEOW_OPENCODE_MODEL" "$@"',
|
|
"open-opencode",
|
|
*passthrough,
|
|
)
|
|
)
|
|
return _docker_command_for_execution(_compose_command(*arguments))
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""准备共享 runtime 并以同一 DB 启动 TUI,调用场景为本地诊断历史 session。"""
|
|
parser = build_parser()
|
|
known, passthrough = parser.parse_known_args(argv)
|
|
settings = Settings.from_env(PROJECT_ROOT / ".env")
|
|
runner = OpenCodeRunner(settings, project_root=PROJECT_ROOT)
|
|
|
|
# Compose named volume 是 executor 部署态权威数据源;诊断只按 service key 定位当前 project。
|
|
if _compose_runtime_selected(runner):
|
|
if not _compose_runtime_running():
|
|
parser.exit(1, f"Compose Agent 服务 {COMPOSE_SERVICE} 未运行,无法打开共享 OpenCode runtime。\n")
|
|
command = _compose_launcher_command(session_list=known.list, passthrough=passthrough)
|
|
try:
|
|
os.execvp(command[0], command)
|
|
except OSError as exc:
|
|
parser.exit(1, f"无法进入 Compose OpenCode runtime: {exc}\n")
|
|
|
|
try:
|
|
runner.prepare_runtime()
|
|
except OpenCodeError as exc:
|
|
parser.exit(1, f"OpenCode runtime 初始化失败 [{exc.code}]: {exc}\n")
|
|
|
|
executable = str(settings.opencode_executable or "opencode")
|
|
environment = runner.build_environment()
|
|
if known.list:
|
|
command = [executable, "session", "list", *passthrough]
|
|
else:
|
|
command = [
|
|
executable,
|
|
str(runner.workspace),
|
|
"--model",
|
|
str(settings.opencode_model),
|
|
*passthrough,
|
|
]
|
|
try:
|
|
os.chdir(runner.workspace)
|
|
os.execvpe(executable, command, environment)
|
|
except OSError as exc:
|
|
parser.exit(1, f"无法启动 OpenCode: {exc}\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|