Compare commits

...
5 Commits
Author SHA1 Message Date
github-actions[bot] 958b9c78f8 🎄 Sync 2026-09-21 23:56:44 2026-09-21 23:56:44 +08:00
github-actions[bot] 3f0bfdec09 💐 Sync 2026-09-21 23:46:31 2026-09-21 23:46:31 +08:00
github-actions[bot] 9dc0dfd5bb 🗽 Sync 2026-09-21 23:08:01 2026-09-21 23:08:01 +08:00
github-actions[bot] fc7323d2bd 🎨 Sync 2026-09-21 22:47:18 2026-09-21 22:47:18 +08:00
github-actions[bot] 385706139a 🦄 Sync 2026-09-21 17:33:30 2026-09-21 17:33:31 +08:00
27 changed files with 919 additions and 378 deletions

No files matched your search

+3 -3
View File
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
AGENTFLOW_ARCH_aarch64:=arm64
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
AGENTFLOW_HASH_x86_64:=478fdb17cfb2b1664e7bc01315186e71f228871c0d20c816d8b3b8fe789bdb4d
AGENTFLOW_HASH_aarch64:=257faeaad2445dd14f87c1e1d9ded869fd301c5df2c2c9af817e70303eab2512
AGENTFLOW_HASH_x86_64:=9174fec77dc835640f94ea8bef84b580b84f947823f4dd63fc495c4c58a68297
AGENTFLOW_HASH_aarch64:=256adf7bcb836259dacbf583c52af04f0134587328689a49811c135260fa5aed
PKG_NAME:=agentflow
PKG_VERSION:=0.3.3
PKG_RELEASE:=12
PKG_RELEASE:=13
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
+2 -2
View File
@@ -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
View File
@@ -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
+12 -7
View File
@@ -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
View File
@@ -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
+12 -7
View File
@@ -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
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
PKG_VERSION:=1.0.1
PKG_RELEASE:=6
PKG_RELEASE:=7
LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
@@ -5,6 +5,13 @@ module("luci.controller.agentflow", package.seeall)
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
local DEFAULT_BASE_PATH = "/apps/agentflow/"
local DEFAULT_PORT = 9000
local AGENTS = {
{ id = "codexcli", package = "@openai/codex" },
{ id = "claude-code", package = "@anthropic-ai/claude-code" },
{ id = "opencode", package = "opencode-ai" },
{ id = "kimi", package = "@moonshot-ai/kimi-code" },
{ id = "reasonix", package = "reasonix" }
}
function index()
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
@@ -133,10 +140,54 @@ local function agentflow_entry_url()
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
end
local function node_modules_root()
local sys = require "luci.sys"
local util = require "luci.util"
local script = table.concat({
'. /lib/functions/mise.sh 2>/dev/null || exit 1',
'istore_runtime_env >/dev/null 2>&1 || exit 1',
'mise_bin="$(command -v mise-istore || command -v mise)"',
'[ -n "$mise_bin" ] || exit 1',
'node_dir="$("$mise_bin" where node@lts 2>/dev/null)"',
'[ -n "$node_dir" ] || exit 1',
'printf "%s/lib/node_modules" "$node_dir"'
}, "; ")
local root = sys.exec("/bin/sh -c " .. util.shellquote(script)) or ""
root = root:match("^%s*(.-)%s*$")
if root == "" or root:sub(1, 1) ~= "/" then
return nil
end
return root
end
local function agent_statuses()
local fs = require "nixio.fs"
local jsonc = require "luci.jsonc"
local root = node_modules_root()
local statuses = {}
for _, agent in ipairs(AGENTS) do
local status = { id = agent.id, installed = false }
if root then
local package_file = root .. "/" .. agent.package .. "/package.json"
local package_data = fs.readfile(package_file)
local package_json = package_data and jsonc.parse(package_data) or nil
if type(package_json) == "table" then
status.installed = true
status.version = package_json.version
end
end
statuses[#statuses + 1] = status
end
return statuses, root ~= nil
end
function agentflow_status()
local sys = require "luci.sys"
local port, base_path = agentflow_config()
local entry_url = agentflow_entry_url()
local agents, agents_available = agent_statuses()
local status = {
running = (sys.call("pidof agentflow >/dev/null") == 0),
@@ -145,7 +196,9 @@ function agentflow_status()
entry_url = entry_url,
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
linkeasefull_running = linkeasefull_running()
linkeasefull_running = linkeasefull_running(),
agents_available = agents_available,
agents = agents
}
write_json(status)
end
@@ -158,20 +211,17 @@ function agentflow_agent_install()
local task_id = "agentflow-agent-install"
local task_script = "/tmp/agentflow-agent-install.sh"
local installer_url = "https://fw.koolcenter.com/binary/geili/agentflow/releases/installapp/installapp-mise.sh"
local agents = {
codexcli = true,
["claude-code"] = true,
opencode = true,
kimi = true,
reasonix = true
}
local supported_agents = {}
for _, supported_agent in ipairs(AGENTS) do
supported_agents[supported_agent.id] = true
end
if not require_post_csrf() then
return
end
local agent = http.formvalue("agent") or ""
if not agents[agent] then
if not supported_agents[agent] then
write_json({ ok = false, error = "unsupported agent" })
return
end
@@ -1,58 +1,378 @@
<%+tasks/embed%>
<style type="text/css">
.agentflow-actions { display:flex; gap:10px; flex-wrap:wrap; margin:12px; }
.agentflow-actions .btn, .agentflow-agent-dialog-actions .btn { min-height:40px; }
.agentflow-agent-modal { position:fixed; inset:0; z-index:10000; display:none; align-items:center; justify-content:center; padding:20px; box-sizing:border-box; background:rgba(15,23,42,.45); }
.agentflow-shell {
--af-panel:#fff;
--af-panel-soft:#f6f8fc;
--af-border:#dfe5ef;
--af-text:#17213d;
--af-muted:#667397;
--af-primary:#356cf6;
--af-primary-hover:#2559d8;
--af-success:#159447;
--af-success-soft:#e9f8ef;
--af-shadow:0 8px 24px rgba(46, 67, 112, .08);
color:var(--af-text);
margin:12px 0 20px;
}
.agentflow-panel {
background:var(--af-panel);
border:1px solid var(--af-border);
border-radius:10px;
box-shadow:var(--af-shadow);
box-sizing:border-box;
}
.agentflow-overview {
display:grid;
grid-template-columns:minmax(260px, .9fr) minmax(320px, 1.1fr);
gap:28px;
padding:24px;
}
.agentflow-service {
display:flex;
align-items:flex-start;
gap:16px;
}
.agentflow-service-icon {
display:flex;
align-items:center;
justify-content:center;
width:54px;
height:54px;
flex:0 0 54px;
border-radius:50%;
background:#eef1f6;
color:var(--af-muted);
font-size:28px;
font-weight:700;
}
.agentflow-service-icon.running {
background:var(--af-success-soft);
color:var(--af-success);
}
.agentflow-service-copy { min-width:0; }
.agentflow-service-copy h3 {
margin:2px 0 6px;
color:var(--af-text);
font-size:20px;
line-height:1.3;
}
.agentflow-service-copy p {
margin:0 0 16px;
color:var(--af-muted);
line-height:1.6;
}
.agentflow-access {
padding-left:28px;
border-left:1px solid var(--af-border);
}
.agentflow-access label {
display:block;
margin:0 0 9px;
font-weight:700;
}
.agentflow-address-row {
display:flex;
gap:8px;
}
.agentflow-address-row input[type="text"] {
min-width:0;
flex:1 1 auto;
height:40px;
box-sizing:border-box;
border:1px solid var(--af-border);
border-radius:7px;
background:var(--af-panel-soft);
color:var(--af-text);
font-variant-numeric:tabular-nums;
}
.agentflow-access-note {
min-height:20px;
margin:10px 0 0;
color:var(--af-muted);
font-size:13px;
}
.agentflow-button {
min-height:40px;
box-sizing:border-box;
border-radius:7px !important;
white-space:nowrap;
}
.agentflow-primary {
border-color:var(--af-primary) !important;
background:var(--af-primary) !important;
color:#fff !important;
}
.agentflow-primary:hover { background:var(--af-primary-hover) !important; }
.agentflow-button:disabled { cursor:not-allowed; opacity:.55; }
.agentflow-agents { margin-top:16px; padding:22px 20px 20px; }
.agentflow-section-heading {
display:flex;
align-items:flex-start;
gap:12px;
margin-bottom:18px;
}
.agentflow-section-mark {
display:flex;
align-items:center;
justify-content:center;
width:38px;
height:38px;
flex:0 0 38px;
border-radius:9px;
background:#edf3ff;
color:var(--af-primary);
font-size:17px;
font-weight:800;
}
.agentflow-section-heading h3 { margin:0 0 4px; color:var(--af-text); font-size:20px; }
.agentflow-section-heading p { margin:0; color:var(--af-muted); line-height:1.55; }
.agentflow-agent-grid {
display:grid;
grid-template-columns:repeat(3, minmax(0, 1fr));
gap:14px;
}
.agentflow-agent-card {
display:flex;
min-width:0;
min-height:190px;
padding:16px;
box-sizing:border-box;
flex-direction:column;
border:1px solid var(--af-border);
border-radius:9px;
background:var(--af-panel);
}
.agentflow-agent-card[data-state="installed"] { border-color:#b8dfc6; }
.agentflow-agent-top { display:flex; align-items:flex-start; gap:12px; }
.agentflow-agent-logo {
display:flex;
align-items:center;
justify-content:center;
width:48px;
height:48px;
flex:0 0 48px;
border-radius:9px;
background:var(--agent-color, var(--af-primary));
color:#fff;
font-size:21px;
font-weight:800;
}
.agentflow-agent-copy { min-width:0; }
.agentflow-agent-title { margin:2px 0 4px; color:var(--af-text); font-size:17px; font-weight:700; line-height:1.4; }
.agentflow-agent-copy p { margin:0; color:var(--af-muted); font-size:13px; line-height:1.45; }
.agentflow-agent-meta {
display:flex;
align-items:center;
gap:10px;
margin:17px 0 12px;
color:var(--af-muted);
font-size:13px;
}
.agentflow-agent-state { display:inline-flex; align-items:center; gap:7px; font-weight:700; }
.agentflow-agent-dot { width:8px; height:8px; border-radius:50%; background:#8a95af; }
.agentflow-agent-card[data-state="installed"] .agentflow-agent-state { color:var(--af-success); }
.agentflow-agent-card[data-state="installed"] .agentflow-agent-dot { background:var(--af-success); box-shadow:0 0 0 4px var(--af-success-soft); }
.agentflow-agent-divider { color:#b4bdd0; }
.agentflow-install-button { width:100%; margin-top:auto; }
.agentflow-agent-card[data-state="installed"] .agentflow-install-button {
border-color:var(--af-success) !important;
background:var(--af-success-soft) !important;
color:var(--af-success) !important;
}
.agentflow-agent-modal {
position:fixed;
inset:0;
z-index:10000;
display:none;
align-items:center;
justify-content:center;
padding:20px;
box-sizing:border-box;
background:rgba(15, 23, 42, .48);
}
.agentflow-agent-modal.open { display:flex; }
.agentflow-agent-dialog, body[theme="light"] .agentflow-agent-dialog { width:min(520px, 100%); padding:20px; box-sizing:border-box; border:1px solid #d0d7de; border-radius:12px; background:#fff; color:inherit; box-shadow:0 18px 48px rgba(15,23,42,.24); }
.agentflow-agent-dialog h3 { margin:0 0 8px; text-wrap:balance; }
.agentflow-agent-dialog > p, body[theme="light"] .agentflow-agent-dialog > p { margin:0 0 16px; color:#666; text-wrap:pretty; }
.agentflow-agent-field label { display:block; margin-bottom:6px; font-weight:600; }
.agentflow-agent-field select { width:100%; min-height:40px; box-sizing:border-box; }
.agentflow-agent-dialog-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:18px; }
body[theme="dark"] .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
body[theme="dark"] .agentflow-agent-dialog > p { color:#aeb8c7; }
.agentflow-agent-dialog {
width:100%;
max-width:460px;
padding:22px;
box-sizing:border-box;
border:1px solid var(--af-border);
border-radius:10px;
background:var(--af-panel);
color:var(--af-text);
box-shadow:0 18px 48px rgba(15, 23, 42, .24);
}
.agentflow-agent-dialog h3 { margin:0 0 8px; color:var(--af-text); }
.agentflow-agent-dialog p { margin:0; color:var(--af-muted); line-height:1.6; }
.agentflow-agent-dialog strong { color:var(--af-text); }
.agentflow-dialog-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:20px; }
body[theme="dark"] .agentflow-shell {
--af-panel:#202733;
--af-panel-soft:#171d27;
--af-border:#3b4658;
--af-text:#edf2fb;
--af-muted:#adb8cb;
--af-primary:#5d88ff;
--af-primary-hover:#7197ff;
--af-success:#50c878;
--af-success-soft:rgba(80, 200, 120, .14);
--af-shadow:0 8px 24px rgba(0, 0, 0, .18);
}
body[theme="dark"] .agentflow-section-mark { background:rgba(93, 136, 255, .14); }
@media (prefers-color-scheme: dark) {
body:not([theme]) .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
body:not([theme]) .agentflow-agent-dialog > p { color:#aeb8c7; }
body:not([theme]) .agentflow-shell {
--af-panel:#202733;
--af-panel-soft:#171d27;
--af-border:#3b4658;
--af-text:#edf2fb;
--af-muted:#adb8cb;
--af-primary:#5d88ff;
--af-primary-hover:#7197ff;
--af-success:#50c878;
--af-success-soft:rgba(80, 200, 120, .14);
--af-shadow:0 8px 24px rgba(0, 0, 0, .18);
}
body:not([theme]) .agentflow-section-mark { background:rgba(93, 136, 255, .14); }
}
@media (max-width:1000px) {
.agentflow-agent-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); }
}
@media (max-width:760px) {
.agentflow-overview { grid-template-columns:1fr; gap:20px; padding:18px; }
.agentflow-access { padding:18px 0 0; border-top:1px solid var(--af-border); border-left:0; }
.agentflow-agent-grid { grid-template-columns:1fr; }
.agentflow-agents { padding:18px 14px 14px; }
.agentflow-address-row { flex-wrap:wrap; }
.agentflow-address-row input[type="text"] { flex-basis:100%; }
.agentflow-address-row .agentflow-button { flex:1 1 auto; }
}
</style>
<script type="text/javascript">//<![CDATA[
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
var agentflowStatusUrl = '<%=url("admin/services/agentflow_status")%>';
var agentflowAgentInstallUrl = '<%=url("admin/services/agentflow/agent_install")%>';
var agentflowCsrfToken = (window.taskd && window.taskd.csrfToken) || '';
var agentflowSelectedAgent = '';
var agentflowAgentNames = {
codexcli: 'Codex',
'claude-code': 'Claude Code',
opencode: 'OpenCode',
kimi: 'Kimi',
reasonix: 'Reasonix'
};
var agentflowLabels = {
installed: '<%:Installed%>',
notInstalled: '<%:Not installed%>',
unknown: '<%:Status unavailable%>',
install: '<%:Install%>',
starting: '<%:Starting...%>',
copied: '<%:Copied%>'
};
function agentflowAgentDialog(open) {
function agentflowAbsoluteUrl(value) {
try { return new URL(value, window.location.href).href; }
catch (e) { return value || ''; }
}
function agentflowRenderStatus(st) {
var icon = document.getElementById('agentflow-service-icon');
var title = document.getElementById('agentflow-service-title');
var description = document.getElementById('agentflow-service-description');
var openButton = document.getElementById('agentflow-open');
var address = document.getElementById('agentflow-address');
var statuses = {};
var agents = st && st.agents || [];
var i;
if (!st) return;
if (icon) {
icon.className = 'agentflow-service-icon' + (st.running ? ' running' : '');
icon.textContent = st.running ? '\u2713' : '!';
}
if (title) title.textContent = st.running ? '<%:AgentFlow service is running%>' : '<%:AgentFlow service is not running%>';
if (description) description.textContent = st.running
? '<%:The service is ready. You can open AgentFlow or install coding agents.%>'
: '<%:Start the service to use AgentFlow. Agent installation remains available.%>';
if (openButton) openButton.disabled = !st.running;
if (address) address.value = agentflowAbsoluteUrl(st.entry_url || '');
for (i = 0; i < agents.length; i++) statuses[agents[i].id] = agents[i];
for (i in agentflowAgentNames) {
if (!agentflowAgentNames.hasOwnProperty(i)) continue;
var card = document.getElementById('agentflow-agent-' + i);
var state = document.getElementById('agentflow-agent-state-' + i);
var version = document.getElementById('agentflow-agent-version-' + i);
var installButton = document.getElementById('agentflow-agent-install-' + i);
var item = statuses[i];
var available = st.agents_available !== false && !!item;
var installed = available && item.installed === true;
if (card) card.setAttribute('data-state', installed ? 'installed' : (available ? 'available' : 'unknown'));
if (state) state.textContent = installed ? agentflowLabels.installed : (available ? agentflowLabels.notInstalled : agentflowLabels.unknown);
if (version) version.textContent = installed && item.version ? item.version : '-';
if (installButton) {
installButton.disabled = !available || installed;
installButton.value = installed ? agentflowLabels.installed : agentflowLabels.install;
}
}
}
function agentflowCopyAddress() {
var address = document.getElementById('agentflow-address');
var feedback = document.getElementById('agentflow-copy-feedback');
if (!address || !address.value) return;
function done() {
if (!feedback) return;
feedback.textContent = agentflowLabels.copied;
window.setTimeout(function() { feedback.textContent = ''; }, 1800);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(address.value).then(done, function() {
address.select();
document.execCommand('copy');
done();
});
} else {
address.select();
document.execCommand('copy');
done();
}
}
function agentflowAgentDialog(open, agent) {
var modal = document.getElementById('agentflow-agent-modal');
var select = document.getElementById('agentflow-agent-select');
var trigger = document.getElementById('agentflow-agent-open');
var name = document.getElementById('agentflow-selected-agent-name');
var confirmButton = document.getElementById('agentflow-agent-install-now');
if (!modal) return;
if (open) {
agentflowSelectedAgent = agent || '';
if (name) name.textContent = agentflowAgentNames[agentflowSelectedAgent] || agentflowSelectedAgent;
modal.classList.add('open');
if (select) select.focus();
if (confirmButton) confirmButton.focus();
} else {
modal.classList.remove('open');
var trigger = document.getElementById('agentflow-agent-install-' + agentflowSelectedAgent);
if (trigger) trigger.focus();
}
}
function agentflowInstallAgent() {
var selected = document.getElementById('agentflow-agent-select');
var button = document.getElementById('agentflow-agent-install-now');
if (!selected || !button) return;
if (!agentflowSelectedAgent || !button) return;
if (!window.taskd || !window.taskd.show_log) {
alert('<%:Task log window is not available.%>');
return;
}
button.disabled = true;
button.value = agentflowLabels.starting;
(new XHR()).post(agentflowAgentInstallUrl, {
agent: selected.value,
agent: agentflowSelectedAgent,
token: agentflowCsrfToken
}, function(x, data) {
button.disabled = false;
button.value = '<%:Install now%>';
if (!data) {
try { data = JSON.parse(x.responseText || '{}'); }
catch (e) { data = {}; }
@@ -66,25 +386,8 @@ function agentflowInstallAgent() {
});
}
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
var el = document.getElementById('agentflow_status');
var openButton = document.getElementById('agentflow-open');
if (st && el) {
if (!st.running) {
if (openButton) openButton.style.display = 'none';
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
} else {
if (openButton) openButton.style.display = '';
var accessMode = '';
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
} else {
accessMode = '<br/><em><%:Access mode: external port.%></em>';
}
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ accessMode;
}
}
XHR.poll(5, agentflowStatusUrl, null, function(x, st) {
agentflowRenderStatus(st);
});
document.addEventListener('keydown', function(ev) {
@@ -92,34 +395,71 @@ document.addEventListener('keydown', function(ev) {
});
//]]></script>
<fieldset class="cbi-section">
<legend><%:Status%></legend>
<p id="agentflow_status">
<em><%:Collecting data...%></em>
</p>
<div class="agentflow-actions">
<input id="agentflow-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Click to open AgentFlow%>" onclick="window.open(agentflowOpenUrl)" style="display:none">
<input id="agentflow-agent-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install agent%>" onclick="agentflowAgentDialog(true)">
</div>
</fieldset>
<div id="agentflow-agent-modal" class="agentflow-agent-modal" role="dialog" aria-modal="true" aria-labelledby="agentflow-agent-title" onclick="if (event.target === this) agentflowAgentDialog(false)">
<div class="agentflow-agent-dialog">
<h3 id="agentflow-agent-title"><%:Install agent%></h3>
<p><%:Select a coding agent to install into the shared AgentFlow runtime.%></p>
<div class="agentflow-agent-field">
<label for="agentflow-agent-select"><%:Agent%></label>
<select id="agentflow-agent-select" class="cbi-input-select">
<option value="codexcli">Codex</option>
<option value="claude-code">Claude Code</option>
<option value="opencode">OpenCode</option>
<option value="kimi">Kimi</option>
<option value="reasonix">Reasonix</option>
</select>
<div class="agentflow-shell">
<section class="agentflow-panel agentflow-overview" aria-labelledby="agentflow-service-title">
<div class="agentflow-service">
<div id="agentflow-service-icon" class="agentflow-service-icon" aria-hidden="true">...</div>
<div class="agentflow-service-copy">
<h3 id="agentflow-service-title"><%:Checking AgentFlow service%></h3>
<p id="agentflow-service-description"><%:Collecting data...%></p>
<input id="agentflow-open" class="btn cbi-button agentflow-button agentflow-primary" type="button" value="<%:Open AgentFlow%>" onclick="window.open(agentflowOpenUrl)" disabled>
</div>
</div>
<div class="agentflow-agent-dialog-actions">
<input class="btn cbi-button" type="button" value="<%:Cancel%>" onclick="agentflowAgentDialog(false)">
<input id="agentflow-agent-install-now" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install now%>" onclick="agentflowInstallAgent()">
<div class="agentflow-access">
<label for="agentflow-address"><%:Local network access%></label>
<div class="agentflow-address-row">
<input id="agentflow-address" type="text" readonly value="">
<input class="btn cbi-button agentflow-button" type="button" value="<%:Copy%>" onclick="agentflowCopyAddress()">
</div>
<p class="agentflow-access-note"><%:Open this address in a browser to access the AgentFlow Web interface.%> <span id="agentflow-copy-feedback" role="status" aria-live="polite"></span></p>
</div>
</section>
<section class="agentflow-panel agentflow-agents" aria-labelledby="agentflow-agents-title">
<div class="agentflow-section-heading">
<div class="agentflow-section-mark" aria-hidden="true">A</div>
<div>
<h3 id="agentflow-agents-title"><%:Agent installation%></h3>
<p><%:Install coding agents into the shared AgentFlow runtime. Installed agents show their current version.%></p>
</div>
</div>
<div class="agentflow-agent-grid">
<div id="agentflow-agent-codexcli" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#356cf6">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">C</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Codex</div><p><%:OpenAI coding agent for software development tasks.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-codexcli"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-codexcli">-</span></span></div>
<input id="agentflow-agent-install-codexcli" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'codexcli')" disabled>
</div>
<div id="agentflow-agent-claude-code" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#dc633d">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">C</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Claude Code</div><p><%:Anthropic coding agent for code analysis and development.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-claude-code"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-claude-code">-</span></span></div>
<input id="agentflow-agent-install-claude-code" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'claude-code')" disabled>
</div>
<div id="agentflow-agent-opencode" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#6a3ee8">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">O</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">OpenCode</div><p><%:Open source coding agent with a flexible terminal workflow.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-opencode"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-opencode">-</span></span></div>
<input id="agentflow-agent-install-opencode" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'opencode')" disabled>
</div>
<div id="agentflow-agent-kimi" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#1677d2">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">K</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Kimi</div><p><%:Moonshot AI coding agent for terminal-based development.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-kimi"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-kimi">-</span></span></div>
<input id="agentflow-agent-install-kimi" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'kimi')" disabled>
</div>
<div id="agentflow-agent-reasonix" class="agentflow-agent-card" data-state="unknown" style="--agent-color:#3f526f">
<div class="agentflow-agent-top"><div class="agentflow-agent-logo" aria-hidden="true">R</div><div class="agentflow-agent-copy"><div class="agentflow-agent-title">Reasonix</div><p><%:AI coding agent for software engineering workflows.%></p></div></div>
<div class="agentflow-agent-meta"><span class="agentflow-agent-state"><span class="agentflow-agent-dot"></span><span id="agentflow-agent-state-reasonix"><%:Checking...%></span></span><span class="agentflow-agent-divider">|</span><span><%:Version%>: <span id="agentflow-agent-version-reasonix">-</span></span></div>
<input id="agentflow-agent-install-reasonix" class="btn cbi-button agentflow-button agentflow-primary agentflow-install-button" type="button" value="<%:Install%>" onclick="agentflowAgentDialog(true, 'reasonix')" disabled>
</div>
</div>
</section>
<div id="agentflow-agent-modal" class="agentflow-agent-modal" role="dialog" aria-modal="true" aria-labelledby="agentflow-agent-dialog-title" onclick="if (event.target === this) agentflowAgentDialog(false)">
<div class="agentflow-agent-dialog">
<h3 id="agentflow-agent-dialog-title"><%:Confirm agent installation%></h3>
<p><%:The installer will download and install%> <strong id="agentflow-selected-agent-name"></strong> <%:into the shared AgentFlow runtime.%></p>
<div class="agentflow-dialog-actions">
<input class="btn cbi-button agentflow-button" type="button" value="<%:Cancel%>" onclick="agentflowAgentDialog(false)">
<input id="agentflow-agent-install-now" class="btn cbi-button agentflow-button agentflow-primary" type="button" value="<%:Install now%>" onclick="agentflowInstallAgent()">
</div>
</div>
</div>
</div>
+81
View File
@@ -72,3 +72,84 @@ msgstr "任务日志窗口不可用。"
msgid "Failed to start agent installation."
msgstr "启动智能体安装失败。"
msgid "Installed"
msgstr "已安装"
msgid "Not installed"
msgstr "未安装"
msgid "Status unavailable"
msgstr "状态不可用"
msgid "Install"
msgstr "安装"
msgid "Starting..."
msgstr "正在启动……"
msgid "Copied"
msgstr "已复制"
msgid "Copy"
msgstr "复制"
msgid "AgentFlow service is running"
msgstr "AgentFlow 服务运行中"
msgid "AgentFlow service is not running"
msgstr "AgentFlow 服务未运行"
msgid "The service is ready. You can open AgentFlow or install coding agents."
msgstr "服务已就绪,可以打开 AgentFlow 或安装编码智能体。"
msgid "Start the service to use AgentFlow. Agent installation remains available."
msgstr "请启动服务后使用 AgentFlow;智能体安装仍然可用。"
msgid "Checking AgentFlow service"
msgstr "正在检查 AgentFlow 服务"
msgid "Open AgentFlow"
msgstr "打开 AgentFlow"
msgid "Local network access"
msgstr "局域网访问"
msgid "Open this address in a browser to access the AgentFlow Web interface."
msgstr "在浏览器中打开此地址即可访问 AgentFlow Web 界面。"
msgid "Agent installation"
msgstr "智能体安装"
msgid "Install coding agents into the shared AgentFlow runtime. Installed agents show their current version."
msgstr "将编码智能体安装到 AgentFlow 共享运行环境中;已安装的智能体会显示当前版本。"
msgid "OpenAI coding agent for software development tasks."
msgstr "OpenAI 提供的代码智能体,适用于软件开发任务。"
msgid "Anthropic coding agent for code analysis and development."
msgstr "Anthropic 提供的代码智能体,适用于代码分析与开发。"
msgid "Open source coding agent with a flexible terminal workflow."
msgstr "开源代码智能体,提供灵活的终端开发工作流。"
msgid "Moonshot AI coding agent for terminal-based development."
msgstr "Moonshot AI 提供的代码智能体,适用于终端开发。"
msgid "AI coding agent for software engineering workflows."
msgstr "面向软件工程工作流的 AI 代码智能体。"
msgid "Checking..."
msgstr "正在检查……"
msgid "Version"
msgstr "版本"
msgid "Confirm agent installation"
msgstr "确认安装智能体"
msgid "The installer will download and install"
msgstr "安装程序将下载并安装"
msgid "into the shared AgentFlow runtime."
msgstr "到 AgentFlow 共享运行环境。"
@@ -16,37 +16,57 @@ class AgentInstallContractTest(unittest.TestCase):
self.assertIn("+luci-lib-taskd", makefile)
self.assertIn('<%+tasks/embed%>', status)
self.assertIn('<select id="agentflow-agent-select"', status)
for agent in ("codexcli", "claude-code", "opencode", "kimi", "reasonix"):
self.assertIn(f'<option value="{agent}">', status)
self.assertNotIn('type="radio"', status)
self.assertIn(f'id="agentflow-agent-{agent}"', status)
self.assertIn(f"agentflowAgentDialog(true, '{agent}')", status)
self.assertNotIn('<select id="agentflow-agent-select"', status)
self.assertNotIn("<h4>", status)
self.assertEqual(status.count('class="agentflow-agent-title"'), 5)
self.assertNotIn("Update", status)
self.assertNotIn("Uninstall", status)
self.assertIn('id="agentflow-open"', status)
self.assertIn('id="agentflow-agent-open"', status)
self.assertGreater(status.index('id="agentflow-agent-open"'), status.index('id="agentflow_status"'))
self.assertIn('margin:12px;', status)
self.assertIn("var selected = document.getElementById('agentflow-agent-select')", status)
self.assertIn('id="agentflow-agent-install-now"', status)
self.assertIn("agentflowSelectedAgent", status)
self.assertIn("window.taskd.show_log(data.task_id", status)
self.assertNotIn('local token =', status)
self.assertIn("window.taskd.csrfToken", status)
self.assertIn("token: agentflowCsrfToken", status)
self.assertIn('http.formvalue("agent")', controller)
self.assertIn('codexcli = true', controller)
self.assertIn('["claude-code"] = true', controller)
self.assertIn('opencode = true', controller)
self.assertIn('kimi = true', controller)
self.assertIn('reasonix = true', controller)
for agent in ("codexcli", "claude-code", "opencode", "kimi", "reasonix"):
self.assertIn(f'id = "{agent}"', controller)
self.assertIn('supported_agents[supported_agent.id] = true', controller)
self.assertIn('context.authtoken or context.token', controller)
self.assertIn('http.formvalue("token") ~= expected', controller)
self.assertIn('/etc/init.d/tasks task_add ', controller)
self.assertIn('task_id = "agentflow-agent-install"', controller)
def test_status_reports_installed_agents_from_shared_mise_runtime(self):
controller = self.read("luasrc/controller/agentflow.lua")
status = self.read("luasrc/view/agentflow/status.htm")
self.assertIn("istore_runtime_env", controller)
self.assertIn('where node@lts', controller)
for package in (
"@openai/codex",
"@anthropic-ai/claude-code",
"opencode-ai",
"@moonshot-ai/kimi-code",
"reasonix",
):
self.assertIn(f'package = "{package}"', controller)
self.assertIn('status.version = package_json.version', controller)
self.assertIn('agents_available = agents_available', controller)
self.assertIn('st.agents_available', status)
self.assertIn('item.installed === true', status)
self.assertIn('installButton.disabled = !available || installed', status)
self.assertIn('installed ? agentflowLabels.installed : agentflowLabels.install', status)
def test_modal_theme_prefers_body_attribute_then_system_theme(self):
status = self.read("luasrc/view/agentflow/status.htm")
self.assertIn('body[theme="light"] .agentflow-agent-dialog', status)
self.assertIn('body[theme="dark"] .agentflow-agent-dialog', status)
self.assertIn('body[theme="dark"] .agentflow-shell', status)
self.assertIn('@media (prefers-color-scheme: dark)', status)
self.assertIn('body:not([theme]) .agentflow-agent-dialog', status)
self.assertIn('body:not([theme]) .agentflow-shell', status)
def test_controller_runs_remote_installer_inside_taskd(self):
controller = self.read("luasrc/controller/agentflow.lua")
+29 -6
View File
@@ -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'],
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.16
PKG_RELEASE:=285
PKG_RELEASE:=286
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -1004,6 +1004,7 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'method', method || "");
opt.set(dom_prefix + 'ss_method', method || "");
opt.set(dom_prefix + 'tcp_fast_open', queryParam.tfo);
if (plugin && plugin != "none") {
plugin = (plugin === "simple-obfs") ? "obfs-local" : plugin;
opt.set(dom_prefix + 'plugin_enabled', true);
@@ -1026,9 +1027,29 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'tls_allowInsecure', true);
}
}
//部分订阅 ShadowTLS 采用 SIP003
if (plugin === "shadow-tls") {
pluginOpts = pluginOpts.replace(/(^|;)password=/, '$1passwd=');
pluginOpts = pluginOpts.replace(/(^|;)version=([123])(?=;|$)/, '$1v$2=1');
if (dom_prefix === "ssrust_") {
opt.set(dom_prefix + 'plugin_opts', pluginOpts || "");
} else if (dom_prefix === "singbox_") {
const opts = Object.fromEntries(pluginOpts.split(';').map(item => item.split('=', 2)));
const host = opts.host;
const password = opts.passwd;
const versionKey = Object.keys(opts).find(key => /^v[123]$/.test(key));
const version = versionKey ? versionKey.slice(1) : "1";
opt.set(dom_prefix + 'shadowtls', true);
opt.set(dom_prefix + 'shadowtls_version', version);
opt.set(dom_prefix + 'shadowtls_password', password || "");
opt.set(dom_prefix + 'shadowtls_serverName', host || "");
opt.set(dom_prefix + 'plugin_enabled', false);
}
}
} else {
opt.set(dom_prefix + 'plugin', "none");
}
if (param !== undefined) {
opt.set('remarks', decodeURIComponent(param));
}
@@ -1129,7 +1150,7 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'finalmask', queryParam.fm || "");
if (queryParam["shadow-tls"]) {
//解析SS Shadow-TLS 插件参数
//解析SS Shadow-TLS 专用参数
const parseShadowTLSParams = function(base64Str, outObj) {
try {
let obj = JSON.parse(b64decsafe(base64Str));
@@ -749,6 +749,11 @@ local function processData(szType, content, add_mode, group, sub_cfg)
if idx_pn then
result.plugin = plugin_info:sub(1, idx_pn - 1)
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
-- 部分订阅 ShadowTLS 采用 SIP003
result.plugin_opts = result.plugin_opts:gsub("^password=", "passwd=")
result.plugin_opts = result.plugin_opts:gsub(";password=", ";passwd=")
result.plugin_opts = result.plugin_opts:gsub("^version=([123])", "v%1=1")
result.plugin_opts = result.plugin_opts:gsub(";version=([123])", ";v%1=1")
else
result.plugin = plugin_info
end
@@ -855,10 +860,22 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.plugin_opts = nil
end
elseif result.type == 'sing-box' then
if result.plugin ~= "obfs-local" and result.plugin ~= "v2ray-plugin" then
if result.plugin ~= "obfs-local" and result.plugin ~= "v2ray-plugin" and result.plugin ~= "shadow-tls" then
result.error_msg = "Sing-Box 不支持 SS " .. result.plugin .. " 插件。"
else
result.plugin_enabled = "1"
-- 部分订阅 ShadowTLS 采用 SIP003
if result.plugin == "shadow-tls" then
for item in result.plugin_opts:gmatch("[^;]+") do
local key, value = item:match("^([^=]+)=(.*)$")
if key == "host" then result.shadowtls_serverName = value end
if key == "passwd" then result.shadowtls_password = value end
if key:match("^v[123]$") then result.shadowtls_version = key:sub(2) end
end
result.shadowtls = "1"
result.plugin_opts = nil
result.plugin_enabled = nil
end
end
else
result.plugin_enabled = "1"
@@ -967,7 +984,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
result.error_msg = sub_ss_type .. " 不支持 shadow-tls 插件。"
else
-- 解析SS Shadow-TLS 插件参数
-- 解析SS Shadow-TLS 专用参数
local function parseShadowTLSParams(b64str, out)
local ok, data = pcall(jsonParse, base64Decode(b64str))
if not ok or type(data) ~= "table" then return "" end
@@ -184,9 +184,9 @@ 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: 'dhcp', title: _('Enable DHCP Server'), desc: _('If this DHCP is enabled, disable the main router's DHCP. If disabled, manually set client devices' gateway and DNS to this bypass router's IP.') },
{ tab: 'netsetup', type: form.Flag, id: 'ipv6', title: _('IPv6 Support'), desc: _('Enable or disable IPv6 router advertisements and DHCPv6.') },
// 固件与系统设置
+5 -2
View File
@@ -69,8 +69,11 @@ msgstr "在旁路由模式下工作时填写的上级主路由 IP 地址。"
msgid "Enable DHCP Server"
msgstr "启用 DHCP 服务器"
msgid "Enable or disable LAN DHCP server (usually disabled in side-router mode to avoid IP address conflicts)."
msgstr "开启或关闭局域网 DHCP 服务(旁路由模式下通常建议关闭,避免局域网 IP 分配冲突)。"
msgid "If this DHCP is enabled, disable the main router's DHCP. If disabled, manually set client devices' gateway and DNS to this bypass router's IP."
msgstr "开启此DHCP则需要关闭主路由DHCP, 关闭此DHCP则需要手动将上网设备的网关和DNS改为此旁路由的IP。"
msgid "Leave empty for ISP DNS."
msgstr "留空则使用 ISP 运营商DNS"
msgid "IPv6 Support"
msgstr "IPv6 支持"
+3 -3
View File
@@ -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_p14
PKG_RELEASE:=26
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
PKG_SOURCE_VERSION:=2ca9a94506f972d89a398d560d5667a809e89d9f
PKG_SOURCE_VERSION:=f46ba800d6f195c5c78a8bf566b4f340b9f1ee25
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;
+1 -1
View File
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=my-default-settings
PKG_VERSION:=2
PKG_RELEASE:=16
PKG_RELEASE:=21
include $(INCLUDE_DIR)/package.mk
+136 -184
View File
@@ -1,284 +1,236 @@
#!/bin/sh
#
# Optimized & Refactored OpenWrt WiFi Configuration Script
# Source Reference: https://github.com/kiddin9/my-packages/blob/master/my-default-settings/files/sbin/mywifi
# Optimized OpenWrt WiFi Configuration Script
#
show_help() {
cat << 'EOF_HELP'
用法:
mywifi [选项] [WiFi名称] [WiFi密码]
LOCK_FILE="/var/run/mywifi.lock"
[ -f /etc/openwrt_release ] && . /etc/openwrt_release
参数:
WiFi名称 基础SSID名称(缺省时自动使用系统 hostname)
WiFi密码 无线密码(缺省或未指定时默认无密码,指定时需 8-63 位)
log_info() { echo "[INFO] $*"; }
log_error() { echo "[ERROR] $*" >&2; }
show_help() {
cat << 'EOF'
用法:
mywifi [选项]
选项:
-s, --ssid <名称> 指定基础WiFi名称
-p, --password <密码> 指定WiFi密码
-s, --ssid <名称> 指定基础WiFi名称(默认使用系统 hostname / Kwrt)
-p, --password <密码> 指定WiFi密码(8-63 位,未指定时默认开放网络)
-f, --force 强制重新配置(忽略 wifi=1 完成标记)
-h, --help 查看帮助信息
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" # 选项传参
EOF_HELP
命名规范:
<WiFi名>_2.4G / <WiFi名>_5G / <WiFi名>_6G
EOF
}
# 1. 解析命令行参数(同时兼顾位置参数与选项参数)
# -------------------------------------------------------------
# 1. 参数解析
# -------------------------------------------------------------
CUSTOM_SSID=""
CUSTOM_PASSWD=""
HAS_PASSWD=0
FORCE=0
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-h|--help) show_help; exit 0 ;;
-f|--force) FORCE=1 ;;
-s|--ssid)
shift
CUSTOM_SSID="$1"
[ $# -ge 2 ] && [ "${2#-}" = "$2" ] && { shift; CUSTOM_SSID="$1"; }
;;
-p|--password)
shift
CUSTOM_PASSWD="$1"
HAS_PASSWD=1
;;
*)
if [ -z "$CUSTOM_SSID" ]; then
CUSTOM_SSID="$1"
elif [ $HAS_PASSWD -eq 0 ]; then
CUSTOM_PASSWD="$1"
HAS_PASSWD=1
if [ $# -ge 2 ] && [ "${2#-}" = "$2" ]; then
shift; CUSTOM_PASSWD="$1"; HAS_PASSWD=1
else
CUSTOM_PASSWD=""; HAS_PASSWD=0
fi
;;
esac
shift
done
# 密码合法性校验
# -------------------------------------------------------------
# 2. 状态标记检查与防并发锁
# -------------------------------------------------------------
# 既未强制重新配置,又无自定义参数,且已配置过时直接跳过
if [ $FORCE -eq 0 ] && [ -z "$CUSTOM_SSID" ] && [ $HAS_PASSWD -eq 0 ]; then
if [ "$(uci -q get base_config.@status[0].wifi 2>/dev/null)" = "1" ]; then
log_info "WiFi 已配置完成,无需重复执行。"
exit 0
fi
fi
# 文件锁(有任务参数时排队等待,否则遇锁直接退出)
WAIT_LOCK=0
[ $FORCE -eq 1 ] || [ -n "$CUSTOM_SSID" ] || [ $HAS_PASSWD -eq 1 ] && WAIT_LOCK=1
exec 9>"$LOCK_FILE" 2>/dev/null || true
if ! flock -n 9 2>/dev/null; then
if [ $WAIT_LOCK -eq 1 ]; then
log_info "检测到 mywifi 正在运行,等待其执行完毕..."
flock -w 60 9 2>/dev/null || { log_error "等待执行超时(60s)!"; exit 1; }
else
log_info "检测到 mywifi 正在运行,直接终止。"
exit 0
fi
fi
# 密码合法性检查
if [ $HAS_PASSWD -eq 1 ] && [ -n "$CUSTOM_PASSWD" ]; then
pwd_len=${#CUSTOM_PASSWD}
if [ "$pwd_len" -lt 8 ] || [ "$pwd_len" -gt 63 ]; then
echo "[ERROR] WiFi 密码长度必须在 8 到 63 个字符之间!" >&2
log_error "WiFi 密码长度必须在 8 到 63 个字符之间!"
exit 1
fi
SSID_PASSWD="$CUSTOM_PASSWD"
else
SSID_PASSWD=""
fi
# 2. 深度就绪检测:确保内核PHY驱动、多频硬件、系统初始化状态和UCI配置全部真正就绪
# -------------------------------------------------------------
# 3. 硬件驱动与 UCI 就绪检测
# -------------------------------------------------------------
wait_wifi_ready() {
local max_wait=15
local waited=0
while [ $waited -lt $max_wait ]; do
local phy_count=0
local uci_count=0
# B. 检查内核物理无线设备 (PHY)
while [ $waited -lt 15 ]; do
local phys="$(ls -d /sys/class/ieee80211/phy* 2>/dev/null)"
[ -n "$phys" ] && phy_count=$(echo "$phys" | wc -w)
local phy_count=$(echo $phys | wc -w)
local uci_radios="$(uci -q show wireless | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
local uci_count=$(echo $uci_radios | wc -w)
# C. 统计 UCI wireless 中实际已解析出的 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)
# 缺少配置文件或配置项落后于物理网卡时,触发 wifi config 生成默认配置
if [ $phy_count -gt "$uci_count" ] || { [ $phy_count -eq 0 ] && [ ! -s /etc/config/wireless ]; }; then
command -v wifi >/dev/null && wifi config >/dev/null 2>&1
uci_radios="$(uci -q show wireless | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
uci_count=$(echo $uci_radios | wc -w)
fi
# D. 自动探测与配置补全:
# - 物理网卡已加载,但 UCI 配置中的 radio 数量少于物理网卡数量(多频网卡尚未完全生成配置)
# - 或者配置文件缺失/没有 radio 配置
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
if [ "$uci_count" -gt 0 ] && { [ "$phy_count" -eq 0 ] || [ "$uci_count" -ge "$phy_count" ]; }; then
return 0
fi
# 重新同步一次 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
fi
fi
sleep 1
waited=$((waited + 1))
done
# 超时保护:只要至少有一个 radio 节点,允许降级继续
if [ $uci_count -gt 0 ]; then
return 0
fi
return 1
}
# 执行深度就绪等待
if ! wait_wifi_ready; then
echo "[ERROR] 未检测到可用无线硬件或无线配置生成超时!" >&2
log_error "未检测到可用无线硬件或无线配置生成超时!"
exit 1
fi
# 3. 识别射频频段函数 (2g, 5g, 6g)
# -------------------------------------------------------------
# 4. 辅助函数:频段识别 & Hostname 解析
# -------------------------------------------------------------
detect_band() {
local radio="$1"
local band="$(uci -q get wireless.${radio}.band 2>/dev/null | tr 'A-Z' 'a-z')"
local band="$(uci -q get wireless.${radio}.band | tr 'A-Z' 'a-z')"
case "$band" in
*2g*|*2.4g*) echo "2g"; return ;;
*5g*) echo "5g"; return ;;
*6g*) echo "6g"; return ;;
*5g*) echo "5g"; return ;;
*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
echo "2g"; return
elif [ "$channel" -ge 32 ] && [ "$channel" -le 177 ]; then
echo "5g"; return
elif [ "$channel" -ge 180 ]; then
echo "6g"; return
fi
local channel="$(uci -q get wireless.${radio}.channel)"
if [ -n "$channel" ] && [ "$channel" -eq "$channel" ] 2>/dev/null; then
[ "$channel" -ge 1 ] && [ "$channel" -le 14 ] && { echo "2g"; return; }
[ "$channel" -ge 32 ] && [ "$channel" -le 177 ] && { echo "5g"; return; }
[ "$channel" -ge 180 ] && { echo "6g"; return; }
fi
# hwmode 推断
local hwmode="$(uci -q get wireless.${radio}.hwmode 2>/dev/null | tr 'A-Z' 'a-z')"
local hwmode="$(uci -q get wireless.${radio}.hwmode | tr 'A-Z' 'a-z')"
case "$hwmode" in
*a*) echo "5g"; return ;;
*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
elif [ "$radio" = "radio1" ]; then
local r0_band="$(uci -q get wireless.radio0.band 2>/dev/null | tr 'A-Z' 'a-z')"
[ "$r0_band" = "2g" ] && echo "5g" && return
# 针对双频 radio0/radio1 的交叉推导
if [ "$radio" = "radio0" ] && [ "$(uci -q get wireless.radio1.band)" = "5g" ]; then
echo "2g"; return
elif [ "$radio" = "radio1" ] && [ "$(uci -q get wireless.radio0.band)" = "2g" ]; then
echo "5g"; return
fi
echo "2g"
}
# 4. 加载系统发行版信息(若存在)
[ -f /etc/openwrt_release ] && . /etc/openwrt_release
get_base_ssid() {
[ -n "$CUSTOM_SSID" ] && { echo "$CUSTOM_SSID"; return; }
local h="$(uci -q get system.@system[0].hostname)"
case "$h" in
""|ieee80211|"(none)"|OpenWrt) echo "Kwrt" ;;
*) echo "$h" ;;
esac
}
# 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"
fi
# -------------------------------------------------------------
# 5. 无线配置生成
# -------------------------------------------------------------
BASE_SSID="$(get_base_ssid)"
# 移除禁用标记
# 清理 disabled 配置行并规范化 radio0/radio1 频段
sed -i '/option disabled/d' /etc/config/wireless 2>/dev/null
[ "$(uci -q get wireless.radio1.band)" = "5g" ] && [ -z "$(uci -q get wireless.radio0.band)" ] && \
uci -q set wireless.radio0.band="2g"
# 修正双频标记
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))
# 重命名现有匿名 wifi-iface 为 default_radioX,便于幂等管理
idx=0
while uci rename wireless.@wifi-iface[$idx]="default_radio$idx" 2>/dev/null; do
idx=$((idx + 1))
done
# 6. 配置各 Radio 参数
wifi_setup_radio() {
local radio="$1"
# 获取所有射频设备
RADIOS="$(uci -q show wireless | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
[ -z "$RADIOS" ] && RADIOS="radio0 radio1 radio2 radio3"
uci -q get wireless.${radio} >/dev/null 2>&1 || return 0
for radio in $RADIOS; do
uci -q get wireless.${radio} >/dev/null 2>&1 || continue
uci -q del wireless.${radio}.disabled
uci -q set wireless.${radio}.country='CN'
local band="$(detect_band "$radio")"
local ssid_suffix=""
band="$(detect_band "$radio")"
case "$band" in
2g) ssid_suffix="_2.4G" ;;
5g) ssid_suffix="_5G" ;;
6g) ssid_suffix="_6G" ;;
*) ssid_suffix="_${band}" ;;
2g) suffix="_2.4G" ;;
5g) suffix="_5G" ;;
6g) suffix="_6G" ;;
*) suffix="_${band}" ;;
esac
local full_ssid="${SSID}${ssid_suffix}"
full_ssid="${BASE_SSID}${suffix}"
iface="default_${radio}"
# 特定硬件架构适配(如高通 ipq60xx)
if [ "$band" = "5g" ]; then
case "$DISTRIB_TARGET" in
*ipq60xx*)
uci -q set wireless.${radio}.htmode='HE80'
;;
esac
fi
# 配置 radio 参数
uci -q del wireless.${radio}.disabled
uci -q set wireless.${radio}.country='CN'
[ "$band" = "5g" ] && case "$DISTRIB_TARGET" in *ipq60xx*) uci -q set wireless.${radio}.htmode='HE80' ;; esac
local iface="default_${radio}"
# 配置 iface 参数
uci -q get wireless.${iface} >/dev/null 2>&1 || uci -q set wireless.${iface}=wifi-iface
uci -q set wireless.${iface}.device="${radio}"
uci -q set wireless.${iface}.mode='ap'
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'
fi
uci -q set wireless.${iface}.key="${SSID_PASSWD}"
echo "[INFO] ${radio} (${band}): SSID='${full_ssid}', 加密='$(uci -q get wireless.${iface}.encryption)'"
if [ $HAS_PASSWD -eq 1 ] && [ -n "$CUSTOM_PASSWD" ]; then
enc="psk2"
[ "$band" = "6g" ] && enc="sae"
uci -q set wireless.${iface}.encryption="$enc"
uci -q set wireless.${iface}.key="$CUSTOM_PASSWD"
log_info "${radio} (${band}): SSID='${full_ssid}', 加密='${enc}'"
else
uci -q set wireless.${iface}.encryption='none'
uci -q del wireless.${iface}.key
echo "[INFO] ${radio} (${band}): SSID='${full_ssid}', 无密码 (开放网络)"
log_info "${radio} (${band}): SSID='${full_ssid}', 无密码 (开放网络)"
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"
for radio in $radios; do
wifi_setup_radio "$radio"
done
# 提交配置
# -------------------------------------------------------------
# 6. 提交配置与重载
# -------------------------------------------------------------
uci commit wireless 2>/dev/null
# 若存在 base_config(kiddin9 固件状态记录),更新状态
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
fi
# 重载无线网络
if command -v wifi >/dev/null 2>&1; then
wifi reload
elif [ -x /sbin/wifi ]; then
/sbin/wifi reload
fi
wifi reload 2>/dev/null || /sbin/wifi reload 2>/dev/null
echo "[SUCCESS] 无线配置已更新并生效。"
log_info "无线配置已更新并生效。"
+2 -2
View File
@@ -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)/-/ \
+31 -23
View File
@@ -11,7 +11,7 @@
# wwand-ncm - NCM/ECM backend (DEPENDS wwand)
# wwand-mhi - PCIe/MHI transport + drivers (DEPENDS wwand; add a backend)
# wwand-esim - eSIM management (DEPENDS wwand + lpac; no backend)
# wwand-gps - GNSS/NMEA glue for ugps (DEPENDS wwand + ugps; no backend)
# wwand-gps - GNSS/NMEA reader (DEPENDS wwand; no backend)
# A typical QMI router installs `wwand-qmi` (which pulls in `wwand`); add
# `wwand-mbim` / `wwand-ncm` for those modems, `wwand-mhi` for a PCIe/MHI modem.
# The ucode tree ships PRECOMPILED to bytecode (repo-root CMakeLists.txt,
@@ -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_p63
PKG_RELEASE:=52
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=1b575d41dd35684e035541afe1f129da072275dd
PKG_SOURCE_VERSION:=2c428ced46e584daae9719f722d2ed7c9aa28fff
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
@@ -439,7 +439,7 @@ define Package/wwand-esim/install
endef
# ---------------------------------------------------------------------------
# wwand-gps: point ugps at the modem's NMEA port. Backend-neutral — finding the
# wwand-gps: read NMEA off the modem's NMEA port. Backend-neutral — finding the
# port and starting the receiver are both in the base package, and neither
# depends on the control protocol.
# ---------------------------------------------------------------------------
@@ -447,34 +447,42 @@ define Package/wwand-gps
SECTION:=net
CATEGORY:=Network
SUBMENU:=WWAN
TITLE:=GNSS/NMEA glue for wwand (ugps)
# ugps is OpenWrt base: it parses NMEA, can set the clock, and publishes a
# `gps` ubus object. It knows nothing about modems, and it reads its tty from
# a STATIC /etc/config/gps — which is exactly the gap this package fills,
# because wwand's NMEA port is discovered and can move between boots or when
# a modem is replaced.
DEPENDS:=+wwand +ugps
TITLE:=GNSS/NMEA reader for wwand
# Reads NMEA off the modem's own port and reports it through wwand's ubus.
# It used to drive ugps (OpenWrt base) instead, and no longer depends on it:
# ugps takes ONE static tty out of /etc/config/gps and publishes ONE `gps`
# object, so a two-modem box could only ever have one position; it exits on
# tty EOF, which on a modem reset means respawning against a device that is
# not back; and it parses neither GSV (satellites in view) nor GSA (fix type,
# PDOP/VDOP) and drops every talker but $GP/$GN. wwand already finds the
# port, starts the receiver and owns the lifecycle, so the reading is the
# small part. Installing ugps alongside is harmless — wwand does not touch
# /etc/config/gps and does not claim the `gps` ubus name.
DEPENDS:=+wwand
endef
define Package/wwand-gps/description
Joins three pieces that already existed with nothing between them: wwand finds
the modem's NMEA port during enumeration (reported as `gps_port`), `option
gnss` starts the receiver with the vendor AT command — QMI's LOC service is
broken on Quectel and AT is what works, and only wwand has the port — and ugps
reads the NMEA and publishes a position.
wwand finds the modem's NMEA port during enumeration (reported as `gps_port`)
and `option gnss` starts the receiver with the vendor AT command — QMI's LOC
service is broken on Quectel and AT is what works, and only wwand has the
port. This package reads that port: NMEA 0183 framing, checksums, RMC/GGA/GSA/
GSV/GLL/VTG/ZDA from ANY talker, reported through `modem_gps` as numbers, with
the satellites in view and their SNR, the fix type and the DOP values.
What this adds is the config write that connects them, plus a `modem_gps` ubus
method that answers with both halves at once: the port and receiver state wwand
knows, merged with whatever ugps reports.
ONE READER PER MODEM, so a two-modem router has two positions. `option
gnss_set_time` hands the receiver's clock to wwand's own set_clock, which only
ever steps a clock that is plainly unset and so never fights sysntpd.
Good citizen: it manages exactly one `config gps` section and only one it
created itself (marked `option wwand '1'`). An operator's own section — a hat
GPS on a serial port, a second receiver — is never touched or repointed.
Good citizen: it writes no config at all and does not claim the `gps` ubus
name, so ugps and an operator's own receiver are unaffected by installing it.
One port is only ever read once — a second modem naming a tty that is already
being read is refused rather than given half a stream.
endef
define Package/wwand-gps/install
$(INSTALL_DIR) $(1)$(UCDIR)
$(INSTALL_DATA) $(WWAND_UCODE)/gps.uc $(1)$(UCDIR)/
$(INSTALL_DATA) $(WWAND_UCODE)/nmea.uc $(1)$(UCDIR)/
endef
# ---------------------------------------------------------------------------
+3 -2
View File
@@ -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
+4 -3
View File
@@ -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
View File
@@ -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")' \