mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
🦄 Sync 2026-09-21 17:33:30
This commit is contained in:
1 parent
1e082ed4ab
commit
4294063e8f
23 files changed
+831
-202
No files matched your search
@@ -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>
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user