mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 21:15:13 +08:00
✨ Sync 2026-06-07 00:43:45
This commit is contained in:
1 parent
75dec81517
commit
85f7b195f8
18 files changed
+4294
-2
No files matched your search
@@ -0,0 +1,110 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.5
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
choice
|
||||
prompt "Backend dependency"
|
||||
default PACKAGE_$(PKG_NAME)_daed
|
||||
help
|
||||
Select which backend package luci-app-daede should pull in by default.
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_daed
|
||||
bool "daed backend"
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_dae
|
||||
bool "dae backend"
|
||||
|
||||
endchoice
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LuCI app for daede dashboard
|
||||
DEPENDS:=+luci-base +PACKAGE_$(PKG_NAME)_dae:dae +PACKAGE_$(PKG_NAME)_daed:daed
|
||||
PKGARCH:=all
|
||||
MAINTAINER:=kenzok8
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
LuCI web interface for managing dae or daed backends.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
po2lmo ./po/zh-cn/daede.po $(PKG_BUILD_DIR)/daede.zh-cn.lmo
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/daede
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DATA) ./root/etc/config/daede $(1)/etc/config/daede
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./root/etc/uci-defaults/90-luci-app-daede-init $(1)/etc/uci-defaults/90-luci-app-daede-init
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-daede.json $(1)/usr/share/luci/menu.d/luci-app-daede.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-daede.json $(1)/usr/share/rpcd/acl.d/luci-app-daede.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci-app-daede
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/pkg-info.sh $(1)/usr/share/luci-app-daede/pkg-info.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/update-geo.sh $(1)/usr/share/luci-app-daede/update-geo.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/update-pkg.sh $(1)/usr/share/luci-app-daede/update-pkg.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/gen-dae-config.sh $(1)/usr/share/luci-app-daede/gen-dae-config.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/*.js $(1)/www/luci-static/resources/view/daede/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/daede.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/daede.zh-cn.lmo
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
if [ -x /etc/uci-defaults/90-luci-app-daede-init ]; then
|
||||
( . /etc/uci-defaults/90-luci-app-daede-init )
|
||||
rm -f /etc/uci-defaults/90-luci-app-daede-init
|
||||
fi
|
||||
rm -f /usr/lib/lua/luci/i18n/daed.zh-cn.lmo
|
||||
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
|
||||
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,137 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const BACKENDS = {
|
||||
daed: {
|
||||
name: 'daed',
|
||||
label: 'daed',
|
||||
uci: 'daed',
|
||||
initd: '/etc/init.d/daed',
|
||||
log: '/var/log/daed/daed.log',
|
||||
pkg: 'daed',
|
||||
hasWebUI: true,
|
||||
useNetns: true,
|
||||
defaultListen: '0.0.0.0:2023'
|
||||
},
|
||||
dae: {
|
||||
name: 'dae',
|
||||
label: 'dae',
|
||||
uci: 'dae',
|
||||
initd: '/etc/init.d/dae',
|
||||
log: '/var/log/dae/dae.log',
|
||||
pkg: 'dae',
|
||||
config: '/etc/dae/config.dae',
|
||||
example: '/etc/dae/example.dae',
|
||||
hasWebUI: false,
|
||||
useNetns: false
|
||||
}
|
||||
};
|
||||
|
||||
function execOk(cmd, args) {
|
||||
return L.resolveDefault(fs.exec(cmd, args || []), { code: 1 }).then(function(res) {
|
||||
return res && res.code === 0;
|
||||
});
|
||||
}
|
||||
|
||||
function exists(path) {
|
||||
return L.resolveDefault(fs.stat(path), null).then(function(st) {
|
||||
return !!st;
|
||||
});
|
||||
}
|
||||
|
||||
function serviceStatus(name) {
|
||||
return L.resolveDefault(callServiceList(name), {}).then(function(svc) {
|
||||
let pid = 0, running = false;
|
||||
|
||||
try {
|
||||
const inst = svc[name].instances[name];
|
||||
running = !!inst.running;
|
||||
pid = inst.pid || 0;
|
||||
} catch (e) {}
|
||||
|
||||
return { running: running, pid: pid };
|
||||
});
|
||||
}
|
||||
|
||||
function detectInstalledBackends() {
|
||||
return Promise.all([
|
||||
exists(BACKENDS.dae.initd),
|
||||
exists(BACKENDS.daed.initd)
|
||||
]).then(function(r) {
|
||||
return { dae: r[0], daed: r[1] };
|
||||
});
|
||||
}
|
||||
|
||||
function detectRunning() {
|
||||
return Promise.all([
|
||||
execOk('/usr/bin/pgrep', ['-x', 'dae']),
|
||||
execOk('/usr/bin/pgrep', ['-x', 'daed'])
|
||||
]).then(function(r) {
|
||||
return { dae: r[0], daed: r[1] };
|
||||
});
|
||||
}
|
||||
|
||||
function loadActiveBackend() {
|
||||
return L.resolveDefault(uci.load('daede'), null).then(function() {
|
||||
const active = uci.get('daede', 'config', 'active_backend');
|
||||
return BACKENDS[active] ? active : '';
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveBackend(name) {
|
||||
if (!BACKENDS[name])
|
||||
return Promise.reject(new Error('invalid backend: ' + name));
|
||||
|
||||
return exists('/etc/config/daede').then(function(ok) {
|
||||
if (!ok)
|
||||
return fs.write('/etc/config/daede', '');
|
||||
}).then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config=daede']); })
|
||||
.then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config.active_backend=' + name]); })
|
||||
.then(function() { return fs.exec('/sbin/uci', ['commit', 'daede']); });
|
||||
}
|
||||
|
||||
function detectBackend() {
|
||||
return Promise.all([
|
||||
detectInstalledBackends(),
|
||||
detectRunning(),
|
||||
loadActiveBackend()
|
||||
]).then(function(r) {
|
||||
const installed = r[0];
|
||||
const running = r[1];
|
||||
const preferred = r[2];
|
||||
let name = preferred;
|
||||
|
||||
if (!name || !installed[name])
|
||||
name = installed.daed ? 'daed' : (installed.dae ? 'dae' : 'daed');
|
||||
|
||||
return {
|
||||
name: name,
|
||||
backend: BACKENDS[name],
|
||||
installed: installed,
|
||||
running: running,
|
||||
preferred: preferred,
|
||||
bothInstalled: !!(installed.dae && installed.daed)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
BACKENDS: BACKENDS,
|
||||
detectInstalledBackends: detectInstalledBackends,
|
||||
detectRunning: detectRunning,
|
||||
detectBackend: detectBackend,
|
||||
setActiveBackend: setActiveBackend,
|
||||
serviceStatus: serviceStatus
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,305 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
|
||||
const MAX_LINES = 5000;
|
||||
|
||||
const CSS = [
|
||||
'.dd-log-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-log-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:10px 14px;margin-bottom:10px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
'.dd-log-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:0 0 8px;margin-bottom:8px;border-bottom:1px dashed rgba(128,128,128,.2)}',
|
||||
'.dd-log-toolbar label{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;cursor:pointer;margin:0;opacity:.85}',
|
||||
'.dd-log-toolbar input[type="checkbox"]{margin:0}',
|
||||
'.dd-log-toolbar input[type="text"]{font-size:11.5px;padding:4px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;min-width:160px}',
|
||||
'.dd-log-toolbar .dd-log-btn{font-size:11.5px;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;cursor:pointer}',
|
||||
'.dd-log-toolbar .dd-log-btn:hover{background:rgba(128,128,128,.1)}',
|
||||
'.dd-log-toolbar .dd-log-meta{margin-left:auto;font-size:10.5px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
|
||||
'.dd-log-pane{height:60vh;min-height:360px;overflow:auto;padding:8px 10px;border:1px solid rgba(0,0,0,.08);border-radius:7px;background:#1a1d21;color:#d8dde6;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-all}',
|
||||
'.dd-log-pane .dd-line{padding:1px 4px;border-radius:3px;display:block}',
|
||||
'.dd-log-pane .dd-line.dd-info{color:#cfd6df}',
|
||||
'.dd-log-pane .dd-line.dd-warn{color:#e8b95a}',
|
||||
'.dd-log-pane .dd-line.dd-error{color:#ea7878;background:rgba(234,120,120,.06)}',
|
||||
'.dd-log-pane .dd-line.dd-debug{color:#7a8290;opacity:.7}',
|
||||
'.dd-log-pane .dd-line.dd-hidden{display:none}',
|
||||
'.dd-log-pane .dd-empty{opacity:.5;font-style:italic}',
|
||||
/* 简化后的字段视觉 */
|
||||
'.dd-log-pane .dd-ts{color:#6b7480;margin-right:8px}',
|
||||
'.dd-log-pane .dd-lvl{display:inline-block;min-width:38px;padding:0 5px;margin-right:8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:.4px;text-align:center;vertical-align:1px}',
|
||||
'.dd-log-pane .dd-lvl-info{color:#7fc7a8;background:rgba(127,199,168,.08)}',
|
||||
'.dd-log-pane .dd-lvl-warn{color:#e8b95a;background:rgba(232,185,90,.10)}',
|
||||
'.dd-log-pane .dd-lvl-error{color:#ea7878;background:rgba(234,120,120,.10)}',
|
||||
'.dd-log-pane .dd-lvl-debug{color:#7a8290;background:rgba(122,130,144,.10)}',
|
||||
'.dd-log-pane .dd-msg{color:inherit}',
|
||||
'.dd-log-pane .dd-kv{margin-left:6px;color:#6b7480;font-size:11px;opacity:.85}',
|
||||
'body.dark .dd-log-card,html[data-theme="dark"] .dd-log-card,html[data-bs-theme="dark"] .dd-log-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}',
|
||||
'body.dark .dd-log-pane,html[data-theme="dark"] .dd-log-pane,html[data-bs-theme="dark"] .dd-log-pane{border-color:rgba(255,255,255,.1)}'
|
||||
].join('');
|
||||
|
||||
/* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */
|
||||
const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/;
|
||||
|
||||
function detectLevel(line) {
|
||||
// daed/dae logs use lvl=info / [INFO] / level=warning style
|
||||
const m = line.match(/\b(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i);
|
||||
if (!m) return '';
|
||||
const lvl = m[1].toUpperCase();
|
||||
if (lvl === 'DEBUG') return 'dd-debug';
|
||||
if (lvl === 'INFO') return 'dd-info';
|
||||
if (lvl.startsWith('WARN')) return 'dd-warn';
|
||||
return 'dd-error';
|
||||
}
|
||||
|
||||
/* 简化时间戳并按本地时区显示:
|
||||
* - daed 默认输出 UTC ISO 8601 (e.g. "2026-05-28T19:07:54Z"),转成本地时间
|
||||
* - 旧 logrus 短格式 (e.g. "May 25 07:04:59") 无时区信息,原样提取
|
||||
*/
|
||||
function shortTs(raw) {
|
||||
if (!raw) return raw;
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(raw)) {
|
||||
const d = new Date(raw);
|
||||
if (!isNaN(d.getTime())) {
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
||||
}
|
||||
}
|
||||
const m = raw.match(/(\d{2}:\d{2}:\d{2})/);
|
||||
return m ? m[1] : raw;
|
||||
}
|
||||
|
||||
function lvlShort(level) {
|
||||
const u = (level || '').toUpperCase();
|
||||
if (u === 'WARNING') return 'WARN';
|
||||
return u || '-';
|
||||
}
|
||||
|
||||
function lvlClass(level) {
|
||||
const u = (level || '').toUpperCase();
|
||||
if (u === 'DEBUG') return 'dd-lvl-debug';
|
||||
if (u === 'INFO') return 'dd-lvl-info';
|
||||
if (u === 'WARN' || u === 'WARNING') return 'dd-lvl-warn';
|
||||
return 'dd-lvl-error';
|
||||
}
|
||||
|
||||
function buildLine(ln) {
|
||||
const cls = detectLevel(ln);
|
||||
const m = ln.match(RE_LINE);
|
||||
if (!m) {
|
||||
return E('div', { 'class': 'dd-line ' + cls }, ln);
|
||||
}
|
||||
const ts = shortTs(m[1]);
|
||||
const lvl = m[2];
|
||||
const msg = m[3] !== undefined ? m[3] : (m[4] || '');
|
||||
const kv = (m[5] || '').trim();
|
||||
const parts = [
|
||||
E('span', { 'class': 'dd-ts' }, ts),
|
||||
E('span', { 'class': 'dd-lvl ' + lvlClass(lvl) }, lvlShort(lvl)),
|
||||
E('span', { 'class': 'dd-msg' }, msg)
|
||||
];
|
||||
if (kv) parts.push(E('span', { 'class': 'dd-kv' }, kv));
|
||||
return E('div', { 'class': 'dd-line ' + cls }, parts);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return backend.detectBackend();
|
||||
},
|
||||
|
||||
render(ctx) {
|
||||
const self = this;
|
||||
const LOG_PATH = ctx.backend.log;
|
||||
const state = {
|
||||
lastContent: '',
|
||||
lastSize: -1,
|
||||
paused: false,
|
||||
autoScroll: true,
|
||||
filter: '',
|
||||
userScrolled: false
|
||||
};
|
||||
|
||||
const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [
|
||||
E('div', { 'class': 'dd-empty' }, _('Loading…'))
|
||||
]);
|
||||
|
||||
// detect manual scroll → auto-pause auto-scroll
|
||||
pane.addEventListener('scroll', function() {
|
||||
const atBottom = pane.scrollHeight - pane.scrollTop - pane.clientHeight < 4;
|
||||
state.userScrolled = !atBottom;
|
||||
});
|
||||
|
||||
const meta = E('span', { 'class': 'dd-log-meta' }, '');
|
||||
|
||||
const cbAuto = E('input', { 'type': 'checkbox', 'checked': 'checked' });
|
||||
cbAuto.addEventListener('change', function() {
|
||||
state.autoScroll = cbAuto.checked;
|
||||
if (state.autoScroll) {
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
state.userScrolled = false;
|
||||
}
|
||||
});
|
||||
|
||||
const cbPause = E('input', { 'type': 'checkbox' });
|
||||
cbPause.addEventListener('change', function() {
|
||||
state.paused = cbPause.checked;
|
||||
});
|
||||
|
||||
const selFilter = E('select', { 'class': 'dd-log-btn' }, [
|
||||
E('option', { 'value': '' }, _('All')),
|
||||
E('option', { 'value': 'info' }, 'INFO'),
|
||||
E('option', { 'value': 'warn' }, 'WARN'),
|
||||
E('option', { 'value': 'error' }, 'ERROR'),
|
||||
E('option', { 'value': 'alive' }, _('Node Status')),
|
||||
E('option', { 'value': 'my_group' }, _('Proxy Traffic'))
|
||||
]);
|
||||
selFilter.addEventListener('change', function() {
|
||||
state.filter = selFilter.value;
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
const btnClear = E('button', { 'class': 'dd-log-btn' }, _('Clear View'));
|
||||
btnClear.addEventListener('click', function() {
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
state.lastContent = '';
|
||||
});
|
||||
|
||||
const btnDownload = E('button', { 'class': 'dd-log-btn' }, _('Download'));
|
||||
btnDownload.addEventListener('click', function() {
|
||||
const blob = new Blob([state.lastContent || ''], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = ctx.name + '-' + (new Date()).toISOString().replace(/[:.]/g, '-') + '.log';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
const btnTruncate = E('button', { 'class': 'dd-log-btn' }, _('Clear File'));
|
||||
btnTruncate.addEventListener('click', function() {
|
||||
if (!confirm(_('Truncate %s log on the router? This cannot be undone.').format(ctx.name)))
|
||||
return;
|
||||
fs.write(LOG_PATH, '').then(function() {
|
||||
state.lastContent = '';
|
||||
state.lastSize = 0;
|
||||
}).catch(function() {});
|
||||
});
|
||||
|
||||
function applyFilter() {
|
||||
const f = state.filter;
|
||||
pane.querySelectorAll('.dd-line').forEach(function(el) {
|
||||
if (!f || el.textContent.toLowerCase().indexOf(f) !== -1)
|
||||
el.classList.remove('dd-hidden');
|
||||
else
|
||||
el.classList.add('dd-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function appendLines(text) {
|
||||
const lines = text.split('\n');
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i];
|
||||
if (!ln) continue;
|
||||
const el = buildLine(ln);
|
||||
if (state.filter && ln.toLowerCase().indexOf(state.filter) === -1)
|
||||
el.classList.add('dd-hidden');
|
||||
frag.appendChild(el);
|
||||
}
|
||||
// drop empty-state placeholder if present
|
||||
const empty = pane.querySelector('.dd-empty');
|
||||
if (empty) pane.removeChild(empty);
|
||||
pane.appendChild(frag);
|
||||
|
||||
// cap rendered rows
|
||||
const overflow = pane.children.length - MAX_LINES;
|
||||
if (overflow > 0) {
|
||||
for (let i = 0; i < overflow; i++) pane.removeChild(pane.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmpty(msg) {
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
pane.appendChild(E('div', { 'class': 'dd-empty' }, msg));
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (state.paused) return Promise.resolve();
|
||||
|
||||
return fs.stat(LOG_PATH).then(function(st) {
|
||||
const size = st.size || 0;
|
||||
if (size === state.lastSize) {
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
return;
|
||||
}
|
||||
|
||||
// File rotated/truncated → full reload
|
||||
const rotated = size < state.lastSize;
|
||||
return fs.read_direct(LOG_PATH, 'text').then(function(content) {
|
||||
content = content || '';
|
||||
let delta;
|
||||
if (rotated || state.lastContent === '') {
|
||||
// full replace
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
delta = content;
|
||||
} else if (content.indexOf(state.lastContent) === 0) {
|
||||
// pure append
|
||||
delta = content.slice(state.lastContent.length);
|
||||
} else {
|
||||
// content changed mid-stream → full replace
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
delta = content;
|
||||
}
|
||||
|
||||
if (delta) appendLines(delta);
|
||||
state.lastContent = content;
|
||||
state.lastSize = size;
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
|
||||
if (state.autoScroll && !state.userScrolled)
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
});
|
||||
}).catch(function(e) {
|
||||
const msg = String(e);
|
||||
if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1)
|
||||
renderEmpty(_('Log file does not exist yet.'));
|
||||
else
|
||||
renderEmpty(_('Error reading log: %s').format(msg));
|
||||
state.lastSize = -1;
|
||||
state.lastContent = '';
|
||||
meta.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
poll.add(tick);
|
||||
tick();
|
||||
|
||||
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, [
|
||||
E('label', {}, [ cbAuto, _('Auto-scroll') ]),
|
||||
E('label', {}, [ cbPause, _('Pause') ]),
|
||||
selFilter,
|
||||
btnClear,
|
||||
btnDownload,
|
||||
btnTruncate,
|
||||
meta
|
||||
]);
|
||||
|
||||
return E('div', { 'class': 'dd-log-wrap' }, [
|
||||
E('style', {}, CSS),
|
||||
E('div', { 'class': 'dd-log-card' }, [
|
||||
E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)),
|
||||
toolbar,
|
||||
pane
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
|
||||
const DATA_PATHS = {
|
||||
geoip: '/usr/share/v2ray/geoip.dat',
|
||||
geosite: '/usr/share/v2ray/geosite.dat'
|
||||
};
|
||||
|
||||
const HEALTH_PATHS = {
|
||||
btf: '/sys/kernel/btf/vmlinux',
|
||||
netns: '/run/netns/daens'
|
||||
};
|
||||
|
||||
const CSS = [
|
||||
'.dd-up-wrap{padding:6px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
'.dd-card-title{font-size:12px;font-weight:600;opacity:.55;margin:0 0 10px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-up-row{display:grid;grid-template-columns:24px 130px 1fr auto auto;gap:10px;align-items:center;font-size:12px;padding:7px 0;border-top:1px dashed rgba(128,128,128,.18)}',
|
||||
'.dd-up-row:first-of-type{border-top:0}',
|
||||
'.dd-up-icon{font-size:14px;text-align:center;line-height:1}',
|
||||
'.dd-up-ok{color:#3da66a}',
|
||||
'.dd-up-warn{color:#d39e00}',
|
||||
'.dd-up-err{color:#d96d6d}',
|
||||
'.dd-up-name{font-weight:600;opacity:.85}',
|
||||
'.dd-up-meta{opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
'.dd-up-version{font-size:11px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap}',
|
||||
'.dd-up-btn{font-size:11px;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;white-space:nowrap}',
|
||||
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
|
||||
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}',
|
||||
'.dd-up-log{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;padding:10px;border:1px solid rgba(128,128,128,.14);border-radius:8px;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;display:none;background:inherit;color:#4a8c63}',
|
||||
'.dd-up-log.show{display:block}',
|
||||
'body.dark .dd-up-log,html[data-theme="dark"] .dd-up-log,html[data-bs-theme="dark"] .dd-up-log{color:#a3d9ad}',
|
||||
/* 手风琴折叠组 —— 与 config.js dd-adv 同构 */
|
||||
'.dd-adv{margin-bottom:14px}',
|
||||
'.dd-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;cursor:pointer;user-select:none;font-size:11.5px;font-weight:600;background:rgba(128,128,128,.04);border:1px solid rgba(0,0,0,.06);border-radius:10px;color:inherit;letter-spacing:.3px;text-transform:uppercase;opacity:.7;box-shadow:0 2px 8px rgba(0,0,0,.03)}',
|
||||
'.dd-adv-bar:hover{background:rgba(56,134,161,.06);opacity:.95}',
|
||||
'.dd-adv-chevron{font-size:14px;font-weight:700;opacity:.55;transition:transform .2s;text-transform:none}',
|
||||
'.dd-adv:not(.dd-closed) .dd-adv-chevron{transform:rotate(90deg)}',
|
||||
'.dd-adv-body{margin-top:8px;padding:10px 14px;border:1px solid rgba(0,0,0,.06);border-radius:10px;background:rgba(255,255,255,.02);box-shadow:0 2px 8px rgba(0,0,0,.03)}',
|
||||
'.dd-adv.dd-closed{margin-bottom:14px}',
|
||||
'.dd-adv.dd-closed .dd-adv-body{display:none}',
|
||||
'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,body.dark .dd-adv-bar,body.dark .dd-adv-body,html[data-theme="dark"] .dd-adv-bar,html[data-theme="dark"] .dd-adv-body,html[data-bs-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-body{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}'
|
||||
].join('');
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (!n && n !== 0) return '-';
|
||||
if (n < 1024) return n + ' B';
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
return (n / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function fmtMtime(epoch) {
|
||||
if (!epoch) return '';
|
||||
const d = new Date(epoch * 1000);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function probeFile(path) {
|
||||
return L.resolveDefault(fs.stat(path), null).then(function(st) {
|
||||
return { exists: !!st, size: st ? st.size : 0, mtime: st ? st.mtime : 0 };
|
||||
});
|
||||
}
|
||||
|
||||
function probePkg(pkg) {
|
||||
return fs.exec('/usr/share/luci-app-daede/pkg-info.sh', [pkg]).then(function(res) {
|
||||
const out = (res.stdout || '').trim().split('\t');
|
||||
return { installed: out[0] || '', latest: out[1] || '' };
|
||||
}).catch(function() {
|
||||
return { installed: '', latest: '' };
|
||||
});
|
||||
}
|
||||
|
||||
function tailLog(path, into) {
|
||||
return L.resolveDefault(fs.read_direct(path, 'text'), '').then(function(content) {
|
||||
if (content) {
|
||||
into.textContent = content;
|
||||
into.classList.add('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
backend.detectBackend(),
|
||||
uci.load('daed').catch(function() {})
|
||||
]).then(function(r) {
|
||||
return r[0];
|
||||
});
|
||||
},
|
||||
|
||||
render: function(ctx) {
|
||||
const logPane = E('pre', { 'class': 'dd-up-log', 'id': 'dd-up-log' }, '');
|
||||
const corePkgs = [];
|
||||
const addCorePkg = function(name) {
|
||||
if (corePkgs.indexOf(name) === -1)
|
||||
corePkgs.push(name);
|
||||
};
|
||||
addCorePkg(ctx.name);
|
||||
addCorePkg('daed');
|
||||
addCorePkg('dae');
|
||||
|
||||
const dataBody = E('div', { 'id': 'dd-up-data' }, E('em', {}, _('Probing…')));
|
||||
const pkgBody = E('div', { 'id': 'dd-up-pkg' }, E('em', {}, _('Probing…')));
|
||||
const healthBody = E('div', { 'id': 'dd-up-health' }, E('em', {}, _('Probing…')));
|
||||
|
||||
const mkRow = function(icon, iconCls, name, meta, btn) {
|
||||
return E('div', { 'class': 'dd-up-row' }, [
|
||||
E('span', { 'class': 'dd-up-icon ' + iconCls }, icon),
|
||||
E('span', { 'class': 'dd-up-name' }, name),
|
||||
E('span', { 'class': 'dd-up-meta', 'title': meta }, meta),
|
||||
E('span', {}, ''),
|
||||
btn || E('span', {}, '')
|
||||
]);
|
||||
};
|
||||
|
||||
// === Data updates (GeoIP / GeoSite) ===
|
||||
const updateGeo = function(kind, btn) {
|
||||
const label = kind === 'geoip' ? 'GeoIP' : 'GeoSite';
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
return fs.exec('/usr/share/luci-app-daede/update-geo.sh', [kind]).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
setTimeout(function() { tailLog('/tmp/luci-app-daede.' + kind + '.log', logPane); }, 2000);
|
||||
}
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Update');
|
||||
});
|
||||
};
|
||||
|
||||
// === Package updates (dae|daed / luci-app-daede) ===
|
||||
const upgradePkg = function(pkg, btn) {
|
||||
if (!confirm(_('Run "apk upgrade %s" now? This may restart the active backend.').format(pkg)))
|
||||
return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
return fs.exec('/usr/share/luci-app-daede/update-pkg.sh', [pkg]).then(function(res) {
|
||||
if (res.code === 0) {
|
||||
setTimeout(function() { tailLog('/tmp/luci-app-daede.pkg-' + pkg + '.log', logPane); }, 3000);
|
||||
}
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Upgrade');
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = function() {
|
||||
const probes = [
|
||||
probeFile(DATA_PATHS.geoip),
|
||||
probeFile(DATA_PATHS.geosite),
|
||||
probeFile(HEALTH_PATHS.btf),
|
||||
backend.detectRunning(),
|
||||
backend.serviceStatus(ctx.name)
|
||||
];
|
||||
|
||||
corePkgs.forEach(function(pkg) {
|
||||
probes.push(probePkg(pkg));
|
||||
});
|
||||
probes.push(probePkg('luci-app-daede'));
|
||||
|
||||
if (ctx.backend.useNetns)
|
||||
probes.push(probeFile(HEALTH_PATHS.netns));
|
||||
|
||||
return Promise.all(probes).then(function(r) {
|
||||
const geoip = r[0], geosite = r[1], btf = r[2], running = r[3], service = r[4];
|
||||
const pkgOffset = 5;
|
||||
const coreInfo = {};
|
||||
corePkgs.forEach(function(pkg, i) {
|
||||
coreInfo[pkg] = r[pkgOffset + i];
|
||||
});
|
||||
const luci = r[pkgOffset + corePkgs.length];
|
||||
const ns = r[pkgOffset + corePkgs.length + 1];
|
||||
|
||||
// data rows
|
||||
while (dataBody.firstChild) dataBody.removeChild(dataBody.firstChild);
|
||||
[
|
||||
{ k: 'geoip', name: 'GeoIP', r: geoip },
|
||||
{ k: 'geosite', name: 'GeoSite', r: geosite }
|
||||
].forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Update'));
|
||||
btn.addEventListener('click', function() { updateGeo(entry.k, btn); });
|
||||
const meta = entry.r.exists
|
||||
? fmtBytes(entry.r.size) + (entry.r.mtime ? ' · ' + fmtMtime(entry.r.mtime) : '')
|
||||
: _('missing — click Update to fetch');
|
||||
dataBody.appendChild(mkRow(
|
||||
entry.r.exists ? '✓' : '✗',
|
||||
entry.r.exists ? 'dd-up-ok' : 'dd-up-err',
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
|
||||
// pkg rows
|
||||
while (pkgBody.firstChild) pkgBody.removeChild(pkgBody.firstChild);
|
||||
corePkgs.map(function(pkg) {
|
||||
const label = _('%s binary').format(pkg) + (ctx.name === pkg ? ' · ' + _('active') : '');
|
||||
return { k: pkg, name: label, r: coreInfo[pkg] };
|
||||
}).concat([
|
||||
{ k: 'luci-app-daede', name: 'luci-app-daede', r: luci }
|
||||
]).forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Upgrade'));
|
||||
btn.addEventListener('click', function() { upgradePkg(entry.k, btn); });
|
||||
const sameVersion = entry.r.installed && entry.r.latest && entry.r.installed === entry.r.latest;
|
||||
const updatable = entry.r.installed && entry.r.latest && entry.r.installed !== entry.r.latest;
|
||||
let meta;
|
||||
if (!entry.r.installed) {
|
||||
meta = _('not installed via package manager');
|
||||
btn.disabled = true;
|
||||
btn.textContent = _('Unavailable');
|
||||
} else if (sameVersion) {
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('up to date');
|
||||
} else if (updatable) {
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' → ' + _('latest') + ': ' + entry.r.latest;
|
||||
} else {
|
||||
meta = _('installed') + ': ' + entry.r.installed + (entry.r.latest ? ' · ' + _('latest') + ': ' + entry.r.latest : '');
|
||||
}
|
||||
pkgBody.appendChild(mkRow(
|
||||
updatable ? '⚠' : (entry.r.installed ? '✓' : '✗'),
|
||||
updatable ? 'dd-up-warn' : (entry.r.installed ? 'dd-up-ok' : 'dd-up-err'),
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
|
||||
// health rows
|
||||
while (healthBody.firstChild) healthBody.removeChild(healthBody.firstChild);
|
||||
healthBody.appendChild(mkRow(
|
||||
running[ctx.name] ? '✓' : '✗',
|
||||
running[ctx.name] ? 'dd-up-ok' : 'dd-up-err',
|
||||
_('%s process').format(ctx.name),
|
||||
running[ctx.name] ? _('pgrep reports the backend is running') : _('backend process is not running')
|
||||
));
|
||||
|
||||
if (btf.exists)
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Kernel BTF'), HEALTH_PATHS.btf + ' · ' + fmtBytes(btf.size)));
|
||||
else
|
||||
healthBody.appendChild(mkRow('✗', 'dd-up-err', _('Kernel BTF'), _('not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf')));
|
||||
|
||||
if (ctx.backend.hasWebUI) {
|
||||
const listenAddr = uci.get('daed', 'config', 'listen_addr') || ctx.backend.defaultListen;
|
||||
const port = listenAddr.split(':').slice(-1)[0];
|
||||
healthBody.appendChild(mkRow(
|
||||
service.running ? '✓' : '✗',
|
||||
service.running ? 'dd-up-ok' : 'dd-up-err',
|
||||
_('WebUI/API'),
|
||||
service.running ? _('service is running, expected on port %s').format(port) : _('service is stopped')
|
||||
));
|
||||
} else {
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Hot reload'), _('/etc/init.d/dae hot_reload is available')));
|
||||
}
|
||||
|
||||
if (ctx.backend.useNetns && ns && ns.exists) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn' }, _('Clean'));
|
||||
btn.addEventListener('click', function() {
|
||||
btn.disabled = true;
|
||||
fs.exec('/sbin/ip', ['netns', 'del', 'daens']).finally(function() { btn.disabled = false; });
|
||||
});
|
||||
healthBody.appendChild(mkRow('⚠', 'dd-up-warn', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('exists, may block daed start'), btn));
|
||||
} else if (ctx.backend.useNetns) {
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('clean')));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
poll.add(refresh);
|
||||
refresh();
|
||||
|
||||
return E('div', { 'class': 'dd-up-wrap' }, [
|
||||
E('style', {}, CSS),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
|
||||
dataBody
|
||||
]),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
|
||||
pkgBody
|
||||
]),
|
||||
(function() {
|
||||
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, _('System Health')),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
E('div', { 'class': 'dd-adv-body' }, [ healthBody ])
|
||||
]);
|
||||
adv.firstChild.addEventListener('click', function() {
|
||||
adv.classList.toggle('dd-closed');
|
||||
});
|
||||
return adv;
|
||||
})(),
|
||||
logPane
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collecting data…"
|
||||
msgstr ""
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr ""
|
||||
|
||||
msgid "STOPPED"
|
||||
msgstr ""
|
||||
|
||||
msgid "Backend"
|
||||
msgstr ""
|
||||
|
||||
msgid "Listen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle service"
|
||||
msgstr ""
|
||||
|
||||
msgid "Open WebUI"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restart"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hot Reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active backend"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is not installed"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is not installed. Install it first, then switch backend."
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching backend stops the current service first. Click start when you want the new backend to run."
|
||||
msgstr ""
|
||||
|
||||
msgid "Stopping current backend…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching to %s…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switched to %s. Click start when you want it to run."
|
||||
msgstr ""
|
||||
|
||||
msgid "Backend switched to %s. Click start to run it."
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to switch backend: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action \"%s\" error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action \"%s\" failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action \"%s\" succeeded."
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
|
||||
msgstr ""
|
||||
|
||||
msgid "daede Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI."
|
||||
msgstr ""
|
||||
|
||||
msgid "Listen Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Host:port that the daed WebUI and GraphQL API listen on."
|
||||
msgstr ""
|
||||
|
||||
msgid "Max Log Size (MB)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rotate the log file once it grows past this many megabytes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Max Log Backups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Number of rotated log files to keep."
|
||||
msgstr ""
|
||||
|
||||
msgid "Log Advanced Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "dae Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "eBPF-based Linux high-performance transparent proxy solution."
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration File"
|
||||
msgstr ""
|
||||
|
||||
msgid "dae Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Text-only mode."
|
||||
msgstr ""
|
||||
|
||||
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
|
||||
msgstr ""
|
||||
|
||||
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save and Hot Reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Initialize from example"
|
||||
msgstr ""
|
||||
|
||||
msgid "+ Insert Block…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert config block at cursor"
|
||||
msgstr ""
|
||||
|
||||
msgid "lines"
|
||||
msgstr ""
|
||||
|
||||
msgid "⚠ %d placeholder"
|
||||
msgstr ""
|
||||
|
||||
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
|
||||
msgstr ""
|
||||
|
||||
msgid " / "
|
||||
msgstr ""
|
||||
|
||||
msgid "这些 "
|
||||
msgstr ""
|
||||
|
||||
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Placeholder lines (click line number to jump):"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jump to line"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validating…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validate failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saving…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reload failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validated · saved · hot-reloaded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading example…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Example loaded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Init failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log file does not exist yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Error reading log: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log file cleared."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to clear log: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Truncate %s log on the router? This cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Data Updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package Updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "System Health"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Upgrade"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clean"
|
||||
msgstr ""
|
||||
|
||||
msgid "missing — click Update to fetch"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s download started — will refresh in 1–3 min."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s update already running."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s update failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s update error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s binary"
|
||||
msgstr ""
|
||||
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
msgid "installed"
|
||||
msgstr ""
|
||||
|
||||
msgid "latest"
|
||||
msgstr ""
|
||||
|
||||
msgid "up to date"
|
||||
msgstr ""
|
||||
|
||||
msgid "not installed via package manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade started — see log below."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade already running."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Probing…"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s process"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kernel BTF"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebUI/API"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hot reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "netns daens"
|
||||
msgstr ""
|
||||
|
||||
msgid "pgrep reports the backend is running"
|
||||
msgstr ""
|
||||
|
||||
msgid "backend process is not running"
|
||||
msgstr ""
|
||||
|
||||
msgid "service is running, expected on port %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "service is stopped"
|
||||
msgstr ""
|
||||
|
||||
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
|
||||
msgstr ""
|
||||
|
||||
msgid "/etc/init.d/dae hot_reload is available"
|
||||
msgstr ""
|
||||
|
||||
msgid "netns daens removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "exists, may block daed start"
|
||||
msgstr ""
|
||||
|
||||
msgid "clean"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s Realtime Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto-scroll"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pause"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter (substring)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,653 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh-Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
# ---- config.js: Service Status ----
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Collecting data…"
|
||||
msgstr "正在收集数据…"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "STOPPED"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "Backend"
|
||||
msgstr "后端"
|
||||
|
||||
msgid "Listen"
|
||||
msgstr "监听"
|
||||
|
||||
msgid "Toggle service"
|
||||
msgstr "开关服务"
|
||||
|
||||
msgid "Open WebUI"
|
||||
msgstr "打开 WebUI"
|
||||
|
||||
msgid "Restart"
|
||||
msgstr "重启"
|
||||
|
||||
msgid "Hot Reload"
|
||||
msgstr "热重载"
|
||||
|
||||
# ---- config.js: Backend Switcher ----
|
||||
msgid "Active backend"
|
||||
msgstr "当前后端"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "当前"
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr "未安装"
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr "切换到 %s"
|
||||
|
||||
msgid "%s is not installed"
|
||||
msgstr "%s 未安装"
|
||||
|
||||
msgid "%s is not installed. Install it first, then switch backend."
|
||||
msgstr "%s 未安装。请先安装再切换后端。"
|
||||
|
||||
msgid "Switching backend stops the current service first. Click start when you want the new backend to run."
|
||||
msgstr "切换后端会先停止当前服务。点击开关启动新后端即可。"
|
||||
|
||||
msgid "Stopping current backend…"
|
||||
msgstr "停止当前后端中…"
|
||||
|
||||
msgid "Switching to %s…"
|
||||
msgstr "切换至 %s 中…"
|
||||
|
||||
msgid "Switched to %s. Click start when you want it to run."
|
||||
msgstr "已切换至 %s。点击开关启动。"
|
||||
|
||||
msgid "Backend switched to %s. Click start to run it."
|
||||
msgstr "已切换至 %s。点击开关启动。"
|
||||
|
||||
msgid "Switch failed."
|
||||
msgstr "切换失败。"
|
||||
|
||||
msgid "Failed to switch backend: %s"
|
||||
msgstr "切换后端失败:%s"
|
||||
|
||||
# ---- config.js: Init Actions ----
|
||||
msgid "Action \"%s\" error: %s"
|
||||
msgstr "操作 \"%s\" 出错:%s"
|
||||
|
||||
msgid "Action \"%s\" failed: %s"
|
||||
msgstr "操作 \"%s\" 失败:%s"
|
||||
|
||||
msgid "Action \"%s\" succeeded."
|
||||
msgstr "操作 \"%s\" 成功。"
|
||||
|
||||
msgid "Toggle failed: %s"
|
||||
msgstr "开关切换失败:%s"
|
||||
|
||||
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
|
||||
msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止前者。"
|
||||
|
||||
# ---- config.js: daede Settings ----
|
||||
msgid "daede Settings"
|
||||
msgstr "daede 设置"
|
||||
|
||||
msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI."
|
||||
msgstr "dae 的现代化控制台。订阅、节点、路由和 DNS 请在 daed WebUI 中管理。"
|
||||
|
||||
msgid "Listen Address"
|
||||
msgstr "监听地址"
|
||||
|
||||
msgid "Host:port that the daed WebUI and GraphQL API listen on."
|
||||
msgstr "daed WebUI 与 GraphQL API 监听的 Host:Port。"
|
||||
|
||||
msgid "Max Log Size (MB)"
|
||||
msgstr "单文件大小上限(MB)"
|
||||
|
||||
msgid "Rotate the log file once it grows past this many megabytes."
|
||||
msgstr "单个日志文件超过此大小后轮转。"
|
||||
|
||||
msgid "Max Log Backups"
|
||||
msgstr "保留旧日志数"
|
||||
|
||||
msgid "Number of rotated log files to keep."
|
||||
msgstr "轮转后保留的旧日志文件数。"
|
||||
|
||||
msgid "Log Advanced Settings"
|
||||
msgstr "日志高级设置"
|
||||
|
||||
# ---- config.js: dae Settings ----
|
||||
msgid "dae Parameters"
|
||||
msgstr "dae 参数"
|
||||
|
||||
msgid "Configuration file path and log rotation settings for dae."
|
||||
msgstr "dae 配置文件路径与日志轮转参数。"
|
||||
|
||||
msgid "Configuration File"
|
||||
msgstr "配置文件"
|
||||
|
||||
# ---- config.js: dae Editor ----
|
||||
msgid "dae Configuration"
|
||||
msgstr "dae 配置"
|
||||
|
||||
msgid "Text-only mode."
|
||||
msgstr "纯文本模式。"
|
||||
|
||||
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
|
||||
msgstr "dae 没有内置 WebUI,下面的 DSL 配置需要手工编辑(订阅、节点、路由、DNS)。「从示例初始化」会载入官方模板——保存前请把占位的订阅链接换成你自己的。想图形化点选管理,请把后端切回 daed。"
|
||||
|
||||
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
|
||||
msgstr "dae 配置文件为空。点「从示例初始化」开始,或直接粘贴你的配置。"
|
||||
|
||||
# ---- config.js: dae Parameters ----
|
||||
msgid "dae Parameters"
|
||||
msgstr "dae 参数"
|
||||
|
||||
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
|
||||
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
|
||||
|
||||
msgid "Save and Hot Reload"
|
||||
msgstr "保存并热重载"
|
||||
|
||||
msgid "Initialize from example"
|
||||
msgstr "从示例初始化"
|
||||
|
||||
msgid "+ Insert Block…"
|
||||
msgstr "+ 插入块…"
|
||||
|
||||
msgid "Insert config block at cursor"
|
||||
msgstr "在光标处插入配置块"
|
||||
|
||||
msgid "lines"
|
||||
msgstr "行"
|
||||
|
||||
msgid "⚠ %d placeholder"
|
||||
msgstr "⚠ %d 处占位"
|
||||
|
||||
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
|
||||
msgstr "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
|
||||
|
||||
msgid " / "
|
||||
msgstr " / "
|
||||
|
||||
msgid "这些 "
|
||||
msgstr "这些 "
|
||||
|
||||
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
|
||||
msgstr " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
|
||||
|
||||
msgid "Placeholder lines (click line number to jump):"
|
||||
msgstr "占位行(点行号跳转):"
|
||||
|
||||
msgid "Jump to line"
|
||||
msgstr "跳转到该行"
|
||||
|
||||
msgid "Validating…"
|
||||
msgstr "校验中…"
|
||||
|
||||
msgid "Validate failed: %s"
|
||||
msgstr "配置校验失败:%s"
|
||||
|
||||
msgid "Saving…"
|
||||
msgstr "保存中…"
|
||||
|
||||
msgid "Reload failed: %s"
|
||||
msgstr "热重载失败:%s"
|
||||
|
||||
msgid "Validated · saved · hot-reloaded"
|
||||
msgstr "已校验 · 已保存 · 已热重载"
|
||||
|
||||
msgid "Save failed: %s"
|
||||
msgstr "保存失败:%s"
|
||||
|
||||
msgid "Loading example…"
|
||||
msgstr "加载示例中…"
|
||||
|
||||
msgid "Example loaded"
|
||||
msgstr "示例已加载"
|
||||
|
||||
msgid "Init failed: %s"
|
||||
msgstr "初始化失败:%s"
|
||||
|
||||
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
|
||||
msgstr "当前后端未安装,请从软件源安装 dae 或 daed。"
|
||||
|
||||
# ---- log.js ----
|
||||
msgid "Loading…"
|
||||
msgstr "加载中…"
|
||||
|
||||
msgid "Log file does not exist yet."
|
||||
msgstr "日志文件尚不存在。"
|
||||
|
||||
msgid "Error reading log: %s"
|
||||
msgstr "读取日志出错:%s"
|
||||
|
||||
msgid "Log file cleared."
|
||||
msgstr "日志文件已清空。"
|
||||
|
||||
msgid "Failed to clear log: %s"
|
||||
msgstr "清空日志失败:%s"
|
||||
|
||||
msgid "Truncate %s log on the router? This cannot be undone."
|
||||
msgstr "确定要清空路由器上的 %s 日志吗?此操作不可恢复。"
|
||||
|
||||
# ---- updates.js: Data Updates ----
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "Updates"
|
||||
msgstr "更新"
|
||||
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Data Updates"
|
||||
msgstr "数据更新"
|
||||
|
||||
msgid "Package Updates"
|
||||
msgstr "软件包更新"
|
||||
|
||||
msgid "System Health"
|
||||
msgstr "系统健康"
|
||||
|
||||
msgid "Update"
|
||||
msgstr "更新"
|
||||
|
||||
msgid "Upgrade"
|
||||
msgstr "升级"
|
||||
|
||||
msgid "Clean"
|
||||
msgstr "清理"
|
||||
|
||||
msgid "missing — click Update to fetch"
|
||||
msgstr "缺失——点 Update 下载"
|
||||
|
||||
msgid "%s download started — will refresh in 1–3 min."
|
||||
msgstr "%s 下载已开始——1–3 分钟后刷新。"
|
||||
|
||||
msgid "%s update already running."
|
||||
msgstr "%s 更新已在运行。"
|
||||
|
||||
msgid "%s update failed: %s"
|
||||
msgstr "%s 更新失败:%s"
|
||||
|
||||
msgid "%s update error: %s"
|
||||
msgstr "%s 更新出错:%s"
|
||||
|
||||
# ---- updates.js: Package Updates ----
|
||||
msgid "%s binary"
|
||||
msgstr "%s 内核"
|
||||
|
||||
msgid "active"
|
||||
msgstr "当前"
|
||||
|
||||
msgid "installed"
|
||||
msgstr "已安装"
|
||||
|
||||
msgid "latest"
|
||||
msgstr "最新"
|
||||
|
||||
msgid "up to date"
|
||||
msgstr "已是最新"
|
||||
|
||||
msgid "not installed via package manager"
|
||||
msgstr "非包管理器安装"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "不可用"
|
||||
|
||||
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
|
||||
msgstr "立即运行 \"apk upgrade %s\"?这可能会重启当前后端。"
|
||||
|
||||
msgid "%s upgrade started — see log below."
|
||||
msgstr "%s 升级已开始——见下方日志。"
|
||||
|
||||
msgid "%s upgrade already running."
|
||||
msgstr "%s 升级已在运行。"
|
||||
|
||||
msgid "%s upgrade failed: %s"
|
||||
msgstr "%s 升级失败:%s"
|
||||
|
||||
msgid "%s upgrade error: %s"
|
||||
msgstr "%s 升级出错:%s"
|
||||
|
||||
# ---- updates.js: System Health ----
|
||||
msgid "Probing…"
|
||||
msgstr "检测中…"
|
||||
|
||||
msgid "%s process"
|
||||
msgstr "%s 进程"
|
||||
|
||||
msgid "Kernel BTF"
|
||||
msgstr "内核 BTF"
|
||||
|
||||
msgid "WebUI/API"
|
||||
msgstr "WebUI/API"
|
||||
|
||||
msgid "Hot reload"
|
||||
msgstr "热重载"
|
||||
|
||||
msgid "netns daens"
|
||||
msgstr "netns daens"
|
||||
|
||||
msgid "pgrep reports the backend is running"
|
||||
msgstr "pgrep 检测到后端正在运行"
|
||||
|
||||
msgid "backend process is not running"
|
||||
msgstr "后端进程未运行"
|
||||
|
||||
msgid "service is running, expected on port %s"
|
||||
msgstr "服务运行中,预期监听端口 %s"
|
||||
|
||||
msgid "service is stopped"
|
||||
msgstr "服务已停止"
|
||||
|
||||
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
|
||||
msgstr "不可用——eBPF 需要 CONFIG_DEBUG_INFO_BTF 或 vmlinux-btf"
|
||||
|
||||
msgid "/etc/init.d/dae hot_reload is available"
|
||||
msgstr "dae 热重载可用"
|
||||
|
||||
msgid "netns daens removed."
|
||||
msgstr "netns daens 已清理。"
|
||||
|
||||
msgid "exists, may block daed start"
|
||||
msgstr "已存在,可能阻碍 daed 启动"
|
||||
|
||||
msgid "clean"
|
||||
msgstr "干净"
|
||||
|
||||
# ---- log.js toolbar ----
|
||||
msgid "%s Realtime Log"
|
||||
msgstr "%s 实时日志"
|
||||
|
||||
msgid "Auto-scroll"
|
||||
msgstr "自动滚动"
|
||||
|
||||
msgid "Pause"
|
||||
msgstr "暂停"
|
||||
|
||||
msgid "Filter (substring)"
|
||||
msgstr "过滤(子串)"
|
||||
|
||||
msgid "Clear View"
|
||||
msgstr "清空显示"
|
||||
|
||||
msgid "Clear File"
|
||||
msgstr "清空文件"
|
||||
|
||||
msgid "Download"
|
||||
msgstr "下载"
|
||||
|
||||
# ---- config.js: Subscription Card ----
|
||||
msgid "Subscription Management"
|
||||
msgstr "订阅管理"
|
||||
|
||||
msgid "Paste subscription URL here"
|
||||
msgstr "在此粘贴订阅链接"
|
||||
|
||||
msgid "Apply and Save"
|
||||
msgstr "应用并保存"
|
||||
|
||||
msgid "Applying…"
|
||||
msgstr "应用…"
|
||||
|
||||
msgid "Saved, start dae to apply"
|
||||
msgstr "已保存,启动 dae 后生效"
|
||||
|
||||
msgid "Saved and applied"
|
||||
msgstr "已保存并应用"
|
||||
|
||||
msgid "Saved, reload failed"
|
||||
msgstr "已保存,重载失败"
|
||||
|
||||
msgid "Not found, load example first"
|
||||
msgstr "未找到订阅行,请先载入示例"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "失败"
|
||||
|
||||
msgid "This will replace your current config. Continue?"
|
||||
msgstr "这将覆盖当前配置,继续?"
|
||||
|
||||
msgid "Example loaded — save to apply"
|
||||
msgstr "示例已载入,保存后生效"
|
||||
|
||||
msgid "Saved OK (start dae to apply)"
|
||||
msgstr "已保存(启动 dae 后生效)"
|
||||
|
||||
msgid "Configuration Editor"
|
||||
msgstr "配置编辑"
|
||||
|
||||
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
|
||||
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
|
||||
|
||||
# ---- config.js: Proxy Health ----
|
||||
msgid "Test YouTube"
|
||||
msgstr "测试 YouTube"
|
||||
|
||||
msgid "Testing…"
|
||||
msgstr "测试中…"
|
||||
|
||||
msgid "YouTube reachable · %d ms"
|
||||
msgstr "YouTube 可访问 · %d ms"
|
||||
|
||||
msgid "YouTube unreachable (timeout)"
|
||||
msgstr "YouTube 无法访问(超时)"
|
||||
|
||||
msgid "YouTube unreachable (HTTP %s)"
|
||||
msgstr "YouTube 无法访问(HTTP %s)"
|
||||
|
||||
# ---- config.js: dae friendly form ----
|
||||
msgid "Subscriptions"
|
||||
msgstr "订阅"
|
||||
|
||||
msgid "Airport / subscription links. dae resolves them into the node pool."
|
||||
msgstr "机场 / 订阅链接,dae 会解析成节点池里的节点。"
|
||||
|
||||
msgid "Tag"
|
||||
msgstr "标签"
|
||||
|
||||
msgid "Subscription URL"
|
||||
msgstr "订阅链接"
|
||||
|
||||
msgid "URL is required"
|
||||
msgstr "订阅链接不能为空"
|
||||
|
||||
msgid "Must start with http(s):// or file://"
|
||||
msgstr "需以 http(s):// 或 file:// 开头"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Nodes"
|
||||
msgstr "节点"
|
||||
|
||||
msgid "Single share links (ss / vmess / vless / trojan / tuic / hysteria2 / socks5 …)."
|
||||
msgstr "每行添加一个节点分享链接,支持 ss、vmess、vless、trojan、tuic、hysteria2、socks5。"
|
||||
|
||||
msgid "Share Link"
|
||||
msgstr "分享链接"
|
||||
|
||||
msgid "Groups"
|
||||
msgstr "分组"
|
||||
|
||||
msgid "Outbound groups. Leave the source empty to use all nodes. Set the route fallback to one of these."
|
||||
msgstr "出站分组。来源留空表示用全部节点。路由兜底要指向其中一个分组。"
|
||||
|
||||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
msgid "Policy"
|
||||
msgstr "策略"
|
||||
|
||||
msgid "Min moving average latency"
|
||||
msgstr "最小移动平均延迟"
|
||||
|
||||
msgid "Min last latency"
|
||||
msgstr "最小最近延迟"
|
||||
|
||||
msgid "Min average of last 10"
|
||||
msgstr "最近 10 次最小均值"
|
||||
|
||||
msgid "Random"
|
||||
msgstr "随机"
|
||||
|
||||
msgid "Fixed (first node)"
|
||||
msgstr "固定(第一个节点)"
|
||||
|
||||
msgid "Source"
|
||||
msgstr "来源"
|
||||
|
||||
msgid "Subscriptions and nodes that feed this group. Pick from the list or type a name. Leave empty to use all nodes."
|
||||
msgstr "进入这个分组的订阅和节点。从列表里选,或手输名称。留空=用全部节点。"
|
||||
|
||||
msgid "subscription"
|
||||
msgstr "订阅"
|
||||
|
||||
msgid "node"
|
||||
msgstr "节点"
|
||||
|
||||
msgid "Name filter"
|
||||
msgstr "名称筛选"
|
||||
|
||||
msgid "Optional. Narrows subscriptions in the source to nodes whose name matches — e.g. 香港 or 新加坡|日本. Nodes you picked by name are always kept."
|
||||
msgstr "可选。只对来源里的订阅做收窄,留下名称匹配的节点,如 香港 或 新加坡|日本;你手动挑的单个节点始终保留。"
|
||||
|
||||
msgid "Routing"
|
||||
msgstr "路由"
|
||||
|
||||
msgid "Private IPs direct"
|
||||
msgstr "私网 IP 直连"
|
||||
|
||||
msgid "LAN / private addresses bypass the proxy."
|
||||
msgstr "局域网 / 私有地址不走代理。"
|
||||
|
||||
msgid "China direct"
|
||||
msgstr "国内直连"
|
||||
|
||||
msgid "China mainland IPs and domains bypass the proxy."
|
||||
msgstr "中国大陆 IP 和域名不走代理。"
|
||||
|
||||
msgid "Block ads"
|
||||
msgstr "拦截广告"
|
||||
|
||||
msgid "Drop traffic matching the ad domain list."
|
||||
msgstr "丢弃命中广告域名表的流量。"
|
||||
|
||||
msgid "Fallback group"
|
||||
msgstr "兜底分组"
|
||||
|
||||
msgid "Default outbound for everything else."
|
||||
msgstr "其余流量的默认出站。"
|
||||
|
||||
msgid "Custom rules"
|
||||
msgstr "自定义规则"
|
||||
|
||||
msgid "Raw dae routing lines, evaluated before fallback. Target must be a group name above (or direct/block)."
|
||||
msgstr "原始 dae 路由行,在兜底前生效。箭头右边要填上面的分组名(或 direct / block)。"
|
||||
|
||||
msgid "China upstream"
|
||||
msgstr "国内 DNS 上游"
|
||||
|
||||
msgid "Resolves China-mainland domains."
|
||||
msgstr "解析中国大陆域名。"
|
||||
|
||||
msgid "Fallback upstream"
|
||||
msgstr "兜底 DNS 上游"
|
||||
|
||||
msgid "Resolves everything else."
|
||||
msgstr "解析其余域名。"
|
||||
|
||||
msgid "Add a subscription or node, then save — it takes effect automatically."
|
||||
msgstr "填好订阅或节点,点保存就自动生效。"
|
||||
|
||||
msgid "Apply failed: %s"
|
||||
msgstr "应用失败:%s"
|
||||
|
||||
msgid "Saved · validated · hot-reloaded"
|
||||
msgstr "已保存 · 已校验 · 已热重载"
|
||||
|
||||
msgid "Please fix the highlighted fields."
|
||||
msgstr "请先修正高亮的字段。"
|
||||
|
||||
msgid "Import from existing config"
|
||||
msgstr "从现有配置导入"
|
||||
|
||||
msgid "Existing config detected"
|
||||
msgstr "检测到已有配置"
|
||||
|
||||
msgid "You have a hand-written config.dae. Import its subscriptions and nodes into the form, or keep editing manually below. Saving the form overwrites config.dae."
|
||||
msgstr "你已有手写的 config.dae。可把里面的订阅和节点导入表单,或在下方继续手动编辑。表单保存会覆盖 config.dae。"
|
||||
|
||||
msgid "Importing…"
|
||||
msgstr "导入中…"
|
||||
|
||||
msgid "Imported, reloading…"
|
||||
msgstr "已导入,正在刷新…"
|
||||
|
||||
msgid "Import failed"
|
||||
msgstr "导入失败"
|
||||
|
||||
msgid "Advanced / Manual Mode"
|
||||
msgstr "高级 / 手动模式"
|
||||
|
||||
msgid "Edit config.dae directly. Saving the form above regenerates the file and overwrites changes made here."
|
||||
msgstr "直接编辑 config.dae。上方表单保存会重新生成文件并覆盖这里的改动。"
|
||||
|
||||
msgid "Update Subscriptions"
|
||||
msgstr "更新订阅"
|
||||
|
||||
msgid "Re-fetch all airport subscriptions (reloads dae)"
|
||||
msgstr "重新拉取所有机场订阅(重载 dae)"
|
||||
|
||||
msgid "Updating subscriptions…"
|
||||
msgstr "更新订阅中…"
|
||||
|
||||
msgid "dae is stopped; it fetches subscriptions on start."
|
||||
msgstr "dae 未运行,启动时会自动拉取订阅。"
|
||||
|
||||
msgid "Update failed: %s"
|
||||
msgstr "更新失败:%s"
|
||||
|
||||
msgid "Subscriptions updated (dae reloaded)"
|
||||
msgstr "订阅已更新(dae 已重载)"
|
||||
|
||||
msgid "Section"
|
||||
msgstr "区块"
|
||||
|
||||
msgid "DNS"
|
||||
msgstr "DNS"
|
||||
|
||||
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
|
||||
msgstr "dae 配置文件为空。点击「从示例初始化」开始,或直接在此粘贴你的配置。"
|
||||
|
||||
# ---- log.js: filter dropdown ----
|
||||
msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
msgid "Node Status"
|
||||
msgstr "节点状态"
|
||||
|
||||
msgid "Proxy Traffic"
|
||||
msgstr "代理流量"
|
||||
|
||||
|
||||
msgid "Logging"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Save manual config"
|
||||
msgstr "保存手动配置"
|
||||
|
||||
msgid "Save and Apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Fixed link — nothing to fetch."
|
||||
msgstr "固定链接,无需更新。"
|
||||
@@ -0,0 +1,446 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh-Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
# ---- config.js: Service Status ----
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Collecting data…"
|
||||
msgstr "正在收集数据…"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "STOPPED"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "Backend"
|
||||
msgstr "后端"
|
||||
|
||||
msgid "Listen"
|
||||
msgstr "监听"
|
||||
|
||||
msgid "Toggle service"
|
||||
msgstr "开关服务"
|
||||
|
||||
msgid "Open WebUI"
|
||||
msgstr "打开 WebUI"
|
||||
|
||||
msgid "Restart"
|
||||
msgstr "重启"
|
||||
|
||||
msgid "Hot Reload"
|
||||
msgstr "热重载"
|
||||
|
||||
# ---- config.js: Backend Switcher ----
|
||||
msgid "Active backend"
|
||||
msgstr "当前后端"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "当前"
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr "未安装"
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr "切换到 %s"
|
||||
|
||||
msgid "%s is not installed"
|
||||
msgstr "%s 未安装"
|
||||
|
||||
msgid "%s is not installed. Install it first, then switch backend."
|
||||
msgstr "%s 未安装。请先安装再切换后端。"
|
||||
|
||||
msgid "Switching backend stops the current service first. Click start when you want the new backend to run."
|
||||
msgstr "切换后端会先停止当前服务。点击开关启动新后端即可。"
|
||||
|
||||
msgid "Stopping current backend…"
|
||||
msgstr "停止当前后端中…"
|
||||
|
||||
msgid "Switching to %s…"
|
||||
msgstr "切换至 %s 中…"
|
||||
|
||||
msgid "Switched to %s. Click start when you want it to run."
|
||||
msgstr "已切换至 %s。点击开关启动。"
|
||||
|
||||
msgid "Backend switched to %s. Click start to run it."
|
||||
msgstr "已切换至 %s。点击开关启动。"
|
||||
|
||||
msgid "Switch failed."
|
||||
msgstr "切换失败。"
|
||||
|
||||
msgid "Failed to switch backend: %s"
|
||||
msgstr "切换后端失败:%s"
|
||||
|
||||
# ---- config.js: Init Actions ----
|
||||
msgid "Action \"%s\" error: %s"
|
||||
msgstr "操作 \"%s\" 出错:%s"
|
||||
|
||||
msgid "Action \"%s\" failed: %s"
|
||||
msgstr "操作 \"%s\" 失败:%s"
|
||||
|
||||
msgid "Action \"%s\" succeeded."
|
||||
msgstr "操作 \"%s\" 成功。"
|
||||
|
||||
msgid "Toggle failed: %s"
|
||||
msgstr "开关切换失败:%s"
|
||||
|
||||
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
|
||||
msgstr "%s 已在运行。%s 与 %s 共用 eBPF/cgroup 挂载点,请先停止前者。"
|
||||
|
||||
# ---- config.js: daede Settings ----
|
||||
msgid "daede Settings"
|
||||
msgstr "daede 设置"
|
||||
|
||||
msgid "A modern dashboard for dae. Subscriptions, nodes, routing and DNS are managed in the daed WebUI."
|
||||
msgstr "dae 的现代化控制台。订阅、节点、路由和 DNS 请在 daed WebUI 中管理。"
|
||||
|
||||
msgid "Listen Address"
|
||||
msgstr "监听地址"
|
||||
|
||||
msgid "Host:port that the daed WebUI and GraphQL API listen on."
|
||||
msgstr "daed WebUI 与 GraphQL API 监听的 Host:Port。"
|
||||
|
||||
msgid "Max Log Size (MB)"
|
||||
msgstr "单文件大小上限(MB)"
|
||||
|
||||
msgid "Rotate the log file once it grows past this many megabytes."
|
||||
msgstr "单个日志文件超过此大小后轮转。"
|
||||
|
||||
msgid "Max Log Backups"
|
||||
msgstr "保留旧日志数"
|
||||
|
||||
msgid "Number of rotated log files to keep."
|
||||
msgstr "轮转后保留的旧日志文件数。"
|
||||
|
||||
msgid "Log Advanced Settings"
|
||||
msgstr "日志高级设置"
|
||||
|
||||
# ---- config.js: dae Settings ----
|
||||
msgid "dae Parameters"
|
||||
msgstr "dae 参数"
|
||||
|
||||
msgid "Configuration file path and log rotation settings for dae."
|
||||
msgstr "dae 配置文件路径与日志轮转参数。"
|
||||
|
||||
msgid "Configuration File"
|
||||
msgstr "配置文件"
|
||||
|
||||
# ---- config.js: dae Editor ----
|
||||
msgid "dae Configuration"
|
||||
msgstr "dae 配置"
|
||||
|
||||
msgid "Text-only mode."
|
||||
msgstr "纯文本模式。"
|
||||
|
||||
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
|
||||
msgstr "dae 没有内置 WebUI,下面的 DSL 配置需要手工编辑(订阅、节点、路由、DNS)。「从示例初始化」会载入官方模板——保存前请把占位的订阅链接换成你自己的。想图形化点选管理,请把后端切回 daed。"
|
||||
|
||||
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
|
||||
msgstr "dae 配置文件为空。点「从示例初始化」开始,或直接粘贴你的配置。"
|
||||
|
||||
# ---- config.js: dae Parameters ----
|
||||
msgid "dae Parameters"
|
||||
msgstr "dae 参数"
|
||||
|
||||
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
|
||||
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
|
||||
|
||||
msgid "Save and Hot Reload"
|
||||
msgstr "保存并热重载"
|
||||
|
||||
msgid "Initialize from example"
|
||||
msgstr "从示例初始化"
|
||||
|
||||
msgid "+ Insert Block…"
|
||||
msgstr "+ 插入块…"
|
||||
|
||||
msgid "Insert config block at cursor"
|
||||
msgstr "在光标处插入配置块"
|
||||
|
||||
msgid "lines"
|
||||
msgstr "行"
|
||||
|
||||
msgid "⚠ %d placeholder"
|
||||
msgstr "⚠ %d 处占位"
|
||||
|
||||
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
|
||||
msgstr "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
|
||||
|
||||
msgid " / "
|
||||
msgstr " / "
|
||||
|
||||
msgid "这些 "
|
||||
msgstr "这些 "
|
||||
|
||||
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
|
||||
msgstr " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
|
||||
|
||||
msgid "Placeholder lines (click line number to jump):"
|
||||
msgstr "占位行(点行号跳转):"
|
||||
|
||||
msgid "Jump to line"
|
||||
msgstr "跳转到该行"
|
||||
|
||||
msgid "Validating…"
|
||||
msgstr "校验中…"
|
||||
|
||||
msgid "Validate failed: %s"
|
||||
msgstr "配置校验失败:%s"
|
||||
|
||||
msgid "Saving…"
|
||||
msgstr "保存中…"
|
||||
|
||||
msgid "Reload failed: %s"
|
||||
msgstr "热重载失败:%s"
|
||||
|
||||
msgid "Validated · saved · hot-reloaded"
|
||||
msgstr "已校验 · 已保存 · 已热重载"
|
||||
|
||||
msgid "Save failed: %s"
|
||||
msgstr "保存失败:%s"
|
||||
|
||||
msgid "Loading example…"
|
||||
msgstr "加载示例中…"
|
||||
|
||||
msgid "Example loaded"
|
||||
msgstr "示例已加载"
|
||||
|
||||
msgid "Init failed: %s"
|
||||
msgstr "初始化失败:%s"
|
||||
|
||||
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
|
||||
msgstr "当前后端未安装,请从软件源安装 dae 或 daed。"
|
||||
|
||||
# ---- log.js ----
|
||||
msgid "Loading…"
|
||||
msgstr "加载中…"
|
||||
|
||||
msgid "Log file does not exist yet."
|
||||
msgstr "日志文件尚不存在。"
|
||||
|
||||
msgid "Error reading log: %s"
|
||||
msgstr "读取日志出错:%s"
|
||||
|
||||
msgid "Log file cleared."
|
||||
msgstr "日志文件已清空。"
|
||||
|
||||
msgid "Failed to clear log: %s"
|
||||
msgstr "清空日志失败:%s"
|
||||
|
||||
msgid "Truncate %s log on the router? This cannot be undone."
|
||||
msgstr "确定要清空路由器上的 %s 日志吗?此操作不可恢复。"
|
||||
|
||||
# ---- updates.js: Data Updates ----
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
msgid "Updates"
|
||||
msgstr "更新"
|
||||
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Data Updates"
|
||||
msgstr "数据更新"
|
||||
|
||||
msgid "Package Updates"
|
||||
msgstr "软件包更新"
|
||||
|
||||
msgid "System Health"
|
||||
msgstr "系统健康"
|
||||
|
||||
msgid "Update"
|
||||
msgstr "更新"
|
||||
|
||||
msgid "Upgrade"
|
||||
msgstr "升级"
|
||||
|
||||
msgid "Clean"
|
||||
msgstr "清理"
|
||||
|
||||
msgid "missing — click Update to fetch"
|
||||
msgstr "缺失——点 Update 下载"
|
||||
|
||||
msgid "%s download started — will refresh in 1–3 min."
|
||||
msgstr "%s 下载已开始——1–3 分钟后刷新。"
|
||||
|
||||
msgid "%s update already running."
|
||||
msgstr "%s 更新已在运行。"
|
||||
|
||||
msgid "%s update failed: %s"
|
||||
msgstr "%s 更新失败:%s"
|
||||
|
||||
msgid "%s update error: %s"
|
||||
msgstr "%s 更新出错:%s"
|
||||
|
||||
# ---- updates.js: Package Updates ----
|
||||
msgid "%s binary"
|
||||
msgstr "%s 内核"
|
||||
|
||||
msgid "active"
|
||||
msgstr "当前"
|
||||
|
||||
msgid "installed"
|
||||
msgstr "已安装"
|
||||
|
||||
msgid "latest"
|
||||
msgstr "最新"
|
||||
|
||||
msgid "up to date"
|
||||
msgstr "已是最新"
|
||||
|
||||
msgid "not installed via package manager"
|
||||
msgstr "非包管理器安装"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "不可用"
|
||||
|
||||
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
|
||||
msgstr "立即运行 \"apk upgrade %s\"?这可能会重启当前后端。"
|
||||
|
||||
msgid "%s upgrade started — see log below."
|
||||
msgstr "%s 升级已开始——见下方日志。"
|
||||
|
||||
msgid "%s upgrade already running."
|
||||
msgstr "%s 升级已在运行。"
|
||||
|
||||
msgid "%s upgrade failed: %s"
|
||||
msgstr "%s 升级失败:%s"
|
||||
|
||||
msgid "%s upgrade error: %s"
|
||||
msgstr "%s 升级出错:%s"
|
||||
|
||||
# ---- updates.js: System Health ----
|
||||
msgid "Probing…"
|
||||
msgstr "检测中…"
|
||||
|
||||
msgid "%s process"
|
||||
msgstr "%s 进程"
|
||||
|
||||
msgid "Kernel BTF"
|
||||
msgstr "内核 BTF"
|
||||
|
||||
msgid "WebUI/API"
|
||||
msgstr "WebUI/API"
|
||||
|
||||
msgid "Hot reload"
|
||||
msgstr "热重载"
|
||||
|
||||
msgid "netns daens"
|
||||
msgstr "netns daens"
|
||||
|
||||
msgid "pgrep reports the backend is running"
|
||||
msgstr "pgrep 检测到后端正在运行"
|
||||
|
||||
msgid "backend process is not running"
|
||||
msgstr "后端进程未运行"
|
||||
|
||||
msgid "service is running, expected on port %s"
|
||||
msgstr "服务运行中,预期监听端口 %s"
|
||||
|
||||
msgid "service is stopped"
|
||||
msgstr "服务已停止"
|
||||
|
||||
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
|
||||
msgstr "不可用——eBPF 需要 CONFIG_DEBUG_INFO_BTF 或 vmlinux-btf"
|
||||
|
||||
msgid "/etc/init.d/dae hot_reload is available"
|
||||
msgstr "dae 热重载可用"
|
||||
|
||||
msgid "netns daens removed."
|
||||
msgstr "netns daens 已清理。"
|
||||
|
||||
msgid "exists, may block daed start"
|
||||
msgstr "已存在,可能阻碍 daed 启动"
|
||||
|
||||
msgid "clean"
|
||||
msgstr "干净"
|
||||
|
||||
# ---- log.js toolbar ----
|
||||
msgid "%s Realtime Log"
|
||||
msgstr "%s 实时日志"
|
||||
|
||||
msgid "Auto-scroll"
|
||||
msgstr "自动滚动"
|
||||
|
||||
msgid "Pause"
|
||||
msgstr "暂停"
|
||||
|
||||
msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
msgid "Node Status"
|
||||
msgstr "节点状态"
|
||||
|
||||
msgid "Proxy Traffic"
|
||||
msgstr "代理流量"
|
||||
|
||||
msgid "Clear View"
|
||||
msgstr "清空显示"
|
||||
|
||||
msgid "Clear File"
|
||||
msgstr "清空文件"
|
||||
|
||||
msgid "Download"
|
||||
msgstr "下载"
|
||||
|
||||
# ---- config.js: Subscription Card ----
|
||||
msgid "Subscription Management"
|
||||
msgstr "订阅管理"
|
||||
|
||||
msgid "Paste subscription URL here"
|
||||
msgstr "在此粘贴订阅链接"
|
||||
|
||||
msgid "Apply and Save"
|
||||
msgstr "应用并保存"
|
||||
|
||||
msgid "Applying…"
|
||||
msgstr "应用…"
|
||||
|
||||
msgid "Saved, start dae to apply"
|
||||
msgstr "已保存,启动 dae 后生效"
|
||||
|
||||
msgid "Saved and applied"
|
||||
msgstr "已保存并应用"
|
||||
|
||||
msgid "Saved, reload failed"
|
||||
msgstr "已保存,重载失败"
|
||||
|
||||
msgid "Not found, load example first"
|
||||
msgstr "未找到订阅行,请先载入示例"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "失败"
|
||||
|
||||
msgid "This will replace your current config. Continue?"
|
||||
msgstr "这将覆盖当前配置,继续?"
|
||||
|
||||
msgid "Example loaded — save to apply"
|
||||
msgstr "示例已载入,保存后生效"
|
||||
|
||||
msgid "Saved OK (start dae to apply)"
|
||||
msgstr "已保存(启动 dae 后生效)"
|
||||
|
||||
msgid "Configuration Editor"
|
||||
msgstr "配置编辑"
|
||||
|
||||
msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via Initialize. Replace placeholder URL before saving. Switch to daed for GUI."
|
||||
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
|
||||
|
||||
# ---- config.js: Proxy Health ----
|
||||
msgid "Check Proxy Status"
|
||||
msgstr "检测代理状态"
|
||||
|
||||
msgid "OK, %d flows through proxy"
|
||||
msgstr "正常,%d 条代理流量"
|
||||
|
||||
msgid "No proxy traffic"
|
||||
msgstr "暂无代理流量"
|
||||
@@ -0,0 +1,2 @@
|
||||
config daede 'config'
|
||||
option active_backend 'daed'
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/bin/sh
|
||||
# Patch the daed package's /etc/init.d/daed to clean up /run/netns/daens on
|
||||
# stop_service(). Without this, restarting daed fails with:
|
||||
# fatal: failed to create netns: open /run/netns/daens: file exists
|
||||
#
|
||||
# This script lives under /etc/uci-defaults/ which OpenWrt runs once on
|
||||
# every package install/upgrade and then removes - so each time luci-app-daede
|
||||
# is (re-)installed we re-patch the daed init.d even if it was overwritten
|
||||
# by a daed package upgrade in between.
|
||||
#
|
||||
# The patch is idempotent: a marker comment guards against double-injection.
|
||||
|
||||
set -e
|
||||
|
||||
# luci-app-daede owns its own LuCI menu and view path. Remove stale files from
|
||||
# the official luci-app-daed package and earlier daede builds that reused
|
||||
# /view/daed.
|
||||
rm -f /usr/share/luci/menu.d/luci-app-daed.json
|
||||
rm -f /usr/share/rpcd/acl.d/luci-app-daed.json
|
||||
rm -rf /usr/share/luci-app-daed
|
||||
rm -rf /www/luci-static/resources/view/daed
|
||||
rm -f /usr/lib/lua/luci/i18n/daed.zh-cn.lmo
|
||||
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
|
||||
rm -rf /tmp/luci-modulecache/* 2>/dev/null || true
|
||||
|
||||
if [ -f /etc/config/daed_luci ]; then
|
||||
old_backend="$(uci -q get daed_luci.config.active_backend || true)"
|
||||
if ! uci -q get daede.config >/dev/null 2>&1; then
|
||||
touch /etc/config/daede
|
||||
uci -q set daede.config=daede
|
||||
case "$old_backend" in
|
||||
dae|daed) uci -q set daede.config.active_backend="$old_backend" ;;
|
||||
esac
|
||||
uci -q commit daede
|
||||
elif ! uci -q get daede.config.active_backend >/dev/null 2>&1; then
|
||||
case "$old_backend" in
|
||||
dae|daed)
|
||||
uci -q set daede.config.active_backend="$old_backend"
|
||||
uci -q commit daede
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
rm -f /etc/config/daed_luci
|
||||
fi
|
||||
|
||||
if ! uci -q get daede.config >/dev/null 2>&1; then
|
||||
touch /etc/config/daede
|
||||
uci -q set daede.config=daede
|
||||
if [ -f /etc/init.d/daed ]; then
|
||||
uci -q set daede.config.active_backend='daed'
|
||||
else
|
||||
uci -q set daede.config.active_backend='dae'
|
||||
fi
|
||||
uci -q commit daede
|
||||
fi
|
||||
|
||||
# Remove --disable-timestamp from dae init so log lines carry timestamps
|
||||
DAE_INIT="/etc/init.d/dae"
|
||||
if [ -f "$DAE_INIT" ]; then
|
||||
sed -i 's/--disable-timestamp//' "$DAE_INIT"
|
||||
sed -i '/procd_append_param command *$/d' "$DAE_INIT"
|
||||
fi
|
||||
|
||||
# dae expects geoip.dat / geosite.dat under /usr/share/dae/;
|
||||
# v2ray-geoip installs them to /usr/share/v2ray/. Create symlinks.
|
||||
if [ -f /usr/share/v2ray/geoip.dat ] && [ ! -f /usr/share/dae/geoip.dat ]; then
|
||||
mkdir -p /usr/share/dae
|
||||
ln -sf /usr/share/v2ray/geoip.dat /usr/share/dae/geoip.dat
|
||||
fi
|
||||
if [ -f /usr/share/v2ray/geosite.dat ] && [ ! -f /usr/share/dae/geosite.dat ]; then
|
||||
mkdir -p /usr/share/dae
|
||||
ln -sf /usr/share/v2ray/geosite.dat /usr/share/dae/geosite.dat
|
||||
fi
|
||||
if [ -f /usr/share/v2ray/geoip.dat ] && [ ! -f /usr/share/daed/geoip.dat ]; then
|
||||
mkdir -p /usr/share/daed
|
||||
ln -sf /usr/share/v2ray/geoip.dat /usr/share/daed/geoip.dat
|
||||
fi
|
||||
if [ -f /usr/share/v2ray/geosite.dat ] && [ ! -f /usr/share/daed/geosite.dat ]; then
|
||||
mkdir -p /usr/share/daed
|
||||
ln -sf /usr/share/v2ray/geosite.dat /usr/share/daed/geosite.dat
|
||||
fi
|
||||
|
||||
# Seed default dae form sections (group/routing/dns) for the LuCI editor.
|
||||
# Idempotent: only add what's missing. Lives in the `dae` UCI package,
|
||||
# consumed by gen-dae-config.sh.
|
||||
if [ -f /etc/config/dae ]; then
|
||||
if ! uci -q show dae | grep -q "=group$"; then
|
||||
g="$(uci add dae group)"
|
||||
uci -q set "dae.$g.name=proxy"
|
||||
uci -q set "dae.$g.policy=min_moving_avg"
|
||||
fi
|
||||
if ! uci -q get dae.routing >/dev/null 2>&1; then
|
||||
uci -q set dae.routing=routing
|
||||
uci -q set dae.routing.private_direct=1
|
||||
uci -q set dae.routing.cn_direct=1
|
||||
uci -q set dae.routing.block_ads=0
|
||||
uci -q set dae.routing.fallback=proxy
|
||||
fi
|
||||
if ! uci -q get dae.dns >/dev/null 2>&1; then
|
||||
uci -q set dae.dns=dns
|
||||
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
|
||||
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
|
||||
fi
|
||||
# Migrate legacy group filters to the unified source / name_filter fields,
|
||||
# then drop the old keys so the form shows real, editable values (the old
|
||||
# fields used to re-populate the new ones and looked un-deletable).
|
||||
# `uci get` joins a list into one space-separated line; the unquoted $() word-
|
||||
# splits it back into items. dae tags/keywords are whitespace-free, so this is
|
||||
# the intended split (do NOT set IFS=newline here — it would merge the items).
|
||||
i=0
|
||||
while uci -q get "dae.@group[$i]" >/dev/null 2>&1; do
|
||||
if [ -z "$(uci -q get dae.@group[$i].source)" ]; then
|
||||
for v in $(uci -q get dae.@group[$i].filter_sub 2>/dev/null); do
|
||||
uci -q add_list dae.@group[$i].source="$v"
|
||||
done
|
||||
fi
|
||||
if [ -z "$(uci -q get dae.@group[$i].name_filter)" ]; then
|
||||
for v in $(uci -q get dae.@group[$i].filter_node 2>/dev/null); do
|
||||
uci -q add_list dae.@group[$i].name_filter="$v"
|
||||
done
|
||||
fi
|
||||
uci -q delete dae.@group[$i].filter_sub 2>/dev/null
|
||||
uci -q delete dae.@group[$i].filter_node 2>/dev/null
|
||||
i=$((i+1))
|
||||
done
|
||||
uci -q commit dae
|
||||
fi
|
||||
|
||||
INIT="/etc/init.d/daed"
|
||||
MARKER="# luci-app-daede: netns-cleanup"
|
||||
|
||||
[ -f "$INIT" ] || exit 0
|
||||
grep -q "$MARKER" "$INIT" && exit 0
|
||||
|
||||
# Inject `ip netns del daens 2>/dev/null || true` right after `rm -f "$LOG"`
|
||||
# inside stop_service(). awk is more reliable than sed for multiline contexts.
|
||||
awk -v marker="$MARKER" '
|
||||
BEGIN { in_stop = 0; injected = 0 }
|
||||
/^stop_service\(\)/ { in_stop = 1 }
|
||||
in_stop && /^}/ { in_stop = 0 }
|
||||
{ print }
|
||||
in_stop && !injected && /rm -f "\$LOG"/ {
|
||||
print "\tip netns del daens 2>/dev/null || true " marker
|
||||
injected = 1
|
||||
}
|
||||
' "$INIT" > "${INIT}.new" && mv "${INIT}.new" "$INIT"
|
||||
chmod +x "$INIT"
|
||||
|
||||
exit 0
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# gen-dae-config.sh - build /etc/dae/config.dae from the dae UCI form sections,
|
||||
# validate with `dae validate`, then overwrite + hot reload only on success.
|
||||
#
|
||||
# generate read UCI -> render config.dae -> validate -> overwrite + hot reload
|
||||
# import parse existing config.dae subscription{}/node{} -> fill UCI form
|
||||
#
|
||||
# UCI model (package dae):
|
||||
# config subscription tag/url, enabled
|
||||
# config node tag/link, enabled
|
||||
# config group name/policy, list source, list name_filter
|
||||
# (old list filter_sub/filter_node still read as fallback)
|
||||
# config routing 'routing' private_direct/cn_direct/block_ads/fallback, list custom
|
||||
# config dns 'dns' cn_upstream/fallback_upstream
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
CONFIG_DAE="/etc/dae/config.dae"
|
||||
TMP_GEN="/tmp/dae-gen.dae"
|
||||
DAE_BIN="/usr/bin/dae"
|
||||
DAE_INITD="/etc/init.d/dae"
|
||||
|
||||
# dae single-quoted strings have no escaping; strip quotes/newlines defensively.
|
||||
sanitize() {
|
||||
printf '%s' "$1" | tr -d "'" | tr -d '\n\r'
|
||||
}
|
||||
|
||||
# ===================== generate =====================
|
||||
|
||||
SUB_BUF=""
|
||||
NODE_BUF=""
|
||||
GROUP_BUF=""
|
||||
FIRST_GROUP=""
|
||||
|
||||
emit_sub() {
|
||||
local s="$1" tag url enabled
|
||||
config_get tag "$s" tag ""
|
||||
config_get url "$s" url ""
|
||||
config_get_bool enabled "$s" enabled 1
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
url="$(sanitize "$url")"
|
||||
[ -n "$url" ] || return 0
|
||||
tag="$(sanitize "$tag")"
|
||||
if [ -n "$tag" ]; then
|
||||
SUB_BUF="${SUB_BUF} ${tag}: '${url}'
|
||||
"
|
||||
else
|
||||
SUB_BUF="${SUB_BUF} '${url}'
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
emit_node() {
|
||||
local s="$1" tag link enabled
|
||||
config_get tag "$s" tag ""
|
||||
config_get link "$s" link ""
|
||||
config_get_bool enabled "$s" enabled 1
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
link="$(sanitize "$link")"
|
||||
[ -n "$link" ] || return 0
|
||||
tag="$(sanitize "$tag")"
|
||||
if [ -n "$tag" ]; then
|
||||
NODE_BUF="${NODE_BUF} ${tag}: '${link}'
|
||||
"
|
||||
else
|
||||
NODE_BUF="${NODE_BUF} '${link}'
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
emit_group() {
|
||||
local s="$1" name policy
|
||||
config_get name "$s" name ""
|
||||
config_get policy "$s" policy "min_moving_avg"
|
||||
name="$(sanitize "$name")"
|
||||
[ -n "$name" ] || return 0
|
||||
[ -n "$FIRST_GROUP" ] || FIRST_GROUP="$name"
|
||||
|
||||
GROUP_BUF="${GROUP_BUF} ${name} {
|
||||
"
|
||||
# Build the source: 'source' entries that match a subscription tag become
|
||||
# subtag(), the rest (node tags / keywords) become name(). Old filter_sub /
|
||||
# filter_node configs still work as a fallback when 'source' is unset.
|
||||
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
|
||||
# source: new field, else old filter_sub (decoupled from the name filter so
|
||||
# editing one never silently drops the other)
|
||||
config_list_foreach "$s" source _collect_source
|
||||
[ -z "$SUBS_ACC" ] && [ -z "$NODES_ACC" ] && config_list_foreach "$s" filter_sub _collect_source
|
||||
# optional name filter narrows the subscription pool only (AND); hand-picked
|
||||
# nodes are explicit choices and stay in unconditionally
|
||||
config_list_foreach "$s" name_filter _collect_kw
|
||||
[ -z "$KW_ACC" ] && config_list_foreach "$s" filter_node _collect_kw
|
||||
local andclause=""
|
||||
[ -n "$KW_ACC" ] && andclause=" && name(${KW_ACC})"
|
||||
|
||||
local emitted=0
|
||||
# multiple filter lines = OR, so subtag and name lines union the sources
|
||||
if [ -n "$SUBS_ACC" ]; then
|
||||
GROUP_BUF="${GROUP_BUF} filter: subtag(${SUBS_ACC})${andclause}
|
||||
"
|
||||
emitted=1
|
||||
fi
|
||||
if [ -n "$NODES_ACC" ]; then
|
||||
GROUP_BUF="${GROUP_BUF} filter: name(${NODES_ACC})
|
||||
"
|
||||
emitted=1
|
||||
fi
|
||||
# name filter with no source = filter the whole pool by name
|
||||
if [ "$emitted" = 0 ] && [ -n "$KW_ACC" ]; then
|
||||
GROUP_BUF="${GROUP_BUF} filter: name(${KW_ACC})
|
||||
"
|
||||
fi
|
||||
GROUP_BUF="${GROUP_BUF} policy: ${policy}
|
||||
}
|
||||
"
|
||||
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
|
||||
}
|
||||
|
||||
SUBS_ACC=""
|
||||
NODES_ACC=""
|
||||
KW_ACC=""
|
||||
# Subscription tags, space-padded for membership tests (" tag " match).
|
||||
SUB_TAGS=""
|
||||
collect_subtag() {
|
||||
local t; config_get t "$1" tag ""
|
||||
t="$(sanitize "$t")"
|
||||
[ -n "$t" ] && SUB_TAGS="${SUB_TAGS} ${t} "
|
||||
}
|
||||
is_subtag() {
|
||||
case "$SUB_TAGS" in *" $1 "*) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
# dae filter args are bare only for plain identifiers; anything with regex
|
||||
# chars / CJK / spaces must be wrapped as regex: '...' or the lexer chokes.
|
||||
filter_arg() {
|
||||
case "$1" in
|
||||
*[!A-Za-z0-9_]*) printf "regex: '%s'" "$1" ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
# name-filter args do substring matching: regex metachars -> regex:, else keyword:.
|
||||
# Intentionally stricter than filter_arg: a plain CJK keyword here stays a
|
||||
# substring `keyword:` match, whereas filter_arg wraps any non-word char as regex.
|
||||
kw_arg() {
|
||||
if printf '%s' "$1" | grep -q '[][|().*+?^$\\]'; then
|
||||
printf "regex: '%s'" "$1"
|
||||
else
|
||||
printf "keyword: '%s'" "$1"
|
||||
fi
|
||||
}
|
||||
# classify a source entry: subscription tag -> subtag bucket, else name bucket
|
||||
_collect_source() {
|
||||
local v="$(sanitize "$1")"
|
||||
[ -n "$v" ] || return 0
|
||||
if is_subtag "$v"; then
|
||||
SUBS_ACC="${SUBS_ACC:+$SUBS_ACC, }$(filter_arg "$v")"
|
||||
else
|
||||
NODES_ACC="${NODES_ACC:+$NODES_ACC, }$(filter_arg "$v")"
|
||||
fi
|
||||
}
|
||||
_collect_kw() {
|
||||
local v="$(sanitize "$1")"
|
||||
[ -n "$v" ] || return 0
|
||||
KW_ACC="${KW_ACC:+$KW_ACC, }$(kw_arg "$v")"
|
||||
}
|
||||
|
||||
generate() {
|
||||
config_load dae
|
||||
|
||||
# global overridable knobs (fall back to defaults)
|
||||
local dial_mode log_level wan_interface lan_interface
|
||||
config_get dial_mode config dial_mode "domain"
|
||||
config_get log_level config log_level "info"
|
||||
config_get wan_interface config wan_interface "auto"
|
||||
config_get lan_interface config lan_interface ""
|
||||
|
||||
SUB_BUF=""; NODE_BUF=""; GROUP_BUF=""; FIRST_GROUP=""
|
||||
SUB_TAGS=""
|
||||
config_foreach collect_subtag subscription
|
||||
config_foreach emit_sub subscription
|
||||
config_foreach emit_node node
|
||||
config_foreach emit_group group
|
||||
|
||||
# fall back to a default group when none defined
|
||||
if [ -z "$GROUP_BUF" ]; then
|
||||
GROUP_BUF=" proxy {
|
||||
policy: min_moving_avg
|
||||
}
|
||||
"
|
||||
FIRST_GROUP="proxy"
|
||||
fi
|
||||
|
||||
# routing / dns singletons
|
||||
local private_direct cn_direct block_ads fallback cn_up fb_up
|
||||
config_get_bool private_direct routing private_direct 1
|
||||
config_get_bool cn_direct routing cn_direct 1
|
||||
config_get_bool block_ads routing block_ads 0
|
||||
config_get fallback routing fallback "$FIRST_GROUP"
|
||||
[ -n "$fallback" ] || fallback="$FIRST_GROUP"
|
||||
fallback="$(sanitize "$fallback")"
|
||||
config_get cn_up dns cn_upstream "udp://dns.alidns.com:53"
|
||||
config_get fb_up dns fallback_upstream "tcp+udp://dns.google:53"
|
||||
cn_up="$(sanitize "$cn_up")"
|
||||
fb_up="$(sanitize "$fb_up")"
|
||||
|
||||
{
|
||||
echo "# Auto-generated by luci-app-daede form. Manual edits are overwritten on next save."
|
||||
echo "global {"
|
||||
echo " tproxy_port: 12345"
|
||||
echo " tproxy_port_protect: true"
|
||||
echo " log_level: ${log_level}"
|
||||
[ -n "$lan_interface" ] && echo " lan_interface: ${lan_interface}"
|
||||
echo " wan_interface: ${wan_interface}"
|
||||
echo " auto_config_kernel_parameter: true"
|
||||
echo " dial_mode: ${dial_mode}"
|
||||
echo " tcp_check_url: 'http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111'"
|
||||
echo " udp_check_dns: 'dns.google:53,8.8.8.8,2001:4860:4860::8888'"
|
||||
echo " check_interval: 30s"
|
||||
echo " check_tolerance: 50ms"
|
||||
echo "}"
|
||||
echo ""
|
||||
|
||||
if [ -n "$SUB_BUF" ]; then
|
||||
echo "subscription {"
|
||||
printf '%s' "$SUB_BUF"
|
||||
echo "}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -n "$NODE_BUF" ]; then
|
||||
echo "node {"
|
||||
printf '%s' "$NODE_BUF"
|
||||
echo "}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "dns {"
|
||||
echo " ipversion_prefer: 4"
|
||||
echo " upstream {"
|
||||
echo " cndns: '${cn_up}'"
|
||||
echo " fallbackdns: '${fb_up}'"
|
||||
echo " }"
|
||||
echo " routing {"
|
||||
echo " request {"
|
||||
echo " qname(geosite:cn) -> cndns"
|
||||
echo " fallback: fallbackdns"
|
||||
echo " }"
|
||||
echo " }"
|
||||
echo "}"
|
||||
echo ""
|
||||
|
||||
echo "group {"
|
||||
printf '%s' "$GROUP_BUF"
|
||||
echo "}"
|
||||
echo ""
|
||||
|
||||
echo "routing {"
|
||||
echo " pname(NetworkManager) -> direct"
|
||||
# multicast / broadcast direct (geoip:private doesn't cover these)
|
||||
echo " dip(224.0.0.0/3) -> direct"
|
||||
echo " dip(255.255.255.255/32) -> direct"
|
||||
echo " dip('ff00::/8') -> direct"
|
||||
[ "$private_direct" = "1" ] && echo " dip(geoip:private) -> direct"
|
||||
# NTP time sync direct
|
||||
echo " l4proto(udp) && dport(123) -> direct"
|
||||
# OS connectivity checks direct (avoid captive-portal false positives)
|
||||
echo " domain(connectivitycheck.gstatic.com) -> direct"
|
||||
echo " domain(msftconnecttest.com) -> direct"
|
||||
if [ "$cn_direct" = "1" ]; then
|
||||
echo " dip(geoip:cn) -> direct"
|
||||
echo " domain(geosite:cn) -> direct"
|
||||
fi
|
||||
[ "$block_ads" = "1" ] && echo " domain(geosite:category-ads-all) -> block"
|
||||
config_list_foreach routing custom _emit_custom
|
||||
echo " fallback: ${fallback}"
|
||||
echo "}"
|
||||
} > "$TMP_GEN"
|
||||
chmod 600 "$TMP_GEN" # dae rejects world/group-accessible config files
|
||||
|
||||
# overwrite live config only if validation passes
|
||||
local out
|
||||
out="$("$DAE_BIN" validate -c "$TMP_GEN" 2>&1)"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "validate failed: $(echo "$out" | head -1)" >&2
|
||||
rm -f "$TMP_GEN"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p /etc/dae
|
||||
cat "$TMP_GEN" > "$CONFIG_DAE"
|
||||
chmod 600 "$CONFIG_DAE"
|
||||
rm -f "$TMP_GEN"
|
||||
|
||||
if /usr/bin/pgrep -x dae >/dev/null 2>&1; then
|
||||
"$DAE_INITD" hot_reload >/dev/null 2>&1
|
||||
fi
|
||||
echo "ok"
|
||||
return 0
|
||||
}
|
||||
|
||||
_emit_custom() {
|
||||
local line="$(printf '%s' "$1" | tr -d '\n\r')"
|
||||
[ -n "$line" ] || return 0
|
||||
echo " ${line}"
|
||||
}
|
||||
|
||||
# ===================== import =====================
|
||||
# Fill the UCI form from an existing config.dae subscription{}/node{} block.
|
||||
# Idempotent: wipe same-type sections first, then rebuild from file.
|
||||
|
||||
import_block() {
|
||||
# $1=block name (subscription|node) $2=uci section type $3=link/url field
|
||||
local block="$1" stype="$2" field="$3"
|
||||
awk -v blk="$block" '
|
||||
$0 ~ "^[[:space:]]*"blk"[[:space:]]*\\{" { inb=1; next }
|
||||
inb && /^[[:space:]]*\}/ { inb=0 }
|
||||
inb { print }
|
||||
' "$CONFIG_DAE" 2>/dev/null | while IFS= read -r raw; do
|
||||
# drop comment, trim
|
||||
line="$(printf '%s' "$raw" | sed 's/#.*$//' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
||||
[ -n "$line" ] || continue
|
||||
tag=""; val=""
|
||||
case "$line" in
|
||||
\'*|\"*)
|
||||
# starts with a quote -> untagged link
|
||||
val="$line"
|
||||
;;
|
||||
[A-Za-z0-9_]*:*)
|
||||
# identifier ':' link -> tagged (split on first colon)
|
||||
tag="$(printf '%s' "$line" | sed "s/[[:space:]]*:.*//")"
|
||||
val="$(printf '%s' "$line" | sed "s/^[^:]*:[[:space:]]*//")"
|
||||
;;
|
||||
*)
|
||||
val="$line"
|
||||
;;
|
||||
esac
|
||||
# strip surrounding quotes
|
||||
val="$(printf '%s' "$val" | sed "s/^['\"]//; s/['\"]$//")"
|
||||
[ -n "$val" ] || continue
|
||||
# skip dae example placeholders (ss://LINK, server-ip:port, localhost:1080 ...)
|
||||
case "$val" in
|
||||
*://LINK|*LINK*|*server-ip*|*://localhost:*|*example.com*) continue ;;
|
||||
esac
|
||||
sid="$(uci add dae "$stype")"
|
||||
[ -n "$tag" ] && uci set "dae.$sid.tag=$tag"
|
||||
uci set "dae.$sid.$field=$val"
|
||||
uci set "dae.$sid.enabled=1"
|
||||
done
|
||||
}
|
||||
|
||||
do_import() {
|
||||
[ -f "$CONFIG_DAE" ] || { echo "no config.dae" >&2; return 1; }
|
||||
while uci -q delete dae.@subscription[0] >/dev/null 2>&1; do :; done
|
||||
while uci -q delete dae.@node[0] >/dev/null 2>&1; do :; done
|
||||
import_block subscription subscription url
|
||||
import_block node node link
|
||||
uci commit dae
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
# ===================== entry =====================
|
||||
|
||||
case "$1" in
|
||||
generate) generate ;;
|
||||
import) do_import ;;
|
||||
*) echo "usage: $0 {generate|import}" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# pkg-info.sh <package>
|
||||
# Prints "<installed>\t<latest>" for the named package, where either field is
|
||||
# empty when unknown. Used by the Updates view to decide whether to enable
|
||||
# the [Upgrade] button.
|
||||
|
||||
PKG="$1"
|
||||
case "$PKG" in
|
||||
dae|daed|luci-app-daede) ;;
|
||||
*) echo "" ; exit 64 ;;
|
||||
esac
|
||||
|
||||
installed=""
|
||||
latest=""
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
if apk info -e "$PKG" >/dev/null 2>&1; then
|
||||
installed=$(apk list -I "$PKG" 2>/dev/null | awk -v p="$PKG" '
|
||||
$1 ~ "^" p "-" {
|
||||
sub("^" p "-", "", $1);
|
||||
print $1;
|
||||
exit
|
||||
}
|
||||
')
|
||||
fi
|
||||
latest=$(apk search "^${PKG}\$" 2>/dev/null | sort -V | tail -1 | sed "s/^${PKG}-//")
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
installed=$(opkg status "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
|
||||
latest=$(opkg info "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$installed" "$latest"
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# proxy-check.sh - test whether dae's transparent proxy can actually reach a
|
||||
# blocked site (default YouTube). dae proxies the router's own WAN-bound traffic
|
||||
# (wan_interface), so a plain request from here is routed through dae just like a
|
||||
# LAN client would be. Prints one line: ok=<0|1> code=<http> ms=<n>
|
||||
|
||||
URL="${1:-https://www.youtube.com/generate_204}"
|
||||
|
||||
code=000
|
||||
ms=0
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
out="$(curl -4 -sL --max-time 8 --connect-timeout 4 \
|
||||
-o /dev/null -w '%{http_code} %{time_total}' "$URL" 2>/dev/null)"
|
||||
code="$(printf '%s' "$out" | awk '{print $1}')"
|
||||
t="$(printf '%s' "$out" | awk '{print $2}')"
|
||||
[ -n "$code" ] || code=000
|
||||
ms="$(awk -v x="${t:-0}" 'BEGIN{printf "%d", x*1000}')"
|
||||
else
|
||||
# busybox wget has no status/timing output — success is exit 0
|
||||
if wget -q -T 8 -O /dev/null "$URL" 2>/dev/null; then code=204; else code=000; fi
|
||||
fi
|
||||
|
||||
ok=0
|
||||
case "$code" in 200|204|301|302) ok=1 ;; esac
|
||||
|
||||
echo "ok=$ok code=$code ms=$ms"
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# update-geo.sh <geoip|geosite>
|
||||
# Fork-and-detach so the LuCI RPC call returns immediately. The diagnostics
|
||||
# card polls the .dat file's mtime/size to detect completion. A lock file
|
||||
# in /tmp prevents concurrent runs.
|
||||
|
||||
TYPE="$1"
|
||||
case "$TYPE" in
|
||||
geoip)
|
||||
URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
|
||||
DEST="/usr/share/v2ray/geoip.dat"
|
||||
;;
|
||||
geosite)
|
||||
URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
|
||||
DEST="/usr/share/v2ray/geosite.dat"
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 <geoip|geosite>" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
LOCK="/tmp/luci-app-daede.${TYPE}.lock"
|
||||
LOG="/tmp/luci-app-daede.${TYPE}.log"
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
# Lock is fresh (< 5 min)? Refuse. Stale? Remove and proceed.
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
age=$(( $(date +%s) - mtime ))
|
||||
if [ "$age" -lt 300 ]; then
|
||||
echo "${TYPE} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
|
||||
exit 75
|
||||
fi
|
||||
rm -f "$LOCK"
|
||||
fi
|
||||
|
||||
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
|
||||
echo "${TYPE} update already in progress" >&2
|
||||
exit 75
|
||||
fi
|
||||
|
||||
# Spawn detached worker — parent returns immediately to LuCI.
|
||||
(
|
||||
exec >"$LOG" 2>&1
|
||||
trap 'rm -f "$LOCK"' EXIT INT TERM
|
||||
|
||||
TMP="${DEST}.new"
|
||||
mkdir -p "$(dirname "$DEST")"
|
||||
echo "$(date '+%F %T') begin: $URL"
|
||||
|
||||
if ! curl -fsSL --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
|
||||
echo "$(date '+%F %T') download failed"
|
||||
rm -f "$TMP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
|
||||
if [ "$size" -lt 102400 ]; then
|
||||
echo "$(date '+%F %T') file too small ($size bytes)"
|
||||
rm -f "$TMP"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mv "$TMP" "$DEST"
|
||||
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
|
||||
echo "started in background, see $LOG"
|
||||
exit 0
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
# update-pkg.sh <dae|daed|luci-app-daede>
|
||||
# Refresh package indexes and upgrade the named package via apk (25.12+) or
|
||||
# opkg (24.10). Forks the work to background so the LuCI RPC call returns
|
||||
# immediately; the result is streamed to /tmp/luci-app-daede.pkg.<name>.log.
|
||||
|
||||
PKG="$1"
|
||||
case "$PKG" in
|
||||
dae|daed|luci-app-daede) ;;
|
||||
*)
|
||||
echo "usage: $0 <dae|daed|luci-app-daede>" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
LOCK="/tmp/luci-app-daede.pkg-${PKG}.lock"
|
||||
LOG="/tmp/luci-app-daede.pkg-${PKG}.log"
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
age=$(( $(date +%s) - mtime ))
|
||||
if [ "$age" -lt 300 ]; then
|
||||
echo "${PKG} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
|
||||
exit 75
|
||||
fi
|
||||
rm -f "$LOCK"
|
||||
fi
|
||||
|
||||
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
|
||||
echo "${PKG} update already in progress" >&2
|
||||
exit 75
|
||||
fi
|
||||
|
||||
(
|
||||
exec >"$LOG" 2>&1
|
||||
trap 'rm -f "$LOCK"' EXIT INT TERM
|
||||
|
||||
echo "$(date '+%F %T') begin upgrade: $PKG"
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
echo "--- apk update ---"
|
||||
apk update 2>&1
|
||||
echo "--- apk upgrade $PKG ---"
|
||||
# --no-self-upgrade so apk itself does not jump versions mid-op
|
||||
apk upgrade --no-self-upgrade "$PKG" 2>&1
|
||||
rc=$?
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
echo "--- opkg update ---"
|
||||
opkg update 2>&1
|
||||
echo "--- opkg upgrade $PKG ---"
|
||||
opkg upgrade "$PKG" 2>&1
|
||||
rc=$?
|
||||
else
|
||||
echo "no package manager found"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "$(date '+%F %T') done (rc=$rc)"
|
||||
|
||||
# luci-app-daede upgrade replaces ACL JSON — reload rpcd so changes apply.
|
||||
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
|
||||
echo "reloading rpcd to pick up new ACL"
|
||||
/etc/init.d/rpcd reload 2>&1
|
||||
fi
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
|
||||
echo "started in background, see $LOG"
|
||||
exit 0
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"admin/services/daede": {
|
||||
"title": "daede",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-daede" ]
|
||||
}
|
||||
},
|
||||
"admin/services/daede/config": {
|
||||
"title": "Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/config"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/updates": {
|
||||
"title": "Updates",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/updates"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/log": {
|
||||
"title": "Log",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/log"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"luci-app-daede": {
|
||||
"description": "Grant access to dae/daed configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/init.d/dae": [ "read" ],
|
||||
"/etc/init.d/daed": [ "read" ],
|
||||
"/etc/config/daede": [ "read" ],
|
||||
"/etc/dae/config.dae": [ "read" ],
|
||||
"/etc/dae/example.dae": [ "read" ],
|
||||
"/var/log/dae/dae.log": [ "read" ],
|
||||
"/var/log/daed/daed.log": [ "read" ],
|
||||
"/usr/share/v2ray/geoip.dat": [ "read" ],
|
||||
"/usr/share/v2ray/geosite.dat": [ "read" ],
|
||||
"/sys/kernel/btf/vmlinux": [ "read" ],
|
||||
"/run/netns/daens": [ "read" ],
|
||||
"/tmp/luci-app-daede.geoip.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.geosite.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.pkg-dae.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.pkg-daed.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.pkg-luci-app-daede.log": [ "read" ],
|
||||
"/usr/bin/pgrep -x dae": [ "exec" ],
|
||||
"/usr/bin/pgrep -x daed": [ "exec" ],
|
||||
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/bin/dae --version": [ "exec" ],
|
||||
"/bin/rm -f /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "dae", "daed", "daede" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/etc/dae/config.dae": [ "write" ],
|
||||
"/tmp/dae-validate.dae": [ "write" ],
|
||||
"/etc/config/daede": [ "write" ],
|
||||
"/var/log/dae/dae.log": [ "write" ],
|
||||
"/var/log/daed/daed.log": [ "write" ],
|
||||
"/etc/init.d/dae start": [ "exec" ],
|
||||
"/etc/init.d/dae stop": [ "exec" ],
|
||||
"/etc/init.d/dae restart": [ "exec" ],
|
||||
"/etc/init.d/dae enable": [ "exec" ],
|
||||
"/etc/init.d/dae disable": [ "exec" ],
|
||||
"/etc/init.d/dae hot_reload": [ "exec" ],
|
||||
"/etc/init.d/daed start": [ "exec" ],
|
||||
"/etc/init.d/daed stop": [ "exec" ],
|
||||
"/etc/init.d/daed restart": [ "exec" ],
|
||||
"/etc/init.d/daed enable": [ "exec" ],
|
||||
"/etc/init.d/daed disable": [ "exec" ],
|
||||
"/sbin/uci set dae.config.enabled=*": [ "exec" ],
|
||||
"/sbin/uci set daed.config.enabled=*": [ "exec" ],
|
||||
"/sbin/uci set daede.config=daede": [ "exec" ],
|
||||
"/sbin/uci set daede.config.active_backend=*": [ "exec" ],
|
||||
"/sbin/uci commit dae": [ "exec" ],
|
||||
"/sbin/uci commit daed": [ "exec" ],
|
||||
"/sbin/uci commit daede": [ "exec" ],
|
||||
"/sbin/ip netns del daens": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-geo.sh geoip": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-geo.sh geosite": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-pkg.sh dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-pkg.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh generate": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ]
|
||||
},
|
||||
"uci": [ "dae", "daed", "daede" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=natflow
|
||||
PKG_VERSION:=20260531
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=881417cc21602eb10504e1ba99068b28a114b858
|
||||
PKG_SOURCE_VERSION:=4b0712931b2c14556b3ac4b6aaf921aeda07846c
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
Reference in new issue
Block a user