🎨 Sync 2026-10-29 20:07:12

This commit is contained in:
github-actions[bot] committed 2026-10-29 20:07:12 +08:00
1 parent 0546d1d8e4
commit 737acd5a18
182 files changed
+10470 -1649

No files matched your search

@@ -103,10 +103,11 @@ function isSetName(s) {
return view.extend({
load: function() {
/* 配置字段走 conf get;数据源健康度走 health --json。
health 失败不能拖垮整个配置页:捕获后渲染成“读取失败”。 */
health/policy-check 失败不能拖垮整个配置页:捕获后渲染成“读取失败”。 */
return Promise.all([
run(['conf', 'get']),
fs.exec(CMD, ['health', '--json']).catch(function() { return null; })
fs.exec(CMD, ['health', '--json']).catch(function() { return null; }),
fs.exec(CMD, ['policy-check', '--json']).catch(function() { return null; })
]);
},
@@ -119,13 +120,24 @@ return view.extend({
} catch (e) {
health = null;
}
/* BUG-25:首装用户 mwan3 里没有 wan_only/wanb_only 策略时,
wire 会静默失效。这里在页面顶部给一条醒目提示 + 一键 init。 */
var pcheck = null;
try {
pcheck = JSON.parse(((res[2] && res[2].stdout) || '').trim() || 'null');
} catch (e) {
pcheck = null;
}
var model = {
mode: 'bind',
policies: [],
isp: [],
dns: [],
domains: []
domains: [],
/* lc:begin */
lc: { enable: true, geo: true, provider: 'auto', interval: 60 }
/* lc:end */
};
out.split('\n').forEach(function(line) {
@@ -146,6 +158,16 @@ return view.extend({
model.dns.push({ policy: f[1], dns: f[2], test: null });
else if (f[0] === 'domain' && f.length >= 3)
model.domains.push({ domain: f[1], policy: f[2] });
/* lc:begin */
else if (f[0] === 'line_check_enable')
model.lc.enable = (f[1] || '1') === '1';
else if (f[0] === 'line_check_geo')
model.lc.geo = (f[1] || '1') === '1';
else if (f[0] === 'line_check_provider')
model.lc.provider = f[1] || 'auto';
else if (f[0] === 'line_check_interval')
model.lc.interval = parseInt(f[1], 10) || 60;
/* lc:end */
});
if (!model.isp.length) {
@@ -528,6 +550,86 @@ return view.extend({
}
}, [ svgIcon('plus'), E('span', {}, [_('添加域名')]) ]);
/* lc:begin */
/* ============================================================ 三、线路体检 */
var lcEnable = E('input', { 'type': 'checkbox', 'checked': model.lc.enable ? '' : null });
lcEnable.addEventListener('change', function() { model.lc.enable = lcEnable.checked; });
var lcGeo = E('input', { 'type': 'checkbox', 'checked': model.lc.geo ? '' : null });
lcGeo.addEventListener('change', function() {
model.lc.geo = lcGeo.checked;
lcProvider.disabled = !lcGeo.checked;
});
var lcProvider = E('select', { 'class': 'cbi-input-select', 'disabled': model.lc.geo ? null : '' },
[
['auto', _('自动(百度优先,失败转 ip-api)')],
['baidu', _('只用百度(HTTPS)')],
['ip-api', _('只用 ip-api(HTTP,明文)')],
['off', _('关闭')]
].map(function(o) {
return E('option', { 'value': o[0], 'selected': model.lc.provider === o[0] ? '' : null }, [ o[1] ]);
}));
lcProvider.addEventListener('change', function() { model.lc.provider = lcProvider.value; });
var lcInterval = E('input', {
'type': 'number', 'class': 'cbi-input-text', 'min': '15', 'max': '3600',
'value': String(model.lc.interval || 60)
});
var lcIntervalErr = errBox();
lcInterval.addEventListener('input', function() {
var n = parseInt(lcInterval.value, 10);
model.lc.interval = isNaN(n) ? 0 : n;
if (model.lc.interval < 15 || model.lc.interval > 3600) {
setErr(lcIntervalErr, _('必须是 15-3600 之间的整数秒'));
model.lc._valid = false;
} else {
setErr(lcIntervalErr, '');
model.lc._valid = true;
}
});
model.lc._valid = !(model.lc.interval < 15 || model.lc.interval > 3600);
var lcSection = E('section', { 'class': 'isp-section' }, [
E('h3', {}, [ _('线路体检') ]),
E('p', { 'class': 'isp-desc' }, [
_('对每条 WAN 做出口类型、四段延迟、NAT 与归属地检查。体检只读,不修改 mwan3/nft 规则。')
]),
E('table', { 'class': 'isp-form-table' }, [
E('tbody', {}, [
E('tr', {}, [
E('td', { 'data-th': _('启用') }, [
E('label', { 'class': 'isp-switch' }, [ lcEnable, E('span') ]),
E('div', { 'class': 'lc-hint' }, [ _('关闭后命令行与页面都不再执行体检') ])
]),
E('td', { 'data-th': _('归属地查询') }, [
E('label', { 'class': 'isp-switch' }, [ lcGeo, E('span') ]),
E('div', { 'class': 'lc-hint' }, [
_('关闭后只停止归属地查询;出口 IP 回显、STUN 与延迟探测仍会发出。卡片显示“未查询”')
])
])
]),
E('tr', {}, [
E('td', { 'data-th': _('归属地服务商') }, [ lcProvider ]),
E('td', { 'data-th': _('最小间隔') }, [
E('div', { 'class': 'lc-inline' }, [
lcInterval, E('span', { 'class': 'lc-hint' }, [ _('秒(15-3600)') ])
]),
lcIntervalErr
])
])
])
]),
E('div', { 'class': 'isp-note lc-privacy-note' }, [
_('隐私说明:开启归属地后,体检会把每条 WAN 的出口公网 IP 发送给第三方(默认百度,失败时 ip-api.com)查询归属地。'),
_('只发送出口 IP,不发送内网地址、设备名、MAC、流量内容。ip-api 使用 HTTP,传输不加密;介意时请选择“关闭”或“只用百度”。'),
_('关闭后省/市显示“未查询”,但仍会用本地 @isp_* 集合判断运营商大类(本机判断,不外发)。'),
_('注意:关闭归属地只停止归属地查询;出口 IP 回显、STUN 与延迟探测仍会按配置的服务器发出。要完全停发,请关闭“启用”。')
])
]);
/* lc:end */
/* ============================================================ 四、数据源(只读) */
/* 数据源地址是 root 路径的下载入口,配置页不开放编辑:
@@ -616,7 +718,13 @@ return view.extend({
isp: ispVal,
dns: dnsVal,
dom: domVal,
mode: modeSel.value
mode: modeSel.value,
/* lc:begin */
lcEnable: model.lc.enable ? '1' : '0',
lcGeo: model.lc.geo ? '1' : '0',
lcProvider: model.lc.provider,
lcInterval: String(model.lc.interval || 60)
/* lc:end */
};
}
@@ -633,6 +741,14 @@ return view.extend({
]), 'danger');
ok = false;
}
/* lc:begin */
if (!model.lc._valid) {
ui.addNotification(null, E('p', {}, [
_('线路体检的最小间隔必须是 15-3600 之间的整数秒')
]), 'danger');
ok = false;
}
/* lc:end */
return ok;
}
@@ -675,6 +791,12 @@ return view.extend({
'dns_table', v.dns,
'domain_split', v.dom,
'dns_mode', v.mode
/* lc:begin */
,'line_check_enable', v.lcEnable,
'line_check_geo', v.lcGeo,
'line_check_provider', v.lcProvider,
'line_check_interval', v.lcInterval
/* lc:end */
]).then(function(r) {
showResult(((r.stdout || '') + (r.stderr || '')) || '(无输出)');
ui.addNotification(null, E('p', {}, [_('预检完成,配置未被修改')]), 'info');
@@ -698,6 +820,12 @@ return view.extend({
.then(function() { return step(['conf', 'set', 'dns_table', v.dns], log); })
.then(function() { return step(['conf', 'set', 'domain_split', v.dom], log); })
.then(function() { return step(['conf', 'set', 'dns_mode', v.mode], log); })
/* lc:begin */
.then(function() { return step(['conf', 'set', 'line_check_enable', v.lcEnable], log); })
.then(function() { return step(['conf', 'set', 'line_check_geo', v.lcGeo], log); })
.then(function() { return step(['conf', 'set', 'line_check_provider', v.lcProvider], log); })
.then(function() { return step(['conf', 'set', 'line_check_interval', v.lcInterval], log); })
/* lc:end */
.then(function() {
if (!apply)
return null;
@@ -719,6 +847,48 @@ return view.extend({
});
}
/* BUG-25:策略缺失/空成员时页面顶部的醒目提示条。
只有 policy-check 明确返回 ok=false 才显示;读不到(旧版脚本/命令失败)
时保持安静,不用一个误报吓用户。 */
var policyBanner = E('div', { 'class': 'isp-banner-wrap' });
if (pcheck && pcheck.ok === false) {
var bad = (pcheck.policies || []).filter(function(p) { return !p.ok; });
var badText = bad.map(function(p) {
return p.name + '(' + (p.detail || _('不可用')) + ')';
}).join('、');
var bannerErr = E('div', { 'class': 'isp-banner-err' });
var bannerBtn;
bannerBtn = E('button', {
'class': 'cbi-button cbi-button-action isp-banner-btn',
'click': function() {
bannerBtn.disabled = true;
bannerBtn.textContent = _('正在初始化…');
bannerErr.textContent = '';
return run(['init', '--assume-yes']).then(function() {
ui.addNotification(null, E('p', {}, [ _('已绑定线路,页面将刷新') ]), 'info');
window.location.reload();
}).catch(function(e) {
bannerBtn.disabled = false;
bannerBtn.textContent = _('重新尝试初始化');
bannerErr.textContent = String(e).replace(/^.*失败([^)]*):/, '');
});
}
}, [ _('运行初始化绑定线路') ]);
policyBanner = E('div', { 'class': 'isp-banner is-warn' }, [
E('div', { 'class': 'isp-banner-msg' }, [
E('strong', {}, [ _('mwan3 策略缺失或没有成员:分流规则不会生效') ]),
E('div', {}, [ _('需要绑定的策略:') + badText ]),
E('div', { 'class': 'isp-banner-hint' }, [
_('按钮会按 ISP 表顺序自动绑定(wan_only→wan1、wanb_only→wan2…);也可以 SSH 执行 '),
E('code', {}, [ 'isp-split init' ]),
_(' 逐个选择接口。')
]),
bannerErr
]),
bannerBtn
]);
}
/* ============================================================ 页面 */
return E('div', { 'class': 'isp-app cbi-map' }, [
@@ -731,6 +901,8 @@ return view.extend({
])
]),
policyBanner,
/* ---- 策略映射 ---- */
E('section', { 'class': 'isp-section' }, [
E('h3', {}, [ _('策略映射') ]),
@@ -802,6 +974,11 @@ return view.extend({
E('div', { 'class': 'isp-btn-row' }, [ addDomBtn ])
]),
/* lc:begin */
/* ---- 线路体检 ---- */
lcSection,
/* lc:end */
/* ---- 数据源(只读) ---- */
E('section', { 'class': 'isp-section' }, [
E('h3', {}, [ _('数据源'), E('span', { 'class': 'isp-tag' }, [ _('只读') ]) ]),