diff --git a/agentflow/Makefile b/agentflow/Makefile index 9bee136eb..f70ec2ad2 100644 --- a/agentflow/Makefile +++ b/agentflow/Makefile @@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64 AGENTFLOW_ARCH_aarch64:=arm64 AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH)) -AGENTFLOW_HASH_x86_64:=95cb43ba4e2ce5f060e3609d050caf25170dc151ba882a3a626494b7750d5fbc -AGENTFLOW_HASH_aarch64:=93e92628b26087b87d8571400e83046197167e4795593e757d4dcf7dac5f8231 +AGENTFLOW_HASH_x86_64:=478fdb17cfb2b1664e7bc01315186e71f228871c0d20c816d8b3b8fe789bdb4d +AGENTFLOW_HASH_aarch64:=257faeaad2445dd14f87c1e1d9ded869fd301c5df2c2c9af817e70303eab2512 PKG_NAME:=agentflow PKG_VERSION:=0.3.3 -PKG_RELEASE:=11 +PKG_RELEASE:=12 AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH) AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH)) diff --git a/luci-app-agentflow/Makefile b/luci-app-agentflow/Makefile index 3a42f8eb7..fbc2e99f6 100644 --- a/luci-app-agentflow/Makefile +++ b/luci-app-agentflow/Makefile @@ -7,9 +7,9 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=AgentFlow -PKG_VERSION:=1.0.0 -PKG_RELEASE:=5 -LUCI_DEPENDS:=+agentflow +luci-compat +PKG_VERSION:=1.0.1 +PKG_RELEASE:=6 +LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd LUCI_MINIFY_CSS:=0 LUCI_MINIFY_JS:=0 diff --git a/luci-app-agentflow/luasrc/controller/agentflow.lua b/luci-app-agentflow/luasrc/controller/agentflow.lua index c1b0924b8..3c7621859 100644 --- a/luci-app-agentflow/luasrc/controller/agentflow.lua +++ b/luci-app-agentflow/luasrc/controller/agentflow.lua @@ -17,10 +17,38 @@ function index() return end + local install = entry({"admin", "services", "agentflow", "agent_install"}, call("agentflow_agent_install")) + install.leaf = true + local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100) page.dependent = true end +local function write_json(obj) + http.prepare_content("application/json") + http.write_json(obj) +end + +local function require_post_csrf() + local dispatcher = require "luci.dispatcher" + local context = dispatcher.context or {} + local expected = context.authtoken or context.token + + if (http.getenv("REQUEST_METHOD") or "") ~= "POST" then + http.status(405, "Method Not Allowed") + write_json({ ok = false, error = "method not allowed" }) + return false + end + + if not context.authsession or not expected or http.formvalue("token") ~= expected then + http.status(403, "Forbidden") + write_json({ ok = false, error = "invalid csrf token" }) + return false + end + + return true +end + local function uhttpd_has_apps_proxy_prefix() local uci = require "luci.model.uci".cursor() local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {} @@ -119,8 +147,97 @@ function agentflow_status() proxy_prefix_enabled = uhttpd_apps_proxy_available(), linkeasefull_running = linkeasefull_running() } - http.prepare_content("application/json") - http.write_json(status) + write_json(status) +end + +function agentflow_agent_install() + local fs = require "nixio.fs" + local jsonc = require "luci.jsonc" + local sys = require "luci.sys" + local util = require "luci.util" + local task_id = "agentflow-agent-install" + local task_script = "/tmp/agentflow-agent-install.sh" + local installer_url = "https://fw.koolcenter.com/binary/geili/agentflow/releases/installapp/installapp-mise.sh" + local agents = { + codexcli = true, + ["claude-code"] = true, + opencode = true, + kimi = true, + reasonix = true + } + + if not require_post_csrf() then + return + end + + local agent = http.formvalue("agent") or "" + if not agents[agent] then + write_json({ ok = false, error = "unsupported agent" }) + return + end + if not fs.access("/etc/init.d/tasks") then + write_json({ ok = false, error = "taskd is not available" }) + return + end + if not fs.access("/lib/functions/mise.sh") then + write_json({ ok = false, error = "mise environment helper is not available" }) + return + end + + local task_status = sys.exec("/etc/init.d/tasks task_status " .. task_id .. " 2>/dev/null") + local status = jsonc.parse(task_status) or {} + local running = type(status) == "table" and status.running + if running == true or running == 1 or running == "1" or running == "true" then + write_json({ ok = true, busy = true, task_id = task_id }) + return + end + + local install_script = table.concat({ + "set -e", + "agent=" .. util.shellquote(agent), + "installer_url=" .. util.shellquote(installer_url), + 'installer="/tmp/agentflow-installapp-mise.$$"', + 'cleanup() { rm -f "$installer" "$0"; }', + "trap cleanup 0 HUP INT TERM", + '. /lib/functions/mise.sh', + 'if ! istore_runtime_env; then echo "[agentflow] Failed to initialize the shared runtime environment" >&2; exit 1; fi', + "set -u", + "export MISE_YES=1", + 'echo "[agentflow] Downloading installer: $installer_url"', + 'if command -v wget >/dev/null 2>&1; then', + '\twget -O "$installer" "$installer_url"', + 'elif command -v curl >/dev/null 2>&1; then', + '\tcurl -fL -o "$installer" "$installer_url"', + 'elif command -v uclient-fetch >/dev/null 2>&1; then', + '\tuclient-fetch -O "$installer" "$installer_url"', + "else", + '\techo "[agentflow] No HTTPS download tool is available" >&2', + "\texit 1", + "fi", + 'if [ ! -s "$installer" ]; then echo "[agentflow] Failed to download the agent installer" >&2; exit 1; fi', + 'chmod 0700 "$installer"', + 'echo "[agentflow] Running installapp-mise.sh for $agent in $HOME"', + '/bin/sh "$installer" "$agent"' + }, "\n") + if not fs.writefile(task_script, install_script .. "\n") then + write_json({ ok = false, error = "failed to create install task", task_id = task_id }) + return + end + if sys.call("chmod 0600 " .. util.shellquote(task_script)) ~= 0 then + fs.unlink(task_script) + write_json({ ok = false, error = "failed to secure install task", task_id = task_id }) + return + end + + local command = "/bin/sh " .. util.shellquote(task_script) + local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(command) .. " >/dev/null 2>&1") + if rc ~= 0 then + fs.unlink(task_script) + write_json({ ok = false, error = "failed to start install task", task_id = task_id }) + return + end + + write_json({ ok = true, task_id = task_id }) end function agentflow_open() diff --git a/luci-app-agentflow/luasrc/view/agentflow/status.htm b/luci-app-agentflow/luasrc/view/agentflow/status.htm index 6e21cd833..615acb0cf 100644 --- a/luci-app-agentflow/luasrc/view/agentflow/status.htm +++ b/luci-app-agentflow/luasrc/view/agentflow/status.htm @@ -1,11 +1,80 @@ +<%+tasks/embed%> + + +
@@ -25,4 +97,29 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)

<%:Collecting data...%>

+
+ + +
+ + diff --git a/luci-app-agentflow/po/zh-cn/agentflow.po b/luci-app-agentflow/po/zh-cn/agentflow.po index 41746741e..3b684b092 100644 --- a/luci-app-agentflow/po/zh-cn/agentflow.po +++ b/luci-app-agentflow/po/zh-cn/agentflow.po @@ -51,3 +51,24 @@ msgstr "AgentFlow 服务未运行。" msgid "The AgentFlow service is running." msgstr "AgentFlow 服务运行中。" + +msgid "Install agent" +msgstr "安装智能体" + +msgid "Agent" +msgstr "智能体" + +msgid "Select a coding agent to install into the shared AgentFlow runtime." +msgstr "选择要安装到 AgentFlow 共享运行环境中的编码智能体。" + +msgid "Cancel" +msgstr "取消" + +msgid "Install now" +msgstr "立即安装" + +msgid "Task log window is not available." +msgstr "任务日志窗口不可用。" + +msgid "Failed to start agent installation." +msgstr "启动智能体安装失败。" diff --git a/luci-app-agentflow/tests/test_agent_install_contract.py b/luci-app-agentflow/tests/test_agent_install_contract.py new file mode 100644 index 000000000..99391e4a0 --- /dev/null +++ b/luci-app-agentflow/tests/test_agent_install_contract.py @@ -0,0 +1,69 @@ +from pathlib import Path +import unittest + + +APP_DIR = Path(__file__).resolve().parents[1] + + +class AgentInstallContractTest(unittest.TestCase): + def read(self, relative): + return (APP_DIR / relative).read_text(encoding="utf-8") + + def test_luci_loads_task_window_and_starts_whitelisted_installer(self): + makefile = self.read("Makefile") + controller = self.read("luasrc/controller/agentflow.lua") + status = self.read("luasrc/view/agentflow/status.htm") + + self.assertIn("+luci-lib-taskd", makefile) + self.assertIn('<%+tasks/embed%>', status) + self.assertIn('