mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-12 19:50:57 +05:00
Fix OpenCode defaults and false health failures
This commit is contained in:
parent
c2566e9b4a
commit
9a3f058f73
@ -206,78 +206,6 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(() => {
|
||||
const frame = document.querySelector(".opencode-agent-frame");
|
||||
if (!frame) return;
|
||||
|
||||
const healthUrl = "{{ proxy_prefix|escapejs }}/global/health";
|
||||
const monitorUrl = "{{ proxy_prefix|escapejs }}/_archivebox/health";
|
||||
const fetchOptions = {
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
redirect: "manual",
|
||||
};
|
||||
let checking = false;
|
||||
let waking = false;
|
||||
let wasUnavailable = false;
|
||||
let healthyChecks = 0;
|
||||
let knownVersion = "{{ opencode_version|escapejs }}";
|
||||
const wake = () => {
|
||||
if (waking) return;
|
||||
waking = true;
|
||||
fetch(healthUrl, fetchOptions)
|
||||
.catch(() => {})
|
||||
.finally(() => { waking = false; });
|
||||
};
|
||||
const check = async () => {
|
||||
if (checking || (document.hidden && wasUnavailable)) return;
|
||||
checking = true;
|
||||
try {
|
||||
const response = await fetch(monitorUrl, fetchOptions);
|
||||
if (!response.ok || response.redirected) {
|
||||
wasUnavailable = true;
|
||||
healthyChecks = 0;
|
||||
if (response.status === 503) {
|
||||
wake();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const health = await response.json();
|
||||
if (!health || health.healthy !== true) {
|
||||
wasUnavailable = true;
|
||||
healthyChecks = 0;
|
||||
return;
|
||||
}
|
||||
const currentVersion = typeof health.version === "string" ? health.version : "";
|
||||
if (knownVersion && currentVersion && currentVersion !== knownVersion) {
|
||||
wasUnavailable = true;
|
||||
}
|
||||
if (currentVersion) {
|
||||
knownVersion = currentVersion;
|
||||
}
|
||||
if (wasUnavailable && ++healthyChecks >= 2) {
|
||||
wasUnavailable = false;
|
||||
healthyChecks = 0;
|
||||
frame.contentWindow.location.reload();
|
||||
}
|
||||
} catch {
|
||||
wasUnavailable = true;
|
||||
healthyChecks = 0;
|
||||
} finally {
|
||||
checking = false;
|
||||
}
|
||||
};
|
||||
|
||||
window.setInterval(check, 3000);
|
||||
window.addEventListener("online", check);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) check();
|
||||
});
|
||||
check();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from archivebox.opencode.views import agent_health_view, agent_view, opencode_proxy_view
|
||||
from archivebox.opencode.views import agent_view, opencode_proxy_view
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", agent_view, name="opencode-agent"),
|
||||
path("opencode/_archivebox/health", agent_health_view, name="opencode-health"),
|
||||
re_path(
|
||||
r"^opencode(?:/(?P<path>.*))?$",
|
||||
opencode_proxy_view,
|
||||
|
||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -26,7 +27,6 @@ from django.http import (
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
HttpResponseForbidden,
|
||||
JsonResponse,
|
||||
StreamingHttpResponse,
|
||||
)
|
||||
from django.shortcuts import redirect, render
|
||||
@ -42,6 +42,7 @@ _PROXY_PREFIX = "/admin/agent/opencode"
|
||||
_PROXY_PREFIX_REGEX = _PROXY_PREFIX.replace("/", r"\/")
|
||||
_PROXY_PREFIX_NO_SLASH_REGEX = _PROXY_PREFIX.lstrip("/").replace("/", r"\/")
|
||||
_CONFIG_PATH = Path(opencode_plugin.__file__).with_name("config.json")
|
||||
_DEFAULT_MODEL = "opencode/big-pickle"
|
||||
|
||||
_TEXT_CONTENT_TYPES = (
|
||||
"text/",
|
||||
@ -219,7 +220,7 @@ def _settings(config: dict) -> dict:
|
||||
str(_config_value(config, "OPENCODE_STATE_DIR", default_data_dir / "opencode")),
|
||||
).expanduser()
|
||||
workdir = Path(
|
||||
str(_config_value(config, "OPENCODE_WORKDIR", opencode_dir / "workdir")),
|
||||
str(_config_value(config, "OPENCODE_WORKDIR", default_data_dir)),
|
||||
).expanduser()
|
||||
binary = str(_config_value(config, "OPENCODE_BINARY", "opencode"))
|
||||
timeout = int(_config_value(config, "OPENCODE_TIMEOUT", 120))
|
||||
@ -321,6 +322,21 @@ def _ensure_project_files(settings: dict) -> None:
|
||||
opencode_skill_path.unlink()
|
||||
opencode_skill_path.symlink_to(editable_skill_path)
|
||||
|
||||
opencode_config_path = settings["config_home"] / "opencode" / "opencode.jsonc"
|
||||
default_config = {
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"model": _DEFAULT_MODEL,
|
||||
}
|
||||
if not opencode_config_path.exists():
|
||||
opencode_config_path.write_text(f"{json.dumps(default_config, indent=2)}\n")
|
||||
else:
|
||||
try:
|
||||
existing_config = json.loads(opencode_config_path.read_text())
|
||||
except (OSError, ValueError):
|
||||
existing_config = None
|
||||
if isinstance(existing_config, dict) and set(existing_config) <= {"$schema"}:
|
||||
opencode_config_path.write_text(f"{json.dumps(default_config, indent=2)}\n")
|
||||
|
||||
|
||||
def _ensure_default_session(settings: dict) -> str:
|
||||
workdir = settings["workdir"].resolve()
|
||||
@ -393,19 +409,6 @@ def _health(settings: dict, timeout: float = 2) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _opencode_version(settings: dict) -> str:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{settings['origin']}/global/health",
|
||||
timeout=2,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return str(data.get("version") or "") if isinstance(data, dict) else ""
|
||||
except (requests.RequestException, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def _ensure_opencode(settings: dict) -> tuple[bool, str]:
|
||||
global _PROCESS, _PROCESS_READY
|
||||
started_process: subprocess.Popen | None = None
|
||||
@ -539,16 +542,13 @@ def agent_view(request: HttpRequest):
|
||||
settings["archivebox_api_url"] = api_url
|
||||
ok, error = _ensure_opencode(settings)
|
||||
recent_session_id = ""
|
||||
opencode_version = ""
|
||||
if ok:
|
||||
try:
|
||||
with _SESSION_LOCK:
|
||||
recent_session_id = _ensure_default_session(settings)
|
||||
opencode_version = _opencode_version(settings)
|
||||
if not opencode_version:
|
||||
ok = False
|
||||
error = "OpenCode health check did not report its version."
|
||||
except (requests.RequestException, RuntimeError, ValueError) as err:
|
||||
if isinstance(err, requests.RequestException) and _owned_process_ready():
|
||||
_stop_owned_process()
|
||||
ok = False
|
||||
error = f"OpenCode project initialization failed: {err}"
|
||||
from archivebox.core.admin_site import archivebox_admin
|
||||
@ -565,7 +565,6 @@ def agent_view(request: HttpRequest):
|
||||
# transient new-session route and appear to have lost prior sessions.
|
||||
"proxy_url": _project_route(settings["workdir"], recent_session_id),
|
||||
"proxy_prefix": _PROXY_PREFIX,
|
||||
"opencode_version": opencode_version,
|
||||
"workdir": str(settings["workdir"].resolve()),
|
||||
"recent_session_id": recent_session_id,
|
||||
}
|
||||
@ -577,23 +576,6 @@ def agent_view(request: HttpRequest):
|
||||
)
|
||||
|
||||
|
||||
def agent_health_view(request: HttpRequest):
|
||||
config = _machine_config()
|
||||
_require_enabled(config)
|
||||
auth_response = _require_superuser(request)
|
||||
if auth_response:
|
||||
return auth_response
|
||||
|
||||
version = _opencode_version(_settings(config))
|
||||
healthy = bool(version)
|
||||
response = JsonResponse(
|
||||
{"healthy": healthy, "version": version},
|
||||
status=200 if healthy else 503,
|
||||
)
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
def _proxy_url(settings: dict, path: str | None) -> str:
|
||||
rel = "/" if not path else f"/{path}"
|
||||
return urljoin(settings["origin"], rel)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
@ -189,14 +190,8 @@ def test_opencode_agent_superuser_gets_admin_wrapper(admin_client, live_opencode
|
||||
assert b'id="header"' in response.content
|
||||
assert b'id="progress-monitor"' in response.content
|
||||
assert response.context["proxy_prefix"] == views._PROXY_PREFIX
|
||||
assert response.context["opencode_version"]
|
||||
assert b"const healthUrl" in response.content
|
||||
assert b"/admin/agent/opencode/global/health" in response.content
|
||||
assert b"/admin/agent/opencode/_archivebox/health" in response.content
|
||||
assert b'redirect: "manual"' in response.content
|
||||
assert b"let waking = false" in response.content
|
||||
assert b"wake();" in response.content
|
||||
assert b"frame.contentWindow.location.reload()" in response.content
|
||||
assert b"/_archivebox/health" not in response.content
|
||||
assert b"window.setInterval(check, 3000)" not in response.content
|
||||
assert response.headers["X-Frame-Options"] == "DENY"
|
||||
assert response.headers["Content-Security-Policy"] == "frame-ancestors 'none'"
|
||||
|
||||
@ -210,21 +205,6 @@ def test_opencode_agent_superuser_gets_admin_wrapper(admin_client, live_opencode
|
||||
assert session.headers["Content-Security-Policy"] == "frame-ancestors 'self'"
|
||||
|
||||
|
||||
def test_opencode_health_monitor_does_not_start_server(admin_client, live_opencode):
|
||||
from archivebox.opencode import views
|
||||
|
||||
views._stop_owned_process()
|
||||
response = admin_client.get(
|
||||
"/admin/agent/opencode/_archivebox/health",
|
||||
HTTP_HOST=ADMIN_TEST_HOST,
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json() == {"healthy": False, "version": ""}
|
||||
assert response.headers["Cache-Control"] == "no-store"
|
||||
assert views._PROCESS is None
|
||||
|
||||
|
||||
def test_opencode_proxy_serves_real_project_and_session(admin_client, live_opencode):
|
||||
workdir = str(live_opencode.config.data_dir.resolve())
|
||||
encoded_workdir = quote(workdir)
|
||||
@ -422,8 +402,15 @@ def test_opencode_starts_with_isolated_state(live_opencode):
|
||||
)
|
||||
project.raise_for_status()
|
||||
|
||||
config = requests.get(
|
||||
f"{live_opencode.settings['origin']}/global/config",
|
||||
timeout=live_opencode.settings["timeout"],
|
||||
)
|
||||
config.raise_for_status()
|
||||
|
||||
assert Path(live_opencode.settings["workdir"]).resolve() == Path(workdir)
|
||||
assert Path(str(project.json()["worktree"])).resolve() == Path(workdir)
|
||||
assert config.json()["model"] == "opencode/big-pickle"
|
||||
assert live_opencode.process.poll() is None
|
||||
assert (live_opencode.config.data_dir / ".git").is_dir()
|
||||
assert (state_dir / "data" / "opencode" / "opencode.db").is_file()
|
||||
@ -455,15 +442,57 @@ def test_opencode_state_dir_is_separate_from_workdir(tmp_path):
|
||||
assert loaded_skill.is_symlink()
|
||||
assert loaded_skill.resolve() == editable_skill.resolve()
|
||||
assert f"ArchiveBox collection directory: {settings['archivebox_data_dir']}" in editable_skill.read_text()
|
||||
assert json.loads((state_dir / "config" / "opencode" / "opencode.jsonc").read_text())["model"] == "opencode/big-pickle"
|
||||
|
||||
|
||||
def test_opencode_default_workdir_does_not_scan_the_collection():
|
||||
def test_opencode_preserves_existing_config(tmp_path):
|
||||
from archivebox.opencode import views
|
||||
|
||||
state_dir = tmp_path / "state"
|
||||
config_path = state_dir / "config" / "opencode" / "opencode.jsonc"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
existing_config = '{\n // Keep the administrator-selected model.\n "model": "anthropic/claude-sonnet-4-5"\n}\n'
|
||||
config_path.write_text(existing_config)
|
||||
|
||||
views._ensure_project_files(
|
||||
views._settings(
|
||||
{
|
||||
"OPENCODE_WORKDIR": str(tmp_path / "workdir"),
|
||||
"OPENCODE_STATE_DIR": str(state_dir),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert config_path.read_text() == existing_config
|
||||
|
||||
|
||||
def test_opencode_adds_default_model_to_schema_only_config(tmp_path):
|
||||
from archivebox.opencode import views
|
||||
|
||||
state_dir = tmp_path / "state"
|
||||
config_path = state_dir / "config" / "opencode" / "opencode.jsonc"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path.write_text('{"$schema": "https://opencode.ai/config.json"}\n')
|
||||
|
||||
views._ensure_project_files(
|
||||
views._settings(
|
||||
{
|
||||
"OPENCODE_WORKDIR": str(tmp_path / "workdir"),
|
||||
"OPENCODE_STATE_DIR": str(state_dir),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert json.loads(config_path.read_text())["model"] == "opencode/big-pickle"
|
||||
|
||||
|
||||
def test_opencode_defaults_to_the_archivebox_collection():
|
||||
from archivebox.opencode import views
|
||||
|
||||
settings = views._settings({})
|
||||
|
||||
assert settings["opencode_dir"] == settings["archivebox_data_dir"] / "opencode"
|
||||
assert settings["workdir"] == settings["opencode_dir"] / "workdir"
|
||||
assert settings["workdir"] == settings["archivebox_data_dir"]
|
||||
assert settings["timeout"] == 120
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user