🎉 Sync 2026-09-21 03:58:45

This commit is contained in:
github-actions[bot] committed 2026-09-21 03:58:45 +08:00
1 parent da26c8c705
commit b8539f4af6
8 files changed
+443 -74

No files matched your search

+3 -3
View File
@@ -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
@@ -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()
@@ -1,11 +1,80 @@
<%+tasks/embed%>
<style type="text/css">
.agentflow-actions { display:flex; gap:10px; flex-wrap:wrap; margin:12px; }
.agentflow-actions .btn, .agentflow-agent-dialog-actions .btn { min-height:40px; }
.agentflow-agent-modal { position:fixed; inset:0; z-index:10000; display:none; align-items:center; justify-content:center; padding:20px; box-sizing:border-box; background:rgba(15,23,42,.45); }
.agentflow-agent-modal.open { display:flex; }
.agentflow-agent-dialog, body[theme="light"] .agentflow-agent-dialog { width:min(520px, 100%); padding:20px; box-sizing:border-box; border:1px solid #d0d7de; border-radius:12px; background:#fff; color:inherit; box-shadow:0 18px 48px rgba(15,23,42,.24); }
.agentflow-agent-dialog h3 { margin:0 0 8px; text-wrap:balance; }
.agentflow-agent-dialog > p, body[theme="light"] .agentflow-agent-dialog > p { margin:0 0 16px; color:#666; text-wrap:pretty; }
.agentflow-agent-field label { display:block; margin-bottom:6px; font-weight:600; }
.agentflow-agent-field select { width:100%; min-height:40px; box-sizing:border-box; }
.agentflow-agent-dialog-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:18px; }
body[theme="dark"] .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
body[theme="dark"] .agentflow-agent-dialog > p { color:#aeb8c7; }
@media (prefers-color-scheme: dark) {
body:not([theme]) .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
body:not([theme]) .agentflow-agent-dialog > p { color:#aeb8c7; }
}
</style>
<script type="text/javascript">//<![CDATA[
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
var agentflowAgentInstallUrl = '<%=url("admin/services/agentflow/agent_install")%>';
var agentflowCsrfToken = (window.taskd && window.taskd.csrfToken) || '';
function agentflowAgentDialog(open) {
var modal = document.getElementById('agentflow-agent-modal');
var select = document.getElementById('agentflow-agent-select');
var trigger = document.getElementById('agentflow-agent-open');
if (!modal) return;
if (open) {
modal.classList.add('open');
if (select) select.focus();
} else {
modal.classList.remove('open');
if (trigger) trigger.focus();
}
}
function agentflowInstallAgent() {
var selected = document.getElementById('agentflow-agent-select');
var button = document.getElementById('agentflow-agent-install-now');
if (!selected || !button) return;
if (!window.taskd || !window.taskd.show_log) {
alert('<%:Task log window is not available.%>');
return;
}
button.disabled = true;
(new XHR()).post(agentflowAgentInstallUrl, {
agent: selected.value,
token: agentflowCsrfToken
}, function(x, data) {
button.disabled = false;
if (!data) {
try { data = JSON.parse(x.responseText || '{}'); }
catch (e) { data = {}; }
}
if (!data.ok) {
alert(data.error || '<%:Failed to start agent installation.%>');
return;
}
agentflowAgentDialog(false);
window.taskd.show_log(data.task_id || 'agentflow-agent-install');
});
}
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
var el = document.getElementById('agentflow_status');
var openButton = document.getElementById('agentflow-open');
if (st && el) {
if (!st.running) {
if (openButton) openButton.style.display = 'none';
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
} else {
if (openButton) openButton.style.display = '';
var accessMode = '';
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
@@ -13,11 +82,14 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)
accessMode = '<br/><em><%:Access mode: external port.%></em>';
}
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ accessMode
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('" + agentflowOpenUrl + "')\"/>";
+ accessMode;
}
}
});
document.addEventListener('keydown', function(ev) {
if (ev.key === 'Escape') agentflowAgentDialog(false);
});
//]]></script>
<fieldset class="cbi-section">
@@ -25,4 +97,29 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)
<p id="agentflow_status">
<em><%:Collecting data...%></em>
</p>
<div class="agentflow-actions">
<input id="agentflow-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Click to open AgentFlow%>" onclick="window.open(agentflowOpenUrl)" style="display:none">
<input id="agentflow-agent-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install agent%>" onclick="agentflowAgentDialog(true)">
</div>
</fieldset>
<div id="agentflow-agent-modal" class="agentflow-agent-modal" role="dialog" aria-modal="true" aria-labelledby="agentflow-agent-title" onclick="if (event.target === this) agentflowAgentDialog(false)">
<div class="agentflow-agent-dialog">
<h3 id="agentflow-agent-title"><%:Install agent%></h3>
<p><%:Select a coding agent to install into the shared AgentFlow runtime.%></p>
<div class="agentflow-agent-field">
<label for="agentflow-agent-select"><%:Agent%></label>
<select id="agentflow-agent-select" class="cbi-input-select">
<option value="codexcli">Codex</option>
<option value="claude-code">Claude Code</option>
<option value="opencode">OpenCode</option>
<option value="kimi">Kimi</option>
<option value="reasonix">Reasonix</option>
</select>
</div>
<div class="agentflow-agent-dialog-actions">
<input class="btn cbi-button" type="button" value="<%:Cancel%>" onclick="agentflowAgentDialog(false)">
<input id="agentflow-agent-install-now" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install now%>" onclick="agentflowInstallAgent()">
</div>
</div>
</div>
+21
View File
@@ -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 "启动智能体安装失败。"
@@ -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('<select id="agentflow-agent-select"', status)
for agent in ("codexcli", "claude-code", "opencode", "kimi", "reasonix"):
self.assertIn(f'<option value="{agent}">', status)
self.assertNotIn('type="radio"', status)
self.assertIn('id="agentflow-open"', status)
self.assertIn('id="agentflow-agent-open"', status)
self.assertGreater(status.index('id="agentflow-agent-open"'), status.index('id="agentflow_status"'))
self.assertIn('margin:12px;', status)
self.assertIn("var selected = document.getElementById('agentflow-agent-select')", status)
self.assertIn("window.taskd.show_log(data.task_id", status)
self.assertNotIn('local token =', status)
self.assertIn("window.taskd.csrfToken", status)
self.assertIn("token: agentflowCsrfToken", status)
self.assertIn('http.formvalue("agent")', controller)
self.assertIn('codexcli = true', controller)
self.assertIn('["claude-code"] = true', controller)
self.assertIn('opencode = true', controller)
self.assertIn('kimi = true', controller)
self.assertIn('reasonix = true', controller)
self.assertIn('context.authtoken or context.token', controller)
self.assertIn('http.formvalue("token") ~= expected', controller)
self.assertIn('/etc/init.d/tasks task_add ', controller)
self.assertIn('task_id = "agentflow-agent-install"', controller)
def test_modal_theme_prefers_body_attribute_then_system_theme(self):
status = self.read("luasrc/view/agentflow/status.htm")
self.assertIn('body[theme="light"] .agentflow-agent-dialog', status)
self.assertIn('body[theme="dark"] .agentflow-agent-dialog', status)
self.assertIn('@media (prefers-color-scheme: dark)', status)
self.assertIn('body:not([theme]) .agentflow-agent-dialog', status)
def test_controller_runs_remote_installer_inside_taskd(self):
controller = self.read("luasrc/controller/agentflow.lua")
self.assertFalse((APP_DIR / "root/usr/libexec/istorec/agentflow-agent.sh").exists())
self.assertNotIn("/usr/libexec/istorec/agentflow-agent.sh", controller)
self.assertIn("installapp/installapp-mise.sh", controller)
self.assertIn("istore_runtime_env", controller)
self.assertIn('wget -O "$installer" "$installer_url"', controller)
self.assertIn('/bin/sh "$installer" "$agent"', controller)
self.assertIn('task_script = "/tmp/agentflow-agent-install.sh"', controller)
self.assertIn("fs.writefile(task_script", controller)
self.assertIn('rm -f "$installer" "$0"', controller)
self.assertIn('local command = "/bin/sh "', controller)
self.assertNotIn('local command = "/bin/sh -c "', controller)
self.assertLess(controller.index("istore_runtime_env"), controller.index('"set -u"'))
if __name__ == "__main__":
unittest.main()