🦄 Sync 2026-09-21 17:33:30

This commit is contained in:
github-actions[bot] committed 2026-09-21 17:33:31 +08:00
1 parent 1e082ed4ab
commit 4294063e8f
23 files changed
+831 -202

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
PKG_VERSION:=1.0.1
PKG_RELEASE:=6
PKG_RELEASE:=7
LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
@@ -5,6 +5,13 @@ module("luci.controller.agentflow", package.seeall)
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
local DEFAULT_BASE_PATH = "/apps/agentflow/"
local DEFAULT_PORT = 9000
local AGENTS = {
{ id = "codexcli", package = "@openai/codex" },
{ id = "claude-code", package = "@anthropic-ai/claude-code" },
{ id = "opencode", package = "opencode-ai" },
{ id = "kimi", package = "@moonshot-ai/kimi-code" },
{ id = "reasonix", package = "reasonix" }
}
function index()
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
@@ -133,10 +140,54 @@ local function agentflow_entry_url()
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
end
local function node_modules_root()
local sys = require "luci.sys"
local util = require "luci.util"
local script = table.concat({
'. /lib/functions/mise.sh 2>/dev/null || exit 1',
'istore_runtime_env >/dev/null 2>&1 || exit 1',
'mise_bin="$(command -v mise-istore || command -v mise)"',
'[ -n "$mise_bin" ] || exit 1',
'node_dir="$("$mise_bin" where node@lts 2>/dev/null)"',
'[ -n "$node_dir" ] || exit 1',
'printf "%s/lib/node_modules" "$node_dir"'
}, "; ")
local root = sys.exec("/bin/sh -c " .. util.shellquote(script)) or ""
root = root:match("^%s*(.-)%s*$")
if root == "" or root:sub(1, 1) ~= "/" then
return nil
end
return root
end
local function agent_statuses()
local fs = require "nixio.fs"
local jsonc = require "luci.jsonc"
local root = node_modules_root()
local statuses = {}
for _, agent in ipairs(AGENTS) do
local status = { id = agent.id, installed = false }
if root then
local package_file = root .. "/" .. agent.package .. "/package.json"
local package_data = fs.readfile(package_file)
local package_json = package_data and jsonc.parse(package_data) or nil
if type(package_json) == "table" then
status.installed = true
status.version = package_json.version
end
end
statuses[#statuses + 1] = status
end
return statuses, root ~= nil
end
function agentflow_status()
local sys = require "luci.sys"
local port, base_path = agentflow_config()
local entry_url = agentflow_entry_url()
local agents, agents_available = agent_statuses()
local status = {
running = (sys.call("pidof agentflow >/dev/null") == 0),
@@ -145,7 +196,9 @@ function agentflow_status()
entry_url = entry_url,
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
linkeasefull_running = linkeasefull_running()
linkeasefull_running = linkeasefull_running(),
agents_available = agents_available,
agents = agents
}
write_json(status)
end
@@ -158,20 +211,17 @@ function agentflow_agent_install()
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
}
local supported_agents = {}
for _, supported_agent in ipairs(AGENTS) do
supported_agents[supported_agent.id] = true
end
if not require_post_csrf() then
return
end
local agent = http.formvalue("agent") or ""
if not agents[agent] then
if not supported_agents[agent] then
write_json({ ok = false, error = "unsupported agent" })
return
end
@@ -1,58 +1,378 @@
<%+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-shell {
--af-panel:#fff;
--af-panel-soft:#f6f8fc;
--af-border:#dfe5ef;
--af-text:#17213d;
--af-muted:#667397;
--af-primary:#356cf6;
--af-primary-hover:#2559d8;
--af-success:#159447;
--af-success-soft:#e9f8ef;
--af-shadow:0 8px 24px rgba(46, 67, 112, .08);
color:var(--af-text);
margin:12px 0 20px;
}
.agentflow-panel {
background:var(--af-panel);
border:1px solid var(--af-border);
border-radius:10px;
box-shadow:var(--af-shadow);
box-sizing:border-box;
}
.agentflow-overview {
display:grid;
grid-template-columns:minmax(260px, .9fr) minmax(320px, 1.1fr);
gap:28px;
padding:24px;
}
.agentflow-service {
display:flex;
align-items:flex-start;
gap:16px;
}
.agentflow-service-icon {
display:flex;
align-items:center;
justify-content:center;
width:54px;
height:54px;
flex:0 0 54px;
border-radius:50%;
background:#eef1f6;
color:var(--af-muted);
font-size:28px;
font-weight:700;
}
.agentflow-service-icon.running {
background:var(--af-success-soft);
color:var(--af-success);
}
.agentflow-service-copy { min-width:0; }
.agentflow-service-copy h3 {
margin:2px 0 6px;
color:var(--af-text);
font-size:20px;
line-height:1.3;
}
.agentflow-service-copy p {
margin:0 0 16px;
color:var(--af-muted);
line-height:1.6;
}
.agentflow-access {
padding-left:28px;
border-left:1px solid var(--af-border);
}
.agentflow-access label {
display:block;
margin:0 0 9px;
font-weight:700;
}
.agentflow-address-row {
display:flex;
gap:8px;
}
.agentflow-address-row input[type="text"] {
min-width:0;
flex:1 1 auto;
height:40px;
box-sizing:border-box;
border:1px solid var(--af-border);
border-radius:7px;
background:var(--af-panel-soft);
color:var(--af-text);
font-variant-numeric:tabular-nums;
}
.agentflow-access-note {
min-height:20px;
margin:10px 0 0;
color:var(--af-muted);
font-size:13px;
}
.agentflow-button {
min-height:40px;
box-sizing:border-box;
border-radius:7px !important;
white-space:nowrap;
}
.agentflow-primary {
border-color:var(--af-primary) !important;
background:var(--af-primary) !important;
color:#fff !important;
}
.agentflow-primary:hover { background:var(--af-primary-hover) !important; }
.agentflow-button:disabled { cursor:not-allowed; opacity:.55; }
.agentflow-agents { margin-top:16px; padding:22px 20px 20px; }
.agentflow-section-heading {
display:flex;
align-items:flex-start;
gap:12px;
margin-bottom:18px;
}
.agentflow-section-mark {
display:flex;
align-items:center;
justify-content:center;
width:38px;
height:38px;
flex:0 0 38px;
border-radius:9px;
background:#edf3ff;
color:var(--af-primary);
font-size:17px;
font-weight:800;
}
.agentflow-section-heading h3 { margin:0 0 4px; color:var(--af-text); font-size:20px; }
.agentflow-section-heading p { margin:0; color:var(--af-muted); line-height:1.55; }
.agentflow-agent-grid {
display:grid;
grid-template-columns:repeat(3, minmax(0, 1fr));
gap:14px;
}
.agentflow-agent-card {
display:flex;
min-width:0;
min-height:190px;
padding:16px;
box-sizing:border-box;
flex-direction:column;
border:1px solid var(--af-border);
border-radius:9px;
background:var(--af-panel);
}
.agentflow-agent-card[data-state="installed"] { border-color:#b8dfc6; }
.agentflow-agent-top { display:flex; align-items:flex-start; gap:12px; }
.agentflow-agent-logo {
display:flex;
align-items:center;
justify-content:center;
width:48px;
height:48px;
flex:0 0 48px;
border-radius:9px;
background:var(--agent-color, var(--af-primary));
color:#fff;
font-size:21px;
font-weight:800;
}
.agentflow-agent-copy { min-width:0; }
.agentflow-agent-title { margin:2px 0 4px; color:var(--af-text); font-size:17px; font-weight:700; line-height:1.4; }
.agentflow-agent-copy p { margin:0; color:var(--af-muted); font-size:13px; line-height:1.45; }
.agentflow-agent-meta {
display:flex;
align-items:center;
gap:10px;
margin:17px 0 12px;
color:var(--af-muted);
font-size:13px;
}
.agentflow-agent-state { display:inline-flex; align-items:center; gap:7px; font-weight:700; }
.agentflow-agent-dot { width:8px; height:8px; border-radius:50%; background:#8a95af; }
.agentflow-agent-card[data-state="installed"] .agentflow-agent-state { color:var(--af-success); }
.agentflow-agent-card[data-state="installed"] .agentflow-agent-dot { background:var(--af-success); box-shadow:0 0 0 4px var(--af-success-soft); }
.agentflow-agent-divider { color:#b4bdd0; }
.agentflow-install-button { width:100%; margin-top:auto; }
.agentflow-agent-card[data-state="installed"] .agentflow-install-button {
border-color:var(--af-success) !important;
background:var(--af-success-soft) !important;
color:var(--af-success) !important;
}
.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, .48);
}
.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; }
.agentflow-agent-dialog {
width:100%;
max-width:460px;
padding:22px;
box-sizing:border-box;
border:1px solid var(--af-border);
border-radius:10px;
background:var(--af-panel);
color:var(--af-text);
box-shadow:0 18px 48px rgba(15, 23, 42, .24);
}
.agentflow-agent-dialog h3 { margin:0 0 8px; color:var(--af-text); }
.agentflow-agent-dialog p { margin:0; color:var(--af-muted); line-height:1.6; }
.agentflow-agent-dialog strong { color:var(--af-text); }
.agentflow-dialog-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:20px; }
body[theme="dark"] .agentflow-shell {
--af-panel:#202733;
--af-panel-soft:#171d27;
--af-border:#3b4658;
--af-text:#edf2fb;
--af-muted:#adb8cb;
--af-primary:#5d88ff;
--af-primary-hover:#7197ff;
--af-success:#50c878;
--af-success-soft:rgba(80, 200, 120, .14);
--af-shadow:0 8px 24px rgba(0, 0, 0, .18);
}
body[theme="dark"] .agentflow-section-mark { background:rgba(93, 136, 255, .14); }
@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; }
body:not([theme]) .agentflow-shell {
--af-panel:#202733;
--af-panel-soft:#171d27;
--af-border:#3b4658;
--af-text:#edf2fb;
--af-muted:#adb8cb;
--af-primary:#5d88ff;
--af-primary-hover:#7197ff;
--af-success:#50c878;
--af-success-soft:rgba(80, 200, 120, .14);
--af-shadow:0 8px 24px rgba(0, 0, 0, .18);
}
body:not([theme]) .agentflow-section-mark { background:rgba(93, 136, 255, .14); }
}
@media (max-width:1000px) {
.agentflow-agent-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); }
}
@media (max-width:760px) {
.agentflow-overview { grid-template-columns:1fr; gap:20px; padding:18px; }
.agentflow-access { padding:18px 0 0; border-top:1px solid var(--af-border); border-left:0; }
.agentflow-agent-grid { grid-template-columns:1fr; }
.agentflow-agents { padding:18px 14px 14px; }
.agentflow-address-row { flex-wrap:wrap; }
.agentflow-address-row input[type="text"] { flex-basis:100%; }
.agentflow-address-row .agentflow-button { flex:1 1 auto; }
}
</style>
<script type="text/javascript">//<![CDATA[
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
var agentflowStatusUrl = '<%=url("admin/services/agentflow_status")%>';
var agentflowAgentInstallUrl = '<%=url("admin/services/agentflow/agent_install")%>';
var agentflowCsrfToken = (window.taskd && window.taskd.csrfToken) || '';
var agentflowSelectedAgent = '';
var agentflowAgentNames = {
codexcli: 'Codex',
'claude-code': 'Claude Code',
opencode: 'OpenCode',
kimi: 'Kimi',
reasonix: 'Reasonix'
};
var agentflowLabels = {
installed: '<%:Installed%>',
notInstalled: '<%:Not installed%>',
unknown: '<%:Status unavailable%>',
install: '<%:Install%>',
starting: '<%:Starting...%>',
copied: '<%:Copied%>'
};
function agentflowAgentDialog(open) {
function agentflowAbsoluteUrl(value) {
try { return new URL(value, window.location.href).href; }
catch (e) { return value || ''; }
}
function agentflowRenderStatus(st) {
var icon = document.getElementById('agentflow-service-icon');
var title = document.getElementById('agentflow-service-title');
var description = document.getElementById('agentflow-service-description');
var openButton = document.getElementById('agentflow-open');
var address = document.getElementById('agentflow-address');
var statuses = {};
var agents = st && st.agents || [];
var i;
if (!st) return;
if (icon) {
icon.className = 'agentflow-service-icon' + (st.running ? ' running' : '');
icon.textContent = st.running ? '\u2713' : '!';
}
if (title) title.textContent = st.running ? '<%:AgentFlow service is running%>' : '<%:AgentFlow service is not running%>';
if (description) description.textContent = st.running
? '<%:The service is ready. You can open AgentFlow or install coding agents.%>'
: '<%:Start the service to use AgentFlow. Agent installation remains available.%>';
if (openButton) openButton.disabled = !st.running;
if (address) address.value = agentflowAbsoluteUrl(st.entry_url || '');
for (i = 0; i < agents.length; i++) statuses[agents[i].id] = agents[i];
for (i in agentflowAgentNames) {
if (!agentflowAgentNames.hasOwnProperty(i)) continue;
var card = document.getElementById('agentflow-agent-' + i);
var state = document.getElementById('agentflow-agent-state-' + i);
var version = document.getElementById('agentflow-agent-version-' + i);
var installButton = document.getElementById('agentflow-agent-install-' + i);
var item = statuses[i];
var available = st.agents_available !== false && !!item;
var installed = available && item.installed === true;
if (card) card.setAttribute('data-state', installed ? 'installed' : (available ? 'available' : 'unknown'));
if (state) state.textContent = installed ? agentflowLabels.installed : (available ? agentflowLabels.notInstalled : agentflowLabels.unknown);
if (version) version.textContent = installed && item.version ? item.version : '-';
if (installButton) {
installButton.disabled = !available || installed;
installButton.value = installed ? agentflowLabels.installed : agentflowLabels.install;
}
}
}
function agentflowCopyAddress() {
var address = document.getElementById('agentflow-address');
var feedback = document.getElementById('agentflow-copy-feedback');
if (!address || !address.value) return;
function done() {
if (!feedback) return;
feedback.textContent = agentflowLabels.copied;
window.setTimeout(function() { feedback.textContent = ''; }, 1800);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(address.value).then(done, function() {
address.select();
document.execCommand('copy');
done();
});
} else {
address.select();
document.execCommand('copy');
done();
}
}
function agentflowAgentDialog(open, agent) {
var modal = document.getElementById('agentflow-agent-modal');
var select = document.getElementById('agentflow-agent-select');
var trigger = document.getElementById('agentflow-agent-open');
var name = document.getElementById('agentflow-selected-agent-name');
var confirmButton = document.getElementById('agentflow-agent-install-now');
if (!modal) return;
if (open) {
agentflowSelectedAgent = agent || '';
if (name) name.textContent = agentflowAgentNames[agentflowSelectedAgent] || agentflowSelectedAgent;
modal.classList.add('open');
if (select) select.focus();
if (confirmButton) confirmButton.focus();
} else {
modal.classList.remove('open');
var trigger = document.getElementById('agentflow-agent-install-' + agentflowSelectedAgent);
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 (!agentflowSelectedAgent || !button) return;
if (!window.taskd || !window.taskd.show_log) {
alert('<%:Task log window is not available.%>');
return;
}
button.disabled = true;
button.value = agentflowLabels.starting;
(new XHR()).post(agentflowAgentInstallUrl, {
agent: selected.value,
agent: agentflowSelectedAgent,
token: agentflowCsrfToken
}, function(x, data) {
button.disabled = false;
button.value = '<%:Install now%>';
if (!data) {
try { data = JSON.parse(x.responseText || '{}'); }
catch (e) { data = {}; }
@@ -66,25 +386,8 @@ function agentflowInstallAgent() {
});
}
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>';
} else {
accessMode = '<br/><em><%:Access mode: external port.%></em>';
}
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ accessMode;
}
}
XHR.poll(5, agentflowStatusUrl, null, function(x, st) {
agentflowRenderStatus(st);
});
document.addEventListener('keydown', function(ev) {
@@ -92,34 +395,71 @@ document.addEventListener('keydown', function(ev) {
});
//]]></script>
<fieldset class="cbi-section">
<legend><%:Status%></legend>
<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 class="agentflow-shell">
<section class="agentflow-panel agentflow-overview" aria-labelledby="agentflow-service-title">
<div class="agentflow-service">
<div id="agentflow-service-icon" class="agentflow-service-icon" aria-hidden="true">...</div>
<div class="agentflow-service-copy">
<h3 id="agentflow-service-title"><%:Checking AgentFlow service%></h3>
<p id="agentflow-service-description"><%:Collecting data...%></p>
<input id="agentflow-open" class="btn cbi-button agentflow-button agentflow-primary" type="button" value="<%:Open AgentFlow%>" onclick="window.open(agentflowOpenUrl)" disabled>
</div>
</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 class="agentflow-access">
<label for="agentflow-address"><%:Local network access%></label>
<div class="agentflow-address-row">
<input id="agentflow-address" type="text" readonly value="">
<input class="btn cbi-button agentflow-button" type="button" value="<%:Copy%>" onclick="agentflowCopyAddress()">
</div>
<p class="agentflow-access-note"><%:Open this address in a browser to access the AgentFlow Web interface.%> <span id="agentflow-copy-feedback" role="status" aria-live="polite"></span></p>
</div>
</section>
<section class="agentflow-panel agentflow-agents" aria-labelledby="agentflow-agents-title">
<div class="agentflow-section-heading">
<div class="agentflow-section-mark" aria-hidden="true">A</div>
<div>
<h3 id="agentflow-agents-title"><%:Agent installation%></h3>
<p><%:Install coding agents into the shared AgentFlow runtime. Installed agents show their current version.%></p>
</div>
</div>
<div class="agentflow-agent-grid">
<div id="agentflow-agent-codexcli" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#356cf6">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">C</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Codex</div><p><%:OpenAI coding agent for software development tasks.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-codexcli"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-codexcli">-</span></span></div>
<input id="agentflow-agent-install-codexcli" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'codexcli')" disabled>
</div>
<div id="agentflow-agent-claude-code" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#dc633d">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">C</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Claude Code</div><p><%:Anthropic coding agent for code analysis and development.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-claude-code"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-claude-code">-</span></span></div>
<input id="agentflow-agent-install-claude-code" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'claude-code')" disabled>
</div>
<div id="agentflow-agent-opencode" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#6a3ee8">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">O</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">OpenCode</div><p><%:Open source coding agent with a flexible terminal workflow.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-opencode"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-opencode">-</span></span></div>
<input id="agentflow-agent-install-opencode" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'opencode')" disabled>
</div>
<div id="agentflow-agent-kimi" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#1677d2">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">K</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Kimi</div><p><%:Moonshot AI coding agent for terminal-based development.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-kimi"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-kimi">-</span></span></div>
<input id="agentflow-agent-install-kimi" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'kimi')" disabled>
</div>
<div id="agentflow-agent-reasonix" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#3f526f">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">R</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Reasonix</div><p><%:AI coding agent for software engineering workflows.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-reasonix"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-reasonix">-</span></span></div>
<input id="agentflow-agent-install-reasonix" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'reasonix')" disabled>
</div>
</div>
</section>
<div id="agentflow-agent-modal" class="agentflow-agent-modal" role="dialog" aria-modal="true" aria-labelledby="agentflow-agent-dialog-title" onclick="if (event.target === this) agentflowAgentDialog(false)">
<div class="agentflow-agent-dialog">
<h3 id="agentflow-agent-dialog-title"><%:Confirm agent installation%></h3>
<p><%:The installer will download and install%> <strong id="agentflow-selected-agent-name"></strong> <%:into the shared AgentFlow runtime.%></p>
<div class="agentflow-dialog-actions">
<input class="btn cbi-button agentflow-button" type="button" value="<%:Cancel%>" onclick="agentflowAgentDialog(false)">
<input id="agentflow-agent-install-now" class="btn cbi-button agentflow-button agentflow-primary" type="button" value="<%:Install now%>" onclick="agentflowInstallAgent()">
</div>
</div>
</div>
</div>
+81
View File
@@ -72,3 +72,84 @@ msgstr "任务日志窗口不可用。"
msgid "Failed to start agent installation."
msgstr "启动智能体安装失败。"
msgid "Installed"
msgstr "已安装"
msgid "Not installed"
msgstr "未安装"
msgid "Status unavailable"
msgstr "状态不可用"
msgid "Install"
msgstr "安装"
msgid "Starting..."
msgstr "正在启动……"
msgid "Copied"
msgstr "已复制"
msgid "Copy"
msgstr "复制"
msgid "AgentFlow service is running"
msgstr "AgentFlow 服务运行中"
msgid "AgentFlow service is not running"
msgstr "AgentFlow 服务未运行"
msgid "The service is ready. You can open AgentFlow or install coding agents."
msgstr "服务已就绪,可以打开 AgentFlow 或安装编码智能体。"
msgid "Start the service to use AgentFlow. Agent installation remains available."
msgstr "请启动服务后使用 AgentFlow;智能体安装仍然可用。"
msgid "Checking AgentFlow service"
msgstr "正在检查 AgentFlow 服务"
msgid "Open AgentFlow"
msgstr "打开 AgentFlow"
msgid "Local network access"
msgstr "局域网访问"
msgid "Open this address in a browser to access the AgentFlow Web interface."
msgstr "在浏览器中打开此地址即可访问 AgentFlow Web 界面。"
msgid "Agent installation"
msgstr "智能体安装"
msgid "Install coding agents into the shared AgentFlow runtime. Installed agents show their current version."
msgstr "将编码智能体安装到 AgentFlow 共享运行环境中;已安装的智能体会显示当前版本。"
msgid "OpenAI coding agent for software development tasks."
msgstr "OpenAI 提供的代码智能体,适用于软件开发任务。"
msgid "Anthropic coding agent for code analysis and development."
msgstr "Anthropic 提供的代码智能体,适用于代码分析与开发。"
msgid "Open source coding agent with a flexible terminal workflow."
msgstr "开源代码智能体,提供灵活的终端开发工作流。"
msgid "Moonshot AI coding agent for terminal-based development."
msgstr "Moonshot AI 提供的代码智能体,适用于终端开发。"
msgid "AI coding agent for software engineering workflows."
msgstr "面向软件工程工作流的 AI 代码智能体。"
msgid "Checking..."
msgstr "正在检查……"
msgid "Version"
msgstr "版本"
msgid "Confirm agent installation"
msgstr "确认安装智能体"
msgid "The installer will download and install"
msgstr "安装程序将下载并安装"
msgid "into the shared AgentFlow runtime."
msgstr "到 AgentFlow 共享运行环境。"
@@ -16,37 +16,57 @@ class AgentInstallContractTest(unittest.TestCase):
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(f'id="agentflow-agent-{agent}"', status)
self.assertIn(f"agentflowAgentDialog(true, '{agent}')", status)
self.assertNotIn('<select id="agentflow-agent-select"', status)
self.assertNotIn("<h4>", status)
self.assertEqual(status.count('class="agentflow-agent-title"'), 5)
self.assertNotIn("Update", status)
self.assertNotIn("Uninstall", 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('id="agentflow-agent-install-now"', status)
self.assertIn("agentflowSelectedAgent", 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)
for agent in ("codexcli", "claude-code", "opencode", "kimi", "reasonix"):
self.assertIn(f'id = "{agent}"', controller)
self.assertIn('supported_agents[supported_agent.id] = 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_status_reports_installed_agents_from_shared_mise_runtime(self):
controller = self.read("luasrc/controller/agentflow.lua")
status = self.read("luasrc/view/agentflow/status.htm")
self.assertIn("istore_runtime_env", controller)
self.assertIn('where node@lts', controller)
for package in (
"@openai/codex",
"@anthropic-ai/claude-code",
"opencode-ai",
"@moonshot-ai/kimi-code",
"reasonix",
):
self.assertIn(f'package = "{package}"', controller)
self.assertIn('status.version = package_json.version', controller)
self.assertIn('agents_available = agents_available', controller)
self.assertIn('st.agents_available', status)
self.assertIn('item.installed === true', status)
self.assertIn('installButton.disabled = !available || installed', status)
self.assertIn('installed ? agentflowLabels.installed : agentflowLabels.install', status)
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('body[theme="dark"] .agentflow-shell', status)
self.assertIn('@media (prefers-color-scheme: dark)', status)
self.assertIn('body:not([theme]) .agentflow-agent-dialog', status)
self.assertIn('body:not([theme]) .agentflow-shell', status)
def test_controller_runs_remote_installer_inside_taskd(self):
controller = self.read("luasrc/controller/agentflow.lua")