mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 00:52:51 +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,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=apfree-wifidog
|
||||
PKG_VERSION:=9.09.2938
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=9.09.2947
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.09.18
|
||||
PKG_RELEASE:=45
|
||||
PKG_VERSION:=2026.09.20
|
||||
PKG_RELEASE:=46
|
||||
|
||||
PKG_SOURCE:=dae-src-2026.09.18-dc29e60bab23.tar.gz
|
||||
PKG_SOURCE:=dae-src-2026.09.20-2a846ee5a7e4.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -5,15 +5,20 @@
|
||||
KPROBE_SKB_AT(3)
|
||||
KPROBE_SKB_AT(4)
|
||||
-KPROBE_SKB_AT(5)
|
||||
|
||||
|
||||
SEC("kprobe/skb_lifetime_termination")
|
||||
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
|
||||
--- a/trace/trace.go
|
||||
+++ b/trace/trace.go
|
||||
@@ -191,7 +191,6 @@ func attachBpfToTargets(objs *bpfObjects, targets map[string]int) (links []link.Link, err error) {
|
||||
1: objs.KprobeSkb1,
|
||||
2: objs.KprobeSkb2,
|
||||
3: objs.KprobeSkb3,
|
||||
4: objs.KprobeSkb4,
|
||||
@@ -191,11 +191,10 @@
|
||||
progByPos := map[int]*ebpf.Program{
|
||||
1: objs.KprobeSkb1,
|
||||
2: objs.KprobeSkb2,
|
||||
3: objs.KprobeSkb3,
|
||||
4: objs.KprobeSkb4,
|
||||
- 5: objs.KprobeSkb5,
|
||||
}
|
||||
}
|
||||
symbolsByPos := make(map[int][]string, len(progByPos))
|
||||
for fn, pos := range targets {
|
||||
if _, ok := progByPos[pos]; !ok {
|
||||
continue
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.09.18
|
||||
PKG_RELEASE:=56
|
||||
PKG_VERSION:=2026.09.20
|
||||
PKG_RELEASE:=57
|
||||
|
||||
PKG_SOURCE:=daed-src-2026.09.18-0d4c9d0c95b9.tar.gz
|
||||
PKG_SOURCE:=daed-src-2026.09.20-d00211e69e94.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -5,15 +5,20 @@
|
||||
KPROBE_SKB_AT(3)
|
||||
KPROBE_SKB_AT(4)
|
||||
-KPROBE_SKB_AT(5)
|
||||
|
||||
|
||||
SEC("kprobe/skb_lifetime_termination")
|
||||
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
|
||||
--- a/dae-core/trace/trace.go
|
||||
+++ b/dae-core/trace/trace.go
|
||||
@@ -191,7 +191,6 @@ func attachBpfToTargets(objs *bpfObjects, targets map[string]int) (links []link.Link, err error) {
|
||||
1: objs.KprobeSkb1,
|
||||
2: objs.KprobeSkb2,
|
||||
3: objs.KprobeSkb3,
|
||||
4: objs.KprobeSkb4,
|
||||
@@ -191,11 +191,10 @@
|
||||
progByPos := map[int]*ebpf.Program{
|
||||
1: objs.KprobeSkb1,
|
||||
2: objs.KprobeSkb2,
|
||||
3: objs.KprobeSkb3,
|
||||
4: objs.KprobeSkb4,
|
||||
- 5: objs.KprobeSkb5,
|
||||
}
|
||||
}
|
||||
symbolsByPos := make(map[int][]string, len(progByPos))
|
||||
for fn, pos := range targets {
|
||||
if _, ok := progByPos[pos]; !ok {
|
||||
continue
|
||||
@@ -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")
|
||||
|
||||
@@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (firewall4/nft; iptables-tagged lines
|
||||
LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.44
|
||||
PKG_RELEASE:=28
|
||||
PKG_VERSION:=0.1.45
|
||||
PKG_RELEASE:=29
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
@@ -28,11 +28,34 @@ case "$${IPKG_INSTROOT}" in
|
||||
''|/) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
# The opkg wrapper invokes this body as <wrapper> <action>.
|
||||
# OpenWrt APK post-upgrade/pre-deinstall wiring is in package-pack.mk;
|
||||
# post-upgrade exports PKG_UPGRADE=1. This guard is belt-and-braces with
|
||||
# the upgrade action arg.
|
||||
# A version-shaped $$1 with empty $$2 is APK uninstall (pre-deinstall).
|
||||
# Same-version APK reinstall uses post-upgrade and does not run this hook.
|
||||
# A version-changing APK upgrade is indistinguishable from uninstall at
|
||||
# this hook (both pass the old version as $$1). Do not claim that path
|
||||
# was lab-proven.
|
||||
[ "$${PKG_UPGRADE:-}" = 1 ] && exit 0
|
||||
case "$${2-}" in
|
||||
remove) ;;
|
||||
upgrade) exit 0 ;;
|
||||
'')
|
||||
case "$${1-}" in
|
||||
remove) ;;
|
||||
upgrade|''|[!0-9]*) exit 0 ;;
|
||||
# Character-class '.' so the version arm is a literal dot (1a2 must not restore).
|
||||
[0-9]*[.][0-9]*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
. /usr/libexec/fwlive-logging.sh
|
||||
# Best-effort restore: always exit 0 so a transient lock/pending-staging
|
||||
# failure cannot strand the package half-removed. Baseline is kept on
|
||||
# failure (restore_wan_log_baseline) for admin recovery; helpers are still
|
||||
# present while this prerm runs.
|
||||
# Best-effort restore still exits 0 so a transient lock/pending-staging failure
|
||||
# cannot strand the package half-removed. The baseline is kept on failure for
|
||||
# admin recovery; helpers are still present while this prerm runs.
|
||||
restore_wan_log_baseline || logger -t fwlive "WAN log baseline restore failed during uninstall"
|
||||
exit 0
|
||||
endef
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
return baseclass.extend({
|
||||
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */
|
||||
APP_VERSION: '0.1.44',
|
||||
APP_VERSION: '0.1.45',
|
||||
ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000],
|
||||
DEFAULT_ROW_LIMIT: 100,
|
||||
FETCH_MODE_OPTIONS: ['auto', 'manual'],
|
||||
|
||||
@@ -184,7 +184,7 @@ return view.extend({
|
||||
{ tab: 'netsetup', type: form.Value, id: 'wan_pppoe_user', title: _('PPPoE Username'), depends: { wan_proto: 'pppoe' } },
|
||||
{ tab: 'netsetup', type: form.Value, id: 'wan_pppoe_pass', title: _('PPPoE Password'), depends: { wan_proto: 'pppoe' }, password: true },
|
||||
{ tab: 'netsetup', type: form.Value, id: 'lan_ipaddr', title: _('LAN IPv4 Address'), datatype: 'ip4addr', placeholder: '10.0.0.1' },
|
||||
{ tab: 'netsetup', type: form.DynamicList, id: 'lan_dns', title: _('Custom DNS Server(s)'), datatype: 'ipaddr', placeholder: '223.5.5.5' },
|
||||
{ tab: 'netsetup', type: form.DynamicList, id: 'lan_dns', title: _('Custom DNS Server(s)'), datatype: 'ipaddr', placeholder: '223.5.5.5', desc: _('Leave empty for ISP DNS.') },
|
||||
{ tab: 'netsetup', type: form.Value, id: 'lan_gateway', title: _('Gateway Address'), datatype: 'ip4addr', placeholder: '', depends: { wan_proto: 'siderouter' }, desc: _('Primary router IP address used when operating in side-router mode.') },
|
||||
{ tab: 'netsetup', type: form.Flag, id: 'dhcp', title: _('Enable DHCP Server'), desc: _('Enable or disable LAN DHCP server (usually disabled in side-router mode to avoid IP address conflicts).') },
|
||||
{ tab: 'netsetup', type: form.Flag, id: 'ipv6', title: _('IPv6 Support'), desc: _('Enable or disable IPv6 router advertisements and DHCPv6.') },
|
||||
|
||||
@@ -72,6 +72,9 @@ msgstr "启用 DHCP 服务器"
|
||||
msgid "Enable or disable LAN DHCP server (usually disabled in side-router mode to avoid IP address conflicts)."
|
||||
msgstr "开启或关闭局域网 DHCP 服务(旁路由模式下通常建议关闭,避免局域网 IP 分配冲突)。"
|
||||
|
||||
msgid "Leave empty for ISP DNS."
|
||||
msgstr "留空则使用 ISP 运营商DNS"
|
||||
|
||||
msgid "IPv6 Support"
|
||||
msgstr "IPv6 支持"
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wwand
|
||||
PKG_VERSION:=1.6.7_p11
|
||||
PKG_RELEASE:=24
|
||||
PKG_VERSION:=1.6.7_p13
|
||||
PKG_RELEASE:=25
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||
PKG_SOURCE_VERSION:=2ca9a94506f972d89a398d560d5667a809e89d9f
|
||||
PKG_SOURCE_VERSION:=e97ffb8db7ca59049195d9a37ae86b69c1e1264d
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
@@ -205,6 +205,13 @@ return view.extend({
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
|
||||
o = s.taboption('general', form.Flag, 'enabled', _('Enable Client Daemon'),
|
||||
_('Enable unified xkcptun client daemon.'));
|
||||
o.enabled = '1';
|
||||
o.disabled = '0';
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.Value, 'remote_addr', _('Default Server Address'),
|
||||
_('Remote xkcptun server IP address or domain name.'));
|
||||
o.datatype = 'host';
|
||||
@@ -244,10 +251,10 @@ return view.extend({
|
||||
s.tab('advanced', _('Advanced Overrides'));
|
||||
|
||||
// Overview Columns in table
|
||||
o = s.option(form.Flag, 'disabled', _('Enabled'));
|
||||
o.enabled = '0';
|
||||
o.disabled = '1';
|
||||
o.default = '0';
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.enabled = '1';
|
||||
o.disabled = '0';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
@@ -274,10 +281,10 @@ return view.extend({
|
||||
o.placeholder = '127.0.0.1';
|
||||
|
||||
// Modal / Tab: General Settings
|
||||
o = s.taboption('general', form.Flag, 'disabled', _('Enable Tunnel'));
|
||||
o.enabled = '0';
|
||||
o.disabled = '1';
|
||||
o.default = '0';
|
||||
o = s.taboption('general', form.Flag, 'enabled', _('Enable Tunnel'));
|
||||
o.enabled = '1';
|
||||
o.disabled = '0';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
@@ -399,10 +406,10 @@ return view.extend({
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
|
||||
// Overview Columns in table
|
||||
o = s.option(form.Flag, 'disabled', _('Enabled'));
|
||||
o.enabled = '0';
|
||||
o.disabled = '1';
|
||||
o.default = '0';
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.enabled = '1';
|
||||
o.disabled = '0';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
@@ -434,14 +441,14 @@ return view.extend({
|
||||
|
||||
o = s.option(form.Flag, 'fec', _('FEC'));
|
||||
o.modalonly = false;
|
||||
o.default = '0';
|
||||
o.default = '1';
|
||||
o.editable = true;
|
||||
|
||||
// Modal / Tab: General Settings
|
||||
o = s.taboption('general', form.Flag, 'disabled', _('Enable Tunnel'));
|
||||
o.enabled = '0';
|
||||
o.disabled = '1';
|
||||
o.default = '0';
|
||||
o = s.taboption('general', form.Flag, 'enabled', _('Enable Tunnel'));
|
||||
o.enabled = '1';
|
||||
o.disabled = '0';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=my-default-settings
|
||||
PKG_VERSION:=2
|
||||
PKG_RELEASE:=16
|
||||
PKG_RELEASE:=17
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
|
||||
@@ -16,25 +16,33 @@ show_help() {
|
||||
选项:
|
||||
-s, --ssid <名称> 指定基础WiFi名称
|
||||
-p, --password <密码> 指定WiFi密码
|
||||
-f, --force 强制重新执行(忽略 wifi=1 已配置检查)
|
||||
-h, --help 查看帮助信息
|
||||
|
||||
并发与等待机制:
|
||||
- 无参调用时:若已有 mywifi 在执行,直接终止退出;
|
||||
- 带 -f 或传参调用时:若已有 mywifi 在执行,自动排队等待其执行完成后再继续执行。
|
||||
|
||||
WiFi命名规范:
|
||||
2.4 GHz -> <WiFi名>_2.4G
|
||||
5 GHz -> <WiFi名>_5G
|
||||
6 GHz -> <WiFi名>_6G
|
||||
|
||||
使用示例:
|
||||
mywifi # 默认使用 hostname,无密码
|
||||
mywifi "HomeWiFi" # HomeWiFi_2.4G / HomeWiFi_5G,无密码
|
||||
mywifi "HomeWiFi" "12345678" # HomeWiFi_2.4G / HomeWiFi_5G,密码 12345678
|
||||
mywifi -s "HomeWiFi" -p "12345678" # 选项传参
|
||||
mywifi # 默认执行(若已配置过或正在执行则自动终止)
|
||||
mywifi -f # 强制重新按 hostname 恢复默认配置(若有执行中则排队等待)
|
||||
mywifi "HomeWiFi" "12345678" # 设置指定 WiFi 名及密码(若有执行中则排队等待)
|
||||
mywifi -f "HomeWiFi" "12345678" # 强制覆盖设置
|
||||
EOF_HELP
|
||||
}
|
||||
|
||||
# 1. 解析命令行参数(同时兼顾位置参数与选项参数)
|
||||
# -------------------------------------------------------------
|
||||
# 1. 优先解析命令行参数(判断是否带有 -f 或自定义参数)
|
||||
# -------------------------------------------------------------
|
||||
CUSTOM_SSID=""
|
||||
CUSTOM_PASSWD=""
|
||||
HAS_PASSWD=0
|
||||
FORCE=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -42,6 +50,9 @@ while [ $# -gt 0 ]; do
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-f|--force)
|
||||
FORCE=1
|
||||
;;
|
||||
-s|--ssid)
|
||||
shift
|
||||
CUSTOM_SSID="$1"
|
||||
@@ -63,7 +74,94 @@ while [ $# -gt 0 ]; do
|
||||
shift
|
||||
done
|
||||
|
||||
# 密码合法性校验
|
||||
# 判断是否需要等待正在运行的实例完成
|
||||
# 只要指定了 -f 或传入了自定义 SSID / 密码,就启用等待锁机制
|
||||
WAIT_LOCK=0
|
||||
if [ $FORCE -eq 1 ] || [ -n "$CUSTOM_SSID" ] || [ $HAS_PASSWD -eq 1 ]; then
|
||||
WAIT_LOCK=1
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 2. 检查 base_config 状态标记(无参且已配置过 wifi=1 时直接退出)
|
||||
# -------------------------------------------------------------
|
||||
if [ $FORCE -eq 0 ]; then
|
||||
if [ "$(uci -q get base_config.@status[0].wifi 2>/dev/null)" = "1" ]; then
|
||||
if [ -z "$CUSTOM_SSID" ] && [ $HAS_PASSWD -eq 0 ]; then
|
||||
echo "[INFO] WiFi 已配置完成 (base_config.@status[0].wifi=1),无需重复执行,直接终止脚本。"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 3. 并发锁处理:带参/-f 模式等待完成,无参模式直接终止
|
||||
# -------------------------------------------------------------
|
||||
LOCK_FILE="/var/run/mywifi.lock"
|
||||
|
||||
acquire_lock() {
|
||||
local cur_pid=$$
|
||||
local cur_ppid=$PPID
|
||||
|
||||
# A. 优先采用 flock 文件描述符锁(内核自动回收,安全可靠)
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
exec 9>"$LOCK_FILE" 2>/dev/null || true
|
||||
if [ $WAIT_LOCK -eq 1 ]; then
|
||||
if ! flock -n 9 2>/dev/null; then
|
||||
echo "[INFO] 检测到 /sbin/mywifi 已在执行中,等待其执行完成后再继续..."
|
||||
flock -w 60 9 2>/dev/null || {
|
||||
echo "[ERROR] 等待其他 /sbin/mywifi 执行超时(60秒)!" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
else
|
||||
if ! flock -n 9 2>/dev/null; then
|
||||
echo "[INFO] 检测到 /sbin/mywifi 已在执行中,直接终止脚本。"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# B. 备用进程轮询检测机制(兼容无 flock 极简环境)
|
||||
local wait_limit=60
|
||||
local waited=0
|
||||
while true; do
|
||||
local other_running=0
|
||||
for p in $(pgrep -f "mywifi" 2>/dev/null); do
|
||||
if [ "$p" != "$cur_pid" ] && [ "$p" != "$cur_ppid" ]; then
|
||||
local cmd="$(cat /proc/$p/cmdline 2>/dev/null | tr '\0' ' ')"
|
||||
case "$cmd" in
|
||||
*mywifi*)
|
||||
other_running=1
|
||||
break
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $other_running -eq 0 ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $WAIT_LOCK -eq 1 ]; then
|
||||
[ $waited -eq 0 ] && echo "[INFO] 检测到 /sbin/mywifi 已在执行中,等待其执行完成后再继续..."
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
if [ $waited -ge $wait_limit ]; then
|
||||
echo "[ERROR] 等待其他 /sbin/mywifi 执行超时!" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "[INFO] 检测到 /sbin/mywifi 已在执行中,直接终止脚本。"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
acquire_lock
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 4. 密码长度合法性校验(若指定了密码)
|
||||
# -------------------------------------------------------------
|
||||
if [ $HAS_PASSWD -eq 1 ] && [ -n "$CUSTOM_PASSWD" ]; then
|
||||
pwd_len=${#CUSTOM_PASSWD}
|
||||
if [ "$pwd_len" -lt 8 ] || [ "$pwd_len" -gt 63 ]; then
|
||||
@@ -75,7 +173,9 @@ else
|
||||
SSID_PASSWD=""
|
||||
fi
|
||||
|
||||
# 2. 深度就绪检测:确保内核PHY驱动、多频硬件、系统初始化状态和UCI配置全部真正就绪
|
||||
# -------------------------------------------------------------
|
||||
# 5. 深度就绪检测(确保多频硬件驱动加载与 UCI 配置完整就绪)
|
||||
# -------------------------------------------------------------
|
||||
wait_wifi_ready() {
|
||||
local max_wait=15
|
||||
local waited=0
|
||||
@@ -84,34 +184,30 @@ wait_wifi_ready() {
|
||||
local phy_count=0
|
||||
local uci_count=0
|
||||
|
||||
# B. 检查内核物理无线设备 (PHY)
|
||||
# 检查内核物理无线设备 (PHY)
|
||||
local phys="$(ls -d /sys/class/ieee80211/phy* 2>/dev/null)"
|
||||
[ -n "$phys" ] && phy_count=$(echo "$phys" | wc -w)
|
||||
|
||||
# C. 统计 UCI wireless 中实际已解析出的 wifi-device 节点数量(非文件大小)
|
||||
# 检查 UCI 中真实生成的 wifi-device 数量
|
||||
if [ -s /etc/config/wireless ]; then
|
||||
local uci_radios="$(uci -q show wireless 2>/dev/null | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
|
||||
[ -n "$uci_radios" ] && uci_count=$(echo "$uci_radios" | wc -w)
|
||||
fi
|
||||
|
||||
# D. 自动探测与配置补全:
|
||||
# - 物理网卡已加载,但 UCI 配置中的 radio 数量少于物理网卡数量(多频网卡尚未完全生成配置)
|
||||
# - 或者配置文件缺失/没有 radio 配置
|
||||
# 自动补全配置(物理硬件数量 > UCI 配置数量)
|
||||
if [ $phy_count -gt 0 ] && [ $uci_count -lt $phy_count ]; then
|
||||
[ -x /sbin/wifi ] && /sbin/wifi config >/dev/null 2>&1
|
||||
elif [ $phy_count -eq 0 ] && [ $uci_count -eq 0 ] && [ ! -s /etc/config/wireless ]; then
|
||||
[ -x /sbin/wifi ] && /sbin/wifi config >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# 重新同步一次 UCI radio 数量
|
||||
# 重新同步 UCI radio 数量
|
||||
if [ -s /etc/config/wireless ]; then
|
||||
local uci_radios_check="$(uci -q show wireless 2>/dev/null | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
|
||||
[ -n "$uci_radios_check" ] && uci_count=$(echo "$uci_radios_check" | wc -w)
|
||||
fi
|
||||
|
||||
# E. 判定就绪标准:
|
||||
# 1) UCI 中有效 wifi-device 数量 > 0
|
||||
# 2) 若检测到物理 PHY,UCI radio 数量必须完全覆盖检测到的 PHY 数量
|
||||
# 就绪标准判定
|
||||
if [ $uci_count -gt 0 ]; then
|
||||
if [ $phy_count -eq 0 ] || [ $uci_count -ge $phy_count ]; then
|
||||
return 0
|
||||
@@ -122,20 +218,18 @@ wait_wifi_ready() {
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
|
||||
# 超时保护:只要至少有一个 radio 节点,允许降级继续
|
||||
if [ $uci_count -gt 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
[ $uci_count -gt 0 ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# 执行深度就绪等待
|
||||
if ! wait_wifi_ready; then
|
||||
echo "[ERROR] 未检测到可用无线硬件或无线配置生成超时!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. 识别射频频段函数 (2g, 5g, 6g)
|
||||
# -------------------------------------------------------------
|
||||
# 6. 频段识别函数 (2g, 5g, 6g)
|
||||
# -------------------------------------------------------------
|
||||
detect_band() {
|
||||
local radio="$1"
|
||||
local band="$(uci -q get wireless.${radio}.band 2>/dev/null | tr 'A-Z' 'a-z')"
|
||||
@@ -146,7 +240,6 @@ detect_band() {
|
||||
*6g*) echo "6g"; return ;;
|
||||
esac
|
||||
|
||||
# 信道 (channel) 推断
|
||||
local channel="$(uci -q get wireless.${radio}.channel 2>/dev/null)"
|
||||
if [ -n "$channel" ] && [ "$channel" -eq "$channel" 2>/dev/null ]; then
|
||||
if [ "$channel" -ge 1 ] && [ "$channel" -le 14 ]; then
|
||||
@@ -158,14 +251,12 @@ detect_band() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# hwmode 推断
|
||||
local hwmode="$(uci -q get wireless.${radio}.hwmode 2>/dev/null | tr 'A-Z' 'a-z')"
|
||||
case "$hwmode" in
|
||||
*a*) echo "5g"; return ;;
|
||||
*b*|*g*) echo "2g"; return ;;
|
||||
esac
|
||||
|
||||
# 双频常规配对推断
|
||||
if [ "$radio" = "radio0" ]; then
|
||||
local r1_band="$(uci -q get wireless.radio1.band 2>/dev/null | tr 'A-Z' 'a-z')"
|
||||
[ "$r1_band" = "5g" ] && echo "2g" && return
|
||||
@@ -177,33 +268,31 @@ detect_band() {
|
||||
echo "2g"
|
||||
}
|
||||
|
||||
# 4. 加载系统发行版信息(若存在)
|
||||
# -------------------------------------------------------------
|
||||
# 7. 配置 SSID 与各频段接口
|
||||
# -------------------------------------------------------------
|
||||
[ -f /etc/openwrt_release ] && . /etc/openwrt_release
|
||||
|
||||
# 5. 确定基础 WiFi 名称
|
||||
if [ -n "$CUSTOM_SSID" ]; then
|
||||
SSID="$CUSTOM_SSID"
|
||||
else
|
||||
SSID="$(uci -q get system.@system[0].hostname 2>/dev/null)"
|
||||
[ -z "$SSID" ] && [ -r /proc/sys/kernel/hostname ] && SSID="$(cat /proc/sys/kernel/hostname)"
|
||||
[ -z "$SSID" ] && SSID="Kwrt"
|
||||
[ -z "$SSID" ] && SSID="$(uci -q get wizard.default.wifi_ssid 2>/dev/null)"
|
||||
[ -z "$SSID" ] && SSID="OpenWrt"
|
||||
fi
|
||||
|
||||
# 移除禁用标记
|
||||
sed -i '/option disabled/d' /etc/config/wireless 2>/dev/null
|
||||
|
||||
# 修正双频标记
|
||||
if [ "$(uci -q get wireless.radio1.band 2>/dev/null)" = "5g" ]; then
|
||||
[ -z "$(uci -q get wireless.radio0.band 2>/dev/null)" ] && uci -q set wireless.radio0.band="2g"
|
||||
fi
|
||||
|
||||
# 重命名匿名接口 section 为 default_radioX
|
||||
widx=0
|
||||
while uci rename wireless.@wifi-iface[$widx]=default_radio$widx >/dev/null 2>&1; do
|
||||
widx=$((widx + 1))
|
||||
done
|
||||
|
||||
# 6. 配置各 Radio 参数
|
||||
wifi_setup_radio() {
|
||||
local radio="$1"
|
||||
|
||||
@@ -223,7 +312,6 @@ wifi_setup_radio() {
|
||||
|
||||
local full_ssid="${SSID}${ssid_suffix}"
|
||||
|
||||
# 特定硬件架构适配(如高通 ipq60xx)
|
||||
if [ "$band" = "5g" ]; then
|
||||
case "$DISTRIB_TARGET" in
|
||||
*ipq60xx*)
|
||||
@@ -240,10 +328,8 @@ wifi_setup_radio() {
|
||||
uci -q set wireless.${iface}.network='lan'
|
||||
uci -q set wireless.${iface}.ssid="${full_ssid}"
|
||||
|
||||
# 密码与加密策略配置
|
||||
if [ -n "$SSID_PASSWD" ]; then
|
||||
if [ "$band" = "6g" ]; then
|
||||
# 6GHz 频段按标准强制要求 WPA3-SAE
|
||||
uci -q set wireless.${iface}.encryption='sae'
|
||||
else
|
||||
uci -q set wireless.${iface}.encryption='psk2'
|
||||
@@ -257,7 +343,7 @@ wifi_setup_radio() {
|
||||
fi
|
||||
}
|
||||
|
||||
# 7. 动态遍历所有射频设备并应用设置
|
||||
# 动态遍历所有射频设备并配置
|
||||
radios="$(uci -q show wireless 2>/dev/null | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
|
||||
[ -z "$radios" ] && radios="radio0 radio1 radio2 radio3"
|
||||
|
||||
@@ -265,10 +351,10 @@ for radio in $radios; do
|
||||
wifi_setup_radio "$radio"
|
||||
done
|
||||
|
||||
# 提交配置
|
||||
# 提交无线配置
|
||||
uci commit wireless 2>/dev/null
|
||||
|
||||
# 若存在 base_config(kiddin9 固件状态记录),更新状态
|
||||
# 标记 WiFi 基础配置已完成
|
||||
if uci -q get base_config.@status[0] >/dev/null 2>&1; then
|
||||
uci -q set base_config.@status[0].wifi=1
|
||||
uci commit base_config 2>/dev/null
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NPM_NAME:=pnpm
|
||||
PKG_NAME:=node-$(PKG_NPM_NAME)
|
||||
PKG_VERSION:=11.15.1
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=11.22.0
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NPM_NAME)-$(PKG_VERSION).tgz
|
||||
PKG_SOURCE_URL:=https://mirrors.tencent.com/npm/$(PKG_NPM_NAME)/-/ \
|
||||
|
||||
+3
-3
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.6.7_p58
|
||||
PKG_RELEASE:=50
|
||||
PKG_VERSION:=1.6.7_p60
|
||||
PKG_RELEASE:=51
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=1b575d41dd35684e035541afe1f129da072275dd
|
||||
PKG_SOURCE_VERSION:=f5cba40880e0a11c00cf6b9040a6eb74e3d222da
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
+3
-2
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=xkcptun
|
||||
PKG_VERSION:=1.09.561
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
|
||||
@@ -34,7 +34,8 @@ endef
|
||||
|
||||
define Package/xkcptun/description
|
||||
xkcptun is a lightweight and high-performance C language implementation of kcptun.
|
||||
This package contains both client and server binaries.
|
||||
This package contains the client and server binaries as well as the
|
||||
xkcp_spy monitoring tool.
|
||||
endef
|
||||
|
||||
define Package/xkcptun/conffiles
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
config global 'client'
|
||||
option enabled '0'
|
||||
option remote_addr '1.2.3.4'
|
||||
option remote_port '9089'
|
||||
option mode 'fast3'
|
||||
@@ -19,10 +20,10 @@ config global 'client'
|
||||
option sockbuf '16777216'
|
||||
option keepalive '10'
|
||||
option conntimeout '60'
|
||||
option key '7f7ac683d720e1437019692eadb0b811'
|
||||
option key 'change_me'
|
||||
|
||||
config client 'client1'
|
||||
option disabled '1'
|
||||
option enabled '0'
|
||||
option name 'ssh_tunnel'
|
||||
option local_interface 'br-lan'
|
||||
option local_port '9088'
|
||||
@@ -50,4 +51,4 @@ config global 'server'
|
||||
option sockbuf '16777216'
|
||||
option keepalive '10'
|
||||
option conntimeout '60'
|
||||
option key '7f7ac683d720e1437019692eadb0b811'
|
||||
option key 'change_me'
|
||||
+19
-12
@@ -53,7 +53,7 @@ append_toml_tunnel() {
|
||||
|
||||
eval "$("validate_${cfgtype}_section" "$cfg" validate_mklocal)"
|
||||
"validate_${cfgtype}_section" "$cfg" || return 1
|
||||
[ "$disabled" = 0 ] || return 1
|
||||
[ "${enabled:-1}" -eq 1 ] || return 1
|
||||
[ -n "$local_port" ] || return 1
|
||||
|
||||
cat << EOF >> "$outfile"
|
||||
@@ -128,15 +128,19 @@ EOF
|
||||
|
||||
config_foreach append_section "$cfgtype"
|
||||
|
||||
if [ "$cfgtype" = "client" ] && [ "$count" -eq 0 ]; then
|
||||
rm -f "$conftoml"
|
||||
return
|
||||
if [ "$cfgtype" = "client" ]; then
|
||||
local cli_enabled
|
||||
config_get_bool cli_enabled client enabled 0
|
||||
if [ "$cli_enabled" -ne 1 ] || [ "$count" -eq 0 ]; then
|
||||
rm -f "$conftoml"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$cfgtype" = "server" ]; then
|
||||
local srv_enabled
|
||||
config_get_bool srv_enabled server enabled 0
|
||||
if [ "$srv_enabled" -ne 1 ] && [ "$count" -eq 0 ]; then
|
||||
if [ "$srv_enabled" -ne 1 ]; then
|
||||
rm -f "$conftoml"
|
||||
return
|
||||
fi
|
||||
@@ -151,6 +155,9 @@ EOF
|
||||
|
||||
start_service() {
|
||||
mkdir -p "$confdir"
|
||||
# generated TOML files contain the pre-shared key: keep them private
|
||||
chmod 0700 "$confdir"
|
||||
umask 077
|
||||
config_load xkcptun
|
||||
start_daemon server
|
||||
start_daemon client
|
||||
@@ -189,10 +196,11 @@ validate_common_options() {
|
||||
local cfgtype="$1"; shift
|
||||
local cfg="$1"; shift
|
||||
local func="$1"; shift
|
||||
local enabled_def="${1:-1}"; shift
|
||||
local mode_profiles='"fast3", "fast2", "fast", "normal", "manual"'
|
||||
|
||||
"${func:-validate}" "$cfgtype" "$cfg" "$@" \
|
||||
'disabled:bool:0' \
|
||||
"enabled:bool:$enabled_def" \
|
||||
'name:string' \
|
||||
"mode:or($mode_profiles)" \
|
||||
'mtu:uinteger' \
|
||||
@@ -217,8 +225,7 @@ validate_common_options() {
|
||||
}
|
||||
|
||||
validate_global_options() {
|
||||
validate_common_options global "$1" "$2" \
|
||||
'enabled:bool:0' \
|
||||
validate_common_options global "$1" "$2" 0 \
|
||||
'remote_addr:host' \
|
||||
'remote_port:port' \
|
||||
'local_interface:string' \
|
||||
@@ -226,15 +233,15 @@ validate_global_options() {
|
||||
}
|
||||
|
||||
validate_server_options() {
|
||||
validate_common_options server "$1" "$2" \
|
||||
validate_common_options server "$1" "$2" 1 \
|
||||
'local_interface:string:eth0' \
|
||||
'local_port:port:9089' \
|
||||
'remote_addr:host:127.0.0.1' \
|
||||
'remote_port:port:443'
|
||||
'target_addr:host' \
|
||||
'target_port:port'
|
||||
}
|
||||
|
||||
validate_client_options() {
|
||||
validate_common_options client "$1" "$2" \
|
||||
validate_common_options client "$1" "$2" 1 \
|
||||
'local_interface:string:br-lan' \
|
||||
'local_port:port:9088' \
|
||||
'proto:or("tcp", "udp")' \
|
||||
|
||||
Reference in new issue
Block a user