🎨 Sync 2026-11-13 17:35:38

This commit is contained in:
github-actions[bot] committed 2026-11-13 17:35:38 +08:00
1 parent 4dc21dd663
commit 342030204e
28 files changed
+398 -162

No files matched your search

+3 -3
View File
@@ -5,11 +5,11 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=honk
PKG_VERSION:=2026.11.12~a09d3bf
PKG_RELEASE:=35
PKG_VERSION:=2026.11.13~f8bbf4f
PKG_RELEASE:=36
HONK_SRC_URL:=https://github.com/daeuniverse/honk.git
HONK_SRC_VERSION:=a09d3bfa272f3670d6606373452be41606bdc29e
HONK_SRC_VERSION:=f8bbf4f4b4ab1cdf673c440df316c3c642a31f9c
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE
+11 -12
View File
@@ -11,7 +11,7 @@ PKG_ARCH_kai:=$(ARCH)
PKG_NAME:=kai
PKG_VERSION:=0.0.28
PKG_RELEASE:=13
PKG_RELEASE:=14
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kai-runtime-v$(PKG_VERSION)/
PKG_HASH:=skip
@@ -27,7 +27,7 @@ define Package/$(PKG_NAME)
CATEGORY:=System
SUBMENU:=
TITLE:=KAI
DEPENDS:=+kai_session +kai-agent +luci-lib-linkeaseauth +linkease-app-entry
DEPENDS:=+kai_session +kai-agent +luci-lib-linkeaseauth
URL:=https://github.com/linkease/istore-ai-helper
endef
@@ -41,13 +41,15 @@ endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
# Compatibility entry for desktops which only scan the old directory.
# KaiPlus owns the desktop entry. Retire only links created by KAI.
root="$${IPKG_INSTROOT:-}"
legacy="$${root}/usr/share/linkeasefull/desktop-apps.d/05-kai-plugin.json"
if [ ! -e "$${legacy}" ] && [ ! -L "$${legacy}" ]; then
mkdir -p "$${legacy%/*}"
ln -s /usr/share/kai/kai-plugin.json "$${legacy}" || exit $$?
fi
for directory in /usr/share/linkease/apps.d /usr/share/linkeasefull/desktop-apps.d; do
registration="$${root}$${directory}/05-kai-plugin.json"
if [ "$$(readlink "$${registration}" 2>/dev/null)" = "/usr/share/kai/kai-plugin.json" ]; then
rm -f "$${registration}"
fi
done
rmdir "$${root}/usr/share/linkeasefull/desktop-apps.d" 2>/dev/null || true
exit 0
endef
@@ -71,14 +73,11 @@ define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/libexec $(1)/etc/init.d $(1)/etc/config $(1)/usr/share/kai/www $(1)/usr/share/linkease/apps.d
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/libexec $(1)/etc/init.d $(1)/etc/config
$(INSTALL_BIN) $(PKG_BUILD_DIR)/kai.$(PKG_ARCH_kai) $(1)/usr/sbin/kai_bin
$(INSTALL_BIN) ./files/kai-session-launch $(1)/usr/libexec/kai-session-launch
$(INSTALL_BIN) ./files/kai.init $(1)/etc/init.d/kai
$(INSTALL_CONF) ./files/kai.config $(1)/etc/config/kai
$(INSTALL_DATA) ./files/kai-plugin.json $(1)/usr/share/kai/kai-plugin.json
$(INSTALL_DATA) ./files/logo.png $(1)/usr/share/kai/www/logo.png
ln -sf /usr/share/kai/kai-plugin.json $(1)/usr/share/linkease/apps.d/05-kai-plugin.json
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
-59
View File
@@ -1,59 +0,0 @@
{
"schemaVersion": 1,
"id": "kai",
"name": "KAI",
"icon": "logo.png",
"staticRoot": "/usr/share/kai/www",
"desktop": {
"mode": "iframe",
"target": {
"scheme": "http",
"hostMode": "request-host",
"port": {
"default": 8197,
"keys": {
"uci": "kai.@kai[0].port"
}
},
"path": "/apps/kai/"
},
"access": {
"scope": "lan",
"blockedTitle": "请在局域网内打开 KAI",
"blockedMessage": "KAI 运行在设备的独立服务端口,请连接设备所在局域网后再打开。"
}
},
"standalone": {
"basePath": "/apps/kai/",
"externalOpen": {
"enabled": true,
"label": "在浏览器中打开 KAI"
}
},
"config": {
"providerOrder": ["uci"],
"providers": {
"uci": {
"type": "uci"
}
}
},
"backend": {
"type": "http",
"transport": "tcp",
"scheme": "http",
"host": "127.0.0.1",
"portFromUci": "kai.@kai[0].port",
"defaultPort": 8197,
"upstreamBasePath": "/apps/kai/",
"pathMode": "preserve",
"proxyMode": "app-base"
},
"window": {
"width": 1180,
"height": 760,
"singleton": true
},
"capabilities": ["assistant", "chat"],
"categories": ["other"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

@@ -1312,7 +1312,7 @@ function loadModalTitle(title, addtitle, section_id) {
return label ? title + ' » ' + label : addtitle;
}
function loadLabel(preadds, section_id) {
function loadChoices(preadds, section_id) {
delete this.keylist;
delete this.vallist;
@@ -1322,7 +1322,7 @@ function loadLabel(preadds, section_id) {
return this.super('load', section_id);
}
function loadLabelValues(uciconfig, sectiontype, options = {}) {
function loadChoicesGetMap(uciconfig, sectiontype, options = {}) {
const values = [];
switch (sectiontype) {
@@ -2106,8 +2106,8 @@ return baseclass.extend({
// load
loadDefaultLabel,
loadModalTitle,
loadLabel,
loadLabelValues,
loadChoices,
loadChoicesGetMap,
// render
renderStatus,
updateStatus,
@@ -627,9 +627,9 @@ function renderListeners(s, uciconfig, isClient) {
_('Name of the Sub rule used for inbound matching.'));
o.value.apply(o, ['', _('none')]);
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('none')],
...hm.loadLabelValues(this.config, 'subrule-group')
...hm.loadChoicesGetMap(this.config, 'subrule-group')
], section_id);
}
o.modalonly = false;
@@ -642,9 +642,9 @@ function renderListeners(s, uciconfig, isClient) {
o.value.apply(o, res);
})
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
o.modalonly = false;
@@ -789,9 +789,9 @@ function renderListeners(s, uciconfig, isClient) {
o.value.apply(o, res);
})
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
o.depends('tls_reality', '1');
@@ -1016,9 +1016,9 @@ function renderListeners(s, uciconfig, isClient) {
o.value.apply(o, res);
})
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
o.depends('hysteria2_realm', '1');
@@ -609,9 +609,9 @@ function renderPayload(s, total, uciconfig) {
initPayload(o, n, 'factor');
const inname_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [
hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'inbound')
...hm.loadChoicesGetMap(this.config, 'inbound')
], section_id);
return inname_old_load.apply(this, arguments);
@@ -624,9 +624,9 @@ function renderPayload(s, total, uciconfig) {
initPayload(o, n, 'factor');
const rematchname_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [
hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'rematch-name')
...hm.loadChoicesGetMap(this.config, 'rematch-name')
], section_id);
return rematchname_old_load.apply(this, arguments);
@@ -661,7 +661,7 @@ function renderPayload(s, total, uciconfig) {
initPayload(o, n, 'factor');
const rule_set_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, hm.loadLabelValues(this.config, 'ruleset'), section_id);
hm.loadChoices.call(this, hm.loadChoicesGetMap(this.config, 'ruleset'), section_id);
return rule_set_old_load.apply(this, arguments);
}
@@ -712,13 +712,13 @@ function renderPayload(s, total, uciconfig) {
initDynamicPayload(o, n, 'factor');
const fused_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [
hm.loadChoices.call(this, [
...hm.inbound_type.map(e => [e, _('IN-TYPE')]), // Multi-values are not supported
...hm.loadLabelValues(this.config, 'inbound').map(e => [e, _('IN-NAME')]),
...hm.loadLabelValues(this.config, 'rematch-name').map(e => [e, _('REMATCH-NAME')]),
...hm.loadChoicesGetMap(this.config, 'inbound').map(e => [e, _('IN-NAME')]),
...hm.loadChoicesGetMap(this.config, 'rematch-name').map(e => [e, _('REMATCH-NAME')]),
[['udp', _('UDP')], _('NETWORK')],
[['tcp', _('TCP')], _('NETWORK')],
...hm.loadLabelValues(this.config, 'ruleset').map(e => [e, _('RULE-SET')])
...hm.loadChoicesGetMap(this.config, 'ruleset').map(e => [e, _('RULE-SET')])
], section_id);
return fused_old_load.apply(this, arguments);
@@ -823,9 +823,9 @@ function renderRules(s, uciconfig) {
initEntryOption(o, 'detour');
const detour_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [
hm.loadChoices.call(this, [
...hm.preset_outbound.full,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
return detour_old_load.apply(this, arguments);
@@ -878,9 +878,9 @@ function renderPolicies(s, uciconfig) {
_('Match rule set.'));
o.value('', _('-- Please choose --'));
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'ruleset', {behaviors: ['domain', 'classical']})
...hm.loadChoicesGetMap(this.config, 'ruleset', {behaviors: ['domain', 'classical']})
], section_id);
}
o.depends('type', 'rule_set');
@@ -909,9 +909,9 @@ function renderPolicies(s, uciconfig) {
o.value.apply(o, res);
})
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
o.editable = true;
@@ -1068,9 +1068,9 @@ return view.extend({
})
so.keep_order = true;
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.proxy,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
so.editable = true;
@@ -1079,9 +1079,9 @@ return view.extend({
so.value('', _('-- Please choose --'));
so.keep_order = true;
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'node')
...hm.loadChoicesGetMap(this.config, 'node')
], section_id);
}
so.validate = function(section_id, value) {
@@ -1099,9 +1099,9 @@ return view.extend({
so.value('', _('-- Please choose --'));
so.keep_order = true;
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'provider')
...hm.loadChoicesGetMap(this.config, 'provider')
], section_id);
}
so.validate = function(section_id, value) {
@@ -1136,10 +1136,10 @@ return view.extend({
so.value.apply(so, res);
})
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.proxy,
['NODE', _('-- PROXY-NODE --')],
...hm.loadLabelValues(this.config, 'node')
...hm.loadChoicesGetMap(this.config, 'node')
], section_id);
}
so.textvalue = hm.textvalue2Value;
@@ -1201,12 +1201,12 @@ return view.extend({
so.value.apply(so, res);
})
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.proxy,
['GROUP', _('-- PROXY-GROUP --')],
...hm.loadLabelValues(this.config, 'proxy_group'),
...hm.loadChoicesGetMap(this.config, 'proxy_group'),
['NODE', _('-- PROXY-NODE --')],
...hm.loadLabelValues(this.config, 'node')
...hm.loadChoicesGetMap(this.config, 'node')
], section_id);
}
so.depends('type', 'select');
@@ -1359,9 +1359,9 @@ return view.extend({
so = ss.option(form.ListValue, 'SUB-RULE', _('SUB-RULE'));
so.load = function(section_id) {
hm.loadLabel.call(this, [
hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'subrule-group')
...hm.loadChoicesGetMap(this.config, 'subrule-group')
], section_id);
return this.section.getOption('entry').instget(section_id).subrule || '';
@@ -1646,9 +1646,9 @@ return view.extend({
);
const detour_old_load = so.load;
so.load = function(section_id) {
hm.loadLabel.call(this, [
hm.loadChoices.call(this, [
...hm.preset_outbound.dns,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
return detour_old_load.apply(this, arguments);
@@ -301,9 +301,9 @@ return view.extend({
so = ss.taboption('field_general', form.ListValue, 'target_sub_rule', _('Use sub rule'));
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'subrule-group')
...hm.loadChoicesGetMap(this.config, 'subrule-group')
], section_id);
}
so.validate = function(section_id, value) {
@@ -2426,9 +2426,9 @@ return view.extend({
so.value.apply(so, res);
})
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
so.textvalue = hm.textvalue2Value;
@@ -2751,9 +2751,9 @@ return view.extend({
so = ss.option(form.ListValue, 'chain_head_sub', _('Destination provider'));
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'provider')
...hm.loadChoicesGetMap(this.config, 'provider')
], section_id);
}
so.rmempty = false;
@@ -2762,9 +2762,9 @@ return view.extend({
so = ss.option(form.ListValue, 'chain_head', _('Destination proxy node'));
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'node')
...hm.loadChoicesGetMap(this.config, 'node')
], section_id);
}
so.rmempty = false;
@@ -2781,9 +2781,9 @@ return view.extend({
so = ss.option(form.ListValue, 'chain_tail_group', _('Transit proxy group'));
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
so.rmempty = false;
@@ -2792,9 +2792,9 @@ return view.extend({
so = ss.option(form.ListValue, 'chain_tail', _('Transit proxy node'));
so.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'node')
...hm.loadChoicesGetMap(this.config, 'node')
], section_id);
}
so.rmempty = false;
@@ -418,9 +418,9 @@ return view.extend({
o.value.apply(o, res);
})
o.load = function(section_id) {
return hm.loadLabel.call(this, [
return hm.loadChoices.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
...hm.loadChoicesGetMap(this.config, 'proxy_group')
], section_id);
}
o.textvalue = hm.textvalue2Value;
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=KaiPlus
PKG_VERSION:=1.0.11-r1
PKG_RELEASE:=16
PKG_VERSION:=1.0.11-r2
PKG_RELEASE:=17
LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-taskd +luci-lib-linkeaseauth +linkease-app-entry
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
@@ -9,14 +9,16 @@ function classic.installed()
and fs.access("/usr/sbin/kai_session", "x")
and fs.access("/etc/init.d/kai", "x")
and fs.access("/etc/config/kai")
and fs.access("/usr/share/kai/kai-plugin.json")
)
end
function classic.status()
local installed = classic.installed()
local port = tonumber(require("luci.model.uci").cursor():get_first("kai", "kai", "port") or "8197")
if not port or port < 1 or port > 65535 or port % 1 ~= 0 then port = nil end
return {
installed = installed,
port = port,
running = installed and sys.call("pidof kai_bin >/dev/null") == 0
}
end
@@ -22,7 +22,8 @@
var classicPageInstalled = <%= installed and "true" or "false" %>;
var classicInstallUrl = '<%=url("admin/services/kaiplus/install_kai")%>';
var classicUpdateUrl = '<%=url("admin/services/kaiplus/update_kai")%>';
var classicOpenUrl = '<%=url("admin/services/linkease_apps/open")%>?id=kai';
var classicAuthUrl = '<%=url("admin/services/linkease_auth/auth")%>';
var classicOpenUrl = '';
function installClassicKai() {
classicPackageTask(false);
@@ -83,7 +84,11 @@ XHR.poll(5, '<%=url("admin/services/kaiplus/classic_status")%>', null, function(
installButton.style.display = st.installed ? 'none' : '';
updateButton.style.display = st.installed ? '' : 'none';
updateHint.style.display = st.installed ? '' : 'none';
openButton.style.display = st.installed && st.running ? '' : 'none';
var port = Number(st.port);
classicOpenUrl = port >= 1 && port <= 65535 && port === Math.floor(port)
? classicAuthUrl + '?return=' + encodeURIComponent('http://' + window.location.hostname + ':' + port + '/apps/kai/')
: '';
openButton.style.display = st.installed && st.running && classicOpenUrl ? '' : 'none';
});
//]]></script>
+12 -4
View File
@@ -1,6 +1,6 @@
const assert = require('node:assert/strict');
const vm = require('node:vm');
function page(installed) {
function page(installed, hostname = '192.168.30.7') {
const elements = Object.fromEntries(['classic-kai-status', 'classic-kai-install', 'classic-kai-update', 'classic-kai-update-hint', 'classic-kai-open']
.map(id => [id, { style: {}, disabled: false }]));
const state = { elements, alerts: [], logs: [], reloads: 0 };
@@ -11,7 +11,7 @@ function page(installed) {
XHR, document: { getElementById: id => elements[id] },
window: {
confirm: () => true, alert: message => state.alerts.push(message),
location: { reload: () => state.reloads++ },
location: { hostname, reload: () => state.reloads++ },
taskd: { csrfToken: 'test-token', show_log: id => state.logs.push(id) }
}
};
@@ -46,13 +46,21 @@ state.context.installClassicKai();
state.post.callback({ responseText: '{"ok":true,"installed":true}' });
assert.equal(state.reloads, 1);
state = page(true);
state.poll(null, { installed: true, running: true });
state.poll(null, { installed: true, running: true, port: 8197 });
assert.equal(state.elements['classic-kai-install'].style.display, 'none');
assert.equal(state.elements['classic-kai-update'].style.display, '');
assert.equal(state.elements['classic-kai-open'].style.display, '');
assert.equal(state.elements['classic-kai-status'].style.color, 'green');
assert.equal(state.reloads, 0);
assert.equal(state.context.classicOpenUrl, '/cgi-bin/luci/admin/services/linkease_apps/open?id=kai');
assert.equal(state.context.classicOpenUrl, '/cgi-bin/luci/admin/services/linkease_auth/auth?return=' + encodeURIComponent('http://192.168.30.7:8197/apps/kai/'));
const ipv6 = page(true, '[fd00::7]');
ipv6.poll(null, { installed: true, running: true, port: 8201 });
assert.equal(ipv6.context.classicOpenUrl, '/cgi-bin/luci/admin/services/linkease_auth/auth?return=' + encodeURIComponent('http://[fd00::7]:8201/apps/kai/'));
for (const port of [0, 65536, 1.5, 'invalid', undefined]) {
ipv6.poll(null, { installed: true, running: true, port });
assert.equal(ipv6.context.classicOpenUrl, '');
assert.equal(ipv6.elements['classic-kai-open'].style.display, 'none');
}
state.poll(null, { installed: true, running: false });
assert.equal(state.elements['classic-kai-open'].style.display, 'none');
assert.equal(state.elements['classic-kai-update'].style.display, '');
+19 -2
View File
@@ -5,7 +5,7 @@ local function check(value, message)
assert(value, message)
checks = checks + 1
end
local runtime_files = { "/usr/sbin/kai_bin", "/usr/sbin/kai_session", "/etc/init.d/kai", "/etc/config/kai", "/usr/share/kai/kai-plugin.json" }
local runtime_files = { "/usr/sbin/kai_bin", "/usr/sbin/kai_session", "/etc/init.d/kai", "/etc/config/kai" }
local function fixture(o)
o = o or {}
local f = { calls = {}, routes = {} }
@@ -22,6 +22,12 @@ local function fixture(o)
exec = function(command) f.task_query = command; return "task status fixture" end
}
package.loaded["nixio.fs"], package.loaded["luci.sys"] = fs, sys
package.loaded["luci.model.uci"] = { cursor = function() return {
get_first = function(_, config, section, option)
check(config == "kai" and section == "kai" and option == "port", "read classic port")
return o.port
end
} end }
package.loaded["luci.jsonc"] = { parse = function() return o.task_status or {} end }
package.loaded["luci.util"] = { shellquote = function(v) return "'" .. v:gsub("'", "'\\''") .. "'" end }
package.loaded["luci.http"] = {
@@ -87,7 +93,18 @@ f.controller.classic_status()
check(f.reply.installed == false and f.reply.running == false, "missing KAI status")
f = fixture({ installed = true, running = true })
f.controller.classic_status()
check(f.reply.installed and f.reply.running, "runtime must work without luci-app-kai")
check(f.reply.installed and f.reply.running, "runtime must work without luci-app-kai or a desktop manifest")
check(f.reply.port == 8197, "default classic port")
for _, port in ipairs({ "8201", "80", "65535" }) do
local configured = fixture({ installed = true, port = port })
configured.controller.classic_status()
check(configured.reply.port == tonumber(port), "custom classic port")
end
for _, port in ipairs({ "0", "65536", "8197.5", "invalid" }) do
local configured = fixture({ installed = true, port = port })
configured.controller.classic_status()
check(configured.reply.port == nil, "invalid ports must not produce a launch target")
end
check(f.calls[1] == "pidof kai_bin >/dev/null", "status must use classic process")
for _, path in ipairs(runtime_files) do
f = fixture({ installed = true, missing = path })
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.429
PKG_RELEASE:=75
PKG_VERSION:=0.1.432
PKG_RELEASE:=76
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
+5 -1
View File
@@ -9,10 +9,14 @@ LUCI_TITLE:=LuCI support for quickstart
LUCI_DEPENDS:=+quickstart +luci-app-ttyd +luci-i18n-ttyd-zh-cn
LUCI_PKGARCH:=all
PKG_VERSION:=0.17.1-rc20261028-r1
PKG_VERSION:=0.17.1-rc20261028-r2
# PKG_RELEASE MUST be empty for luci.mk
PKG_RELEASE:=
ifeq ($(CONFIG_USE_APK),y)
PKG_VERSION:=$(subst -rc,_rc,$(PKG_VERSION))
endif
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
+3 -3
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=198
PKG_RELEASE:=102
PKG_RELEASE:=103
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -33,7 +33,7 @@ LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+libuci-lua +lua +luci-compat +dns2tcp +dnsmasq-full \
+ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +curl +nping \
+resolveip +curl +nping +coreutils-timeout \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
+PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
@@ -109,7 +109,7 @@ endchoice
choice
prompt "V2ray-core Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
default PACKAGE_$(PKG_NAME)_INCLUDE_Xray
config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
bool "None"
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-trafficctl
PKG_VERSION:=1.30.3
PKG_RELEASE:=30
PKG_VERSION:=1.30.4
PKG_RELEASE:=31
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
@@ -554,6 +554,20 @@ return baseclass.extend({
o.value('cdma', 'CDMA');
bind(o);
/* NSA vs SA is a switch of its own, not part of "Radio technology":
the modem's vendor AT setting, which wwand knows only for the
Foxconn T99W175 and the Quectel RG50xQ/RM5xxQ (wwand atcmd.uc
nr5g_mode_commands); elsewhere the daemon reports it as not
applied */
o = s.taboption(tab, form.ListValue, 'nr5g_mode', _('5G NR mode'),
_('Which kind of 5G the modem may use: non-standalone (5G on an LTE anchor), standalone (5G core), or both. Separate from "Radio technology", which only allows 5G at all. Set over QMI on Qualcomm modems (QMI, or MBIM with the QMI passthrough), otherwise over AT on the Foxconn T99W175 and Quectel RG50xQ/RM5xxQ. With "5G NR" alone, "NSA only" finds no network, because NSA needs LTE.'));
o.value('', _('Leave modem setting unchanged'));
o.value('both', _('NSA and SA'));
o.value('nsa', _('NSA only'));
o.value('sa', _('SA only'));
o.optional = true;
bind(o);
/* the names are config_check / nas.uc RAT_ORDER_IDS; the daemon drops
an unknown or repeated one with a warning, so it is refused here
before it gets that far */
+3 -3
View File
@@ -20,12 +20,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-proto-wwand
PKG_VERSION:=1.7.0_pre1
PKG_RELEASE:=18
PKG_VERSION:=1.8.0_pre2
PKG_RELEASE:=19
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git
PKG_SOURCE_VERSION:=fb547fef6576e16ca0e83bd8379f82c27a040613
PKG_SOURCE_VERSION:=c451947e1355487391964dea79d73da2e58738a4
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
+74
View File
@@ -0,0 +1,74 @@
#
# Copyright (C) 2026 Quickstart NetPolicy Contributors
#
# This is free software, licensed under the GNU General Public License v3.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=quickstart-netpolicy
PKG_VERSION:=fastnet-runtime-v0.8.9
PKG_RELEASE:=1
NETPOLICY_TARGET_x86_64:=x86_64-unknown-linux-musl
NETPOLICY_TARGET_aarch64:=aarch64-unknown-linux-musl
NETPOLICY_TARGET:=$(NETPOLICY_TARGET_$(ARCH))
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(NETPOLICY_TARGET).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/quickstart-runtime-v0.17.1-rc20261028/
PKG_HASH_x86_64:=39861c6d508a900d691315b6443f274ea1e283bc3f8c9f9fa91b4e992814a372
PKG_HASH_aarch64:=ef2dd2c67ce02b945e89c9fbff66650586fb1c1410717b9306e583c4ad53e480
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
ifeq ($(CONFIG_USE_APK),y)
PKG_VERSION:=fastnet-runtime-v0.8.9
endif
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
include $(INCLUDE_DIR)/package.mk
# The packaged ELF must match binarySha256 in the runtime manifest.
RSTRIP:=:
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=Quickstart native network policy engine
URL:=https://github.com/istoreos/quickstart-netpolicy
DEPENDS:=@(x86_64||aarch64) +ca-bundle
endef
define Package/$(PKG_NAME)/description
Independent eBPF network-policy engine for Quickstart device rate limits.
It uses its own binary, port, data files and kernel maps and does not replace Bandix.
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/quickstart-netpolicy
endef
define Build/Prepare
rm -rf $(PKG_BUILD_DIR)
$(INSTALL_DIR) $(PKG_BUILD_DIR)
gzip -dc $(DL_DIR)/$(PKG_SOURCE) | tar -C $(PKG_BUILD_DIR) -xf -
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin $(1)/etc/init.d $(1)/etc/config $(1)/usr/share/quickstart-netpolicy
$(INSTALL_BIN) $(PKG_BUILD_DIR)/payload/quickstart-netpolicy $(1)/usr/bin/quickstart-netpolicy
$(INSTALL_BIN) ./files/quickstart-netpolicy.init $(1)/etc/init.d/quickstart-netpolicy
$(INSTALL_CONF) ./files/quickstart-netpolicy.config $(1)/etc/config/quickstart-netpolicy
$(INSTALL_DATA) $(PKG_BUILD_DIR)/manifest.json $(1)/usr/share/quickstart-netpolicy/manifest.json
$(INSTALL_DATA) $(PKG_BUILD_DIR)/payload/LICENSE $(1)/usr/share/quickstart-netpolicy/LICENSE
$(INSTALL_DATA) $(PKG_BUILD_DIR)/payload/NOTICE $(1)/usr/share/quickstart-netpolicy/NOTICE
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
@@ -0,0 +1,18 @@
# Legacy main keeps its original endpoint and data directory.
config quickstart-netpolicy 'main'
option network 'lan'
option enabled '0'
option interface 'br-lan'
option port '8765'
option data_dir '/usr/share/quickstart-netpolicy'
option log_level 'info'
option traffic_history '0'
option dns_monitoring '0'
# Additional networks are opt-in and require explicit stable resources:
# config quickstart-netpolicy 'office'
# option enabled '0'
# option network 'office'
# option interface 'br-office'
# option port '8766'
# option data_dir '/usr/share/quickstart-netpolicy/networks/office'
+119
View File
@@ -0,0 +1,119 @@
#!/bin/sh /etc/rc.common
# quickstart-scope-context: 1
START=91
STOP=15
USE_PROCD=1
PROG=/usr/bin/quickstart-netpolicy
service_triggers() {
procd_add_reload_trigger quickstart-netpolicy
config_load quickstart-netpolicy
config_foreach add_network_trigger quickstart-netpolicy
}
add_network_trigger() {
local network enabled
config_get_bool enabled "$1" enabled 0
[ "$enabled" -eq 1 ] || return 0
config_get network "$1" network
[ "$1" != main ] || network="${network:-lan}"
[ -z "$network" ] || procd_add_reload_interface_trigger "$network"
}
instance_error() {
logger -t quickstart-netpolicy "instance $1: $2"
}
# Validate the whole enabled set before opening any instance. Duplicate resources
# invalidate both entries; configuration order must not choose the winning LAN.
collect_instance() {
local section="$1" enabled network interface port data_dir log_level history dns other
config_get_bool enabled "$section" enabled 0
[ "$enabled" -eq 1 ] || return 0
config_get network "$section" network
config_get interface "$section" interface
config_get port "$section" port
config_get data_dir "$section" data_dir
config_get log_level "$section" log_level info
config_get_bool history "$section" traffic_history 0
config_get_bool dns "$section" dns_monitoring 0
if [ "$section" = main ]; then
network="${network:-lan}"
interface="${interface:-br-lan}"
port="${port:-8765}"
data_dir="${data_dir:-/usr/share/quickstart-netpolicy}"
fi
case "$section" in ''|*[!A-Za-z0-9_-]*) instance_error "$section" 'invalid instance identity'; return 0;; esac
case "$network" in ''|*[!A-Za-z0-9_-]*) instance_error "$section" 'invalid network identity'; return 0;; esac
case "$interface" in ''|*[!A-Za-z0-9_.-]*) instance_error "$section" 'invalid interface'; return 0;; esac
case "$port" in ''|*[!0-9]*) instance_error "$section" 'port is required'; return 0;; esac
port="$(expr "$port" + 0)"
[ "$port" -gt 0 ] && [ "$port" -le 65535 ] || { instance_error "$section" 'invalid port'; return 0; }
case "$data_dir" in /*) ;; *) instance_error "$section" 'absolute data_dir is required'; return 0;; esac
case "$data_dir" in *'|'*|*'
'*) instance_error "$section" 'invalid data_dir'; return 0;; esac
case "$log_level" in trace|debug|info|warn|error) ;; *) instance_error "$section" 'invalid log level'; return 0;; esac
local canonical
canonical="$(readlink -f "$data_dir" 2>/dev/null)"
[ -z "$canonical" ] || data_dir="$canonical"
local IFS='
'
for other in $instance_records; do
local old_section old_network old_interface old_port old_dir rest
old_section="${other%%|*}"; rest="${other#*|}"
old_network="${rest%%|*}"; rest="${rest#*|}"
old_interface="${rest%%|*}"; rest="${rest#*|}"
old_port="${rest%%|*}"; rest="${rest#*|}"
old_dir="${rest%%|*}"
if [ "$network" = "$old_network" ] || [ "$interface" = "$old_interface" ] || [ "$port" = "$old_port" ] || [ "$data_dir" = "$old_dir" ]; then
invalid_instances="$invalid_instances $section $old_section"
instance_error "$section" "duplicate network/interface/port/data_dir with $old_section"
fi
done
instance_records="${instance_records}${instance_records:+
}$section|$network|$interface|$port|$data_dir|$log_level|$history|$dns"
}
start_instance() {
local section="$1" network interface port data_dir log_level history dns status actual
case " $invalid_instances " in *" $section "*) return 0;; esac
config_get network "$section" network lan
config_get interface "$section" interface br-lan
config_get port "$section" port 8765
config_get data_dir "$section" data_dir /usr/share/quickstart-netpolicy
config_get log_level "$section" log_level info
config_get_bool history "$section" traffic_history 0
config_get_bool dns "$section" dns_monitoring 0
status="$(ubus -S call "network.interface.$network" status 2>/dev/null)" || { instance_error "$section" 'network unavailable'; return 0; }
actual="$(printf '%s' "$status" | jsonfilter -e '@.l3_device')"
[ "$actual" = "$interface" ] || { instance_error "$section" 'interface does not match logical network'; return 0; }
[ -d "/sys/class/net/$interface" ] || { instance_error "$section" 'interface unavailable'; return 0; }
procd_open_instance "$section"
procd_set_param command "$binary" --iface "$interface" --port "$port" --data-dir "$data_dir" --network-id "$network" --instance-id "$section" --log-level "$log_level" --enable-traffic
[ "$history" -eq 1 ] && procd_append_param command --traffic-enable-storage
[ "$dns" -eq 1 ] && procd_append_param command --enable-dns
procd_set_param netdev "$interface"
procd_set_param respawn 3600 5 3
procd_set_param term_timeout 10
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param limits core="0 0"
procd_close_instance
}
start_service() {
local instance_records='' invalid_instances='' record binary="$PROG"
[ -x "$binary" ] || { instance_error main "missing executable $binary"; return 1; }
config_load quickstart-netpolicy
config_foreach collect_instance quickstart-netpolicy
local IFS='
'
for record in $instance_records; do start_instance "${record%%|*}"; done
}
# procd compares named instance commands, so unchanged networks keep their
# processes and TC links. Do not implement reload as stop/start.
reload_service() {
rc_procd start_service
}
+30 -3
View File
@@ -10,19 +10,26 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_quickstart:=$(ARCH)
PKG_NAME:=quickstart
PKG_VERSION:=0.13.1
PKG_RELEASE:=6
PKG_VERSION:=0.17.1-rc20261028
PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/linkease/istore-packages/releases/download/prebuilt/
PKG_SOURCE_URL:=https://github.com/istoreos/quickstart/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)
ifeq ($(CONFIG_USE_APK),y)
PKG_VERSION:=$(subst -rc,_rc,$(PKG_VERSION))
endif
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
include $(INCLUDE_DIR)/package.mk
# Runtime manifests cover these prebuilt bytes; do not strip them again.
RSTRIP:=:
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
@@ -39,6 +46,23 @@ endef
define Package/$(PKG_NAME)/conffiles
/etc/config/quickstart
/etc/quickstart/home-layout.json
/etc/quickstart/home-layout.json.bak
/etc/quickstart/home-layout.json.damaged
/etc/quickstart/home-layout.json.bak.damaged
/etc/quickstart/home-launcher.json
/etc/quickstart/home-launcher.json.bak
/etc/quickstart/home-launcher.json.damaged
/etc/quickstart/home-launcher.json.bak.damaged
/etc/quickstart/device-classifications.json
/etc/quickstart/device-profiles.json
/etc/quickstart/manual-devices.json
/etc/quickstart/device-groups.json
/etc/quickstart/traffic-insights.json
/etc/quickstart/device-policy-effects-v1.json
/etc/quickstart/task-transactions-v1.json
/etc/quickstart/lan-device-model-v1.json
/etc/quickstart/network-audit.json
endef
define Package/$(PKG_NAME)/postinst
@@ -71,7 +95,10 @@ define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
$(INSTALL_BIN) ./files/startdhns.hotplug $(1)/etc/hotplug.d/iface/21-startdhns
$(INSTALL_DIR) $(1)/usr/share/quickstart
$(INSTALL_DATA) $(PKG_BUILD_DIR)/MANIFEST $(1)/usr/share/quickstart/MANIFEST
$(INSTALL_DATA) ./files/manuf $(1)/usr/share/quickstart/manuf
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
$(INSTALL_DATA) ./files/quickstart.keep $(1)/lib/upgrade/keep.d/quickstart
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
+8
View File
@@ -0,0 +1,8 @@
/etc/quickstart/home-layout.json
/etc/quickstart/home-layout.json.bak
/etc/quickstart/home-layout.json.damaged
/etc/quickstart/home-layout.json.bak.damaged
/etc/quickstart/home-launcher.json
/etc/quickstart/home-launcher.json.bak
/etc/quickstart/home-launcher.json.damaged
/etc/quickstart/home-launcher.json.bak.damaged
+2 -2
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya-core
PKG_VERSION:=2.5.14
PKG_RELEASE:=1
PKG_VERSION:=2.5.15
PKG_RELEASE:=2
PKG_SOURCE:=v2rayA-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -13,8 +13,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya
PKG_VERSION:=2.5.14
PKG_RELEASE:=8
PKG_VERSION:=2.5.15
PKG_RELEASE:=9
PKG_SOURCE:=v2rayA-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
+3 -3
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_VERSION:=1.9.0_pre14
PKG_RELEASE:=92
PKG_VERSION:=1.9.0_pre20
PKG_RELEASE:=93
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=4f3c54b1eeb65d898246545435ca2300e57ac30e
PKG_SOURCE_VERSION:=b646c9ec93928e8a8e57356551cfa7717d7171b6
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only