mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
🎄 Sync 2026-06-14 03:16:16
This commit is contained in:
parent
8eb87f53b0
commit
79dca49776
@ -3,8 +3,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.06.13.2
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=2026.06.14-2
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_SOURCE:=dae-src-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.10.4
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=1.12.0
|
||||
PKG_RELEASE:=6
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@ -76,9 +76,12 @@ define Package/$(PKG_NAME)/install
|
||||
$(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_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.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)/www/luci-static/resources/view/daede/vendor
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/vendor/* $(1)/www/luci-static/resources/view/daede/vendor/
|
||||
|
||||
$(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
|
||||
|
||||
@ -0,0 +1,261 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
function utf8Base64(value) {
|
||||
const bytes = new TextEncoder().encode(String(value));
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function utf8FromBase64(value) {
|
||||
const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const bytes = Uint8Array.from(binary, function(ch) { return ch.charCodeAt(0); });
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
}
|
||||
|
||||
function base64Url(value) {
|
||||
return utf8Base64(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function endpoint(server, port) {
|
||||
const host = String(server).indexOf(':') >= 0 && String(server)[0] !== '['
|
||||
? '[' + server + ']'
|
||||
: server;
|
||||
return host + ':' + port;
|
||||
}
|
||||
|
||||
function requireFields(node, fields) {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const key = fields[i];
|
||||
if (node[key] === undefined || node[key] === null || node[key] === '')
|
||||
throw new Error('Missing required field: ' + key);
|
||||
}
|
||||
}
|
||||
|
||||
function setIf(params, key, value) {
|
||||
if (value !== undefined && value !== null && value !== '')
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
function arrayValue(value) {
|
||||
return Array.isArray(value) ? value.join(',') : value;
|
||||
}
|
||||
|
||||
function addTransport(params, node) {
|
||||
const network = node.network || 'tcp';
|
||||
params.set('type', network);
|
||||
|
||||
if (network === 'ws') {
|
||||
const opts = node['ws-opts'] || {};
|
||||
setIf(params, 'path', opts.path);
|
||||
setIf(params, 'host', opts.headers && (opts.headers.Host || opts.headers.host));
|
||||
} else if (network === 'grpc') {
|
||||
const opts = node['grpc-opts'] || {};
|
||||
setIf(params, 'serviceName', opts['grpc-service-name'] || opts.serviceName);
|
||||
} else if (network === 'http' || network === 'h2') {
|
||||
const opts = node['h2-opts'] || node['http-opts'] || {};
|
||||
setIf(params, 'path', Array.isArray(opts.path) ? opts.path[0] : opts.path);
|
||||
setIf(params, 'host', arrayValue(opts.host));
|
||||
}
|
||||
}
|
||||
|
||||
function makeResult(node, link) {
|
||||
return {
|
||||
ok: true,
|
||||
name: String(node.name || 'Node'),
|
||||
type: String(node.type || '').toLowerCase(),
|
||||
link: link,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
function convertSs(node) {
|
||||
requireFields(node, [ 'server', 'port', 'cipher', 'password' ]);
|
||||
const auth = base64Url(node.cipher + ':' + node.password);
|
||||
let query = '';
|
||||
if (node.plugin) {
|
||||
const fields = [ String(node.plugin) ];
|
||||
const opts = node['plugin-opts'] || {};
|
||||
Object.keys(opts).forEach(function(key) {
|
||||
const value = opts[key];
|
||||
if (value === true)
|
||||
fields.push(key);
|
||||
else if (value !== false && value !== undefined && value !== null && value !== '')
|
||||
fields.push(key + '=' + value);
|
||||
});
|
||||
const params = new URLSearchParams();
|
||||
params.set('plugin', fields.join(';'));
|
||||
query = '?' + params.toString();
|
||||
}
|
||||
return 'ss://' + auth + '@' + endpoint(node.server, node.port) + query + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertVmess(node) {
|
||||
requireFields(node, [ 'server', 'port', 'uuid' ]);
|
||||
const network = node.network || 'tcp';
|
||||
const ws = node['ws-opts'] || {};
|
||||
const grpc = node['grpc-opts'] || {};
|
||||
const h2 = node['h2-opts'] || {};
|
||||
const payload = {
|
||||
v: '2',
|
||||
ps: String(node.name || 'Node'),
|
||||
add: String(node.server),
|
||||
port: String(node.port),
|
||||
id: String(node.uuid),
|
||||
aid: Number(node.alterId || node.alter_id || 0),
|
||||
scy: String(node.cipher || 'auto'),
|
||||
net: network,
|
||||
type: 'none',
|
||||
host: String((ws.headers && (ws.headers.Host || ws.headers.host)) || arrayValue(h2.host) || ''),
|
||||
path: String(ws.path || grpc['grpc-service-name'] || (Array.isArray(h2.path) ? h2.path[0] : h2.path) || ''),
|
||||
tls: node.tls ? 'tls' : 'none',
|
||||
sni: String(node.servername || node.sni || ''),
|
||||
alpn: String(arrayValue(node.alpn) || ''),
|
||||
fp: String(node.fingerprint || node['client-fingerprint'] || '')
|
||||
};
|
||||
return 'vmess://' + utf8Base64(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function convertVless(node) {
|
||||
requireFields(node, [ 'server', 'port', 'uuid' ]);
|
||||
const params = new URLSearchParams();
|
||||
addTransport(params, node);
|
||||
params.set('encryption', String(node.encryption || 'none'));
|
||||
setIf(params, 'flow', node.flow);
|
||||
|
||||
const reality = node['reality-opts'];
|
||||
if (reality || node.tls) {
|
||||
params.set('security', reality ? 'reality' : 'tls');
|
||||
setIf(params, 'sni', node.servername || node.sni);
|
||||
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allowInsecure', '1');
|
||||
if (reality) {
|
||||
setIf(params, 'pbk', reality['public-key']);
|
||||
setIf(params, 'sid', reality['short-id']);
|
||||
setIf(params, 'spx', reality['spider-x']);
|
||||
}
|
||||
}
|
||||
|
||||
return 'vless://' + encodeURIComponent(node.uuid) + '@' + endpoint(node.server, node.port) +
|
||||
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertTrojan(node) {
|
||||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
addTransport(params, node);
|
||||
setIf(params, 'sni', node.sni || node.servername);
|
||||
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allowInsecure', '1');
|
||||
return 'trojan://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||||
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertTuic(node) {
|
||||
requireFields(node, [ 'server', 'port', 'uuid', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
setIf(params, 'sni', node.sni);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
setIf(params, 'congestion_control', node['congestion-controller'] || node['congestion-control']);
|
||||
setIf(params, 'udp_relay_mode', node['udp-relay-mode']);
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allow_insecure', '1');
|
||||
return 'tuic://' + encodeURIComponent(node.uuid) + ':' + encodeURIComponent(node.password) + '@' +
|
||||
endpoint(node.server, node.port) + (params.toString() ? '?' + params.toString() : '') +
|
||||
'#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertHysteria2(node) {
|
||||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
setIf(params, 'sni', node.sni);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
setIf(params, 'obfs', node.obfs);
|
||||
setIf(params, 'obfs-password', node['obfs-password']);
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('insecure', '1');
|
||||
return 'hysteria2://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||||
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertAnytls(node) {
|
||||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
setIf(params, 'sni', node.sni || node.servername);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
setIf(params, 'client-fingerprint', node['client-fingerprint'] || node.fingerprint);
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allowInsecure', '1');
|
||||
if (node.udp)
|
||||
params.set('udp', '1');
|
||||
return 'anytls://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||||
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertProxy(node) {
|
||||
const type = String(node && node.type || '').toLowerCase();
|
||||
const converters = {
|
||||
ss: convertSs,
|
||||
vmess: convertVmess,
|
||||
vless: convertVless,
|
||||
trojan: convertTrojan,
|
||||
tuic: convertTuic,
|
||||
hysteria2: convertHysteria2,
|
||||
hy2: convertHysteria2,
|
||||
anytls: convertAnytls
|
||||
};
|
||||
|
||||
try {
|
||||
if (!node || typeof node !== 'object')
|
||||
throw new Error('Node must be an object');
|
||||
if (!converters[type])
|
||||
throw new Error('Unsupported protocol: ' + (type || 'unknown'));
|
||||
return makeResult(node, converters[type](node));
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
name: String(node && node.name || 'Node'),
|
||||
type: type || 'unknown',
|
||||
link: '',
|
||||
error: String(e && e.message || e)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function convertProxies(proxies) {
|
||||
return Array.isArray(proxies) ? proxies.map(convertProxy) : [];
|
||||
}
|
||||
|
||||
function normalizeLink(link) {
|
||||
const value = String(link || '').trim();
|
||||
if (value.indexOf('vmess://') === 0) {
|
||||
try {
|
||||
const payload = JSON.parse(utf8FromBase64(value.slice(8)));
|
||||
delete payload.ps;
|
||||
return 'vmess://' + utf8Base64(JSON.stringify(payload));
|
||||
} catch (e) {}
|
||||
}
|
||||
return value.replace(/#.*$/, '');
|
||||
}
|
||||
|
||||
const api = {
|
||||
convertProxy: convertProxy,
|
||||
convertProxies: convertProxies,
|
||||
normalizeLink: normalizeLink
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
module.exports = api;
|
||||
|
||||
if (typeof baseclass !== 'undefined')
|
||||
return baseclass.extend(api);
|
||||
|
||||
return api;
|
||||
@ -0,0 +1,419 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
'require view.daede.clash-converter as clashConverter';
|
||||
'require view.daede.styles as styles';
|
||||
|
||||
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
|
||||
const GENERATOR = '/usr/share/luci-app-daede/gen-dae-config.sh';
|
||||
|
||||
function loadYamlParser() {
|
||||
if (window.jsyaml && window.jsyaml.load)
|
||||
return Promise.resolve(window.jsyaml);
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
const script = document.createElement('script');
|
||||
script.src = L.resource('view/daede/vendor/js-yaml.min.js');
|
||||
script.onload = function() {
|
||||
if (window.jsyaml && window.jsyaml.load)
|
||||
resolve(window.jsyaml);
|
||||
else
|
||||
reject(new Error(_('YAML parser failed to load')));
|
||||
};
|
||||
script.onerror = function() { reject(new Error(_('YAML parser failed to load'))); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function daedEndpoint() {
|
||||
const listen = uci.get('daed', 'config', 'listen_addr') || '0.0.0.0:2023';
|
||||
const match = String(listen).match(/:(\d+)$/);
|
||||
const port = match ? match[1] : '2023';
|
||||
const host = window.location.hostname.indexOf(':') >= 0 ? '[' + window.location.hostname + ']' : window.location.hostname;
|
||||
return 'http://' + host + ':' + port + '/graphql';
|
||||
}
|
||||
|
||||
function graphQL(endpoint, query, variables, token) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (token)
|
||||
headers.Authorization = 'Bearer ' + token;
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ query: query, variables: variables || {} })
|
||||
}).then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error(_('daed returned HTTP %s').format(response.status));
|
||||
return response.json();
|
||||
}).then(function(body) {
|
||||
if (body.errors && body.errors.length)
|
||||
throw new Error(body.errors.map(function(e) { return e.message; }).join('; '));
|
||||
return body.data;
|
||||
});
|
||||
}
|
||||
|
||||
function requestDaedCredentials() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const username = E('input', { 'class': 'cbi-input-text', 'autocomplete': 'username', 'placeholder': _('Username') });
|
||||
const password = E('input', { 'class': 'cbi-input-password', 'type': 'password', 'autocomplete': 'current-password', 'placeholder': _('Password') });
|
||||
const cancel = E('button', { 'class': 'btn cbi-button' }, _('Cancel'));
|
||||
const login = E('button', { 'class': 'btn cbi-button cbi-button-positive' }, _('Sign in and import'));
|
||||
|
||||
cancel.addEventListener('click', function() {
|
||||
password.value = '';
|
||||
ui.hideModal();
|
||||
reject(new Error(_('Import cancelled')));
|
||||
});
|
||||
login.addEventListener('click', function() {
|
||||
if (!username.value || !password.value)
|
||||
return;
|
||||
const result = { username: username.value, password: password.value };
|
||||
password.value = '';
|
||||
ui.hideModal();
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
ui.showModal(_('Sign in to daed'), [
|
||||
E('p', {}, _('Credentials are used only for this import and are not saved.')),
|
||||
E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Username')), E('div', { 'class': 'cbi-value-field' }, username) ]),
|
||||
E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Password')), E('div', { 'class': 'cbi-value-field' }, password) ]),
|
||||
E('div', { 'class': 'right' }, [ cancel, ' ', login ])
|
||||
]);
|
||||
username.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function uniqueNodeTag(existing, index) {
|
||||
let n = index + 1;
|
||||
let tag = 'converted_' + n;
|
||||
while (existing[tag]) {
|
||||
n++;
|
||||
tag = 'converted_' + n;
|
||||
}
|
||||
existing[tag] = true;
|
||||
return tag;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
backend.detectBackend(),
|
||||
uci.load('dae').catch(function() {}),
|
||||
uci.load('daed').catch(function() {}),
|
||||
loadYamlParser()
|
||||
]).then(function(values) {
|
||||
return { ctx: values[0], yaml: values[3] };
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
const state = {
|
||||
results: [],
|
||||
filter: '',
|
||||
target: data.ctx.installed[data.ctx.name] ? data.ctx.name : (data.ctx.installed.dae ? 'dae' : 'daed')
|
||||
};
|
||||
|
||||
const urlInput = E('input', { 'class': 'dd-conv-url', 'placeholder': 'https://example.com/clash.yaml', 'autocomplete': 'off' });
|
||||
const yamlInput = E('textarea', { 'class': 'dd-conv-yaml', 'placeholder': _('Or paste Clash YAML here'), 'spellcheck': 'false' });
|
||||
const parseUrl = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Fetch and parse'));
|
||||
const parsePaste = E('button', { 'class': 'cbi-button' }, _('Parse pasted YAML'));
|
||||
const filterInput = E('input', { 'class': 'dd-conv-filter', 'placeholder': _('Filter name or protocol') });
|
||||
const selectNew = E('button', { 'class': 'cbi-button' }, _('Select compatible new nodes'));
|
||||
const clearSelection = E('button', { 'class': 'cbi-button' }, _('Clear selection'));
|
||||
const targetSelect = E('select', { 'class': 'dd-conv-target' });
|
||||
const importButton = E('button', { 'class': 'cbi-button cbi-button-positive', 'disabled': 'disabled' }, _('Import selected nodes'));
|
||||
const summary = E('span', { 'class': 'dd-meta' }, _('No YAML parsed yet'));
|
||||
const status = E('div', { 'class': 'dd-conv-status' }, '');
|
||||
const resultBody = E('div', { 'class': 'dd-conv-results' });
|
||||
|
||||
[ 'dae', 'daed' ].forEach(function(name) {
|
||||
if (data.ctx.installed[name])
|
||||
targetSelect.appendChild(E('option', { 'value': name, 'selected': state.target === name ? 'selected' : null }, name));
|
||||
});
|
||||
|
||||
const setStatus = function(text, kind) {
|
||||
status.textContent = text || '';
|
||||
status.className = 'dd-conv-status' + (kind ? ' ' + kind : '');
|
||||
};
|
||||
|
||||
const selectedResults = function() {
|
||||
return state.results.filter(function(item) { return item.ok && item.selected && !item.duplicate; });
|
||||
};
|
||||
|
||||
const classifyDaeDuplicates = function() {
|
||||
const existing = {};
|
||||
(uci.sections('dae', 'node') || []).forEach(function(section) {
|
||||
if (section.link)
|
||||
existing[clashConverter.normalizeLink(section.link)] = true;
|
||||
});
|
||||
state.results.forEach(function(item) {
|
||||
item.duplicate = !!(item.ok && existing[clashConverter.normalizeLink(item.link)]);
|
||||
if (item.duplicate)
|
||||
item.selected = false;
|
||||
});
|
||||
};
|
||||
|
||||
const renderResults = function() {
|
||||
while (resultBody.firstChild)
|
||||
resultBody.removeChild(resultBody.firstChild);
|
||||
|
||||
const filter = state.filter.toLowerCase();
|
||||
const shown = state.results.filter(function(item) {
|
||||
return !filter || item.name.toLowerCase().indexOf(filter) >= 0 || item.type.toLowerCase().indexOf(filter) >= 0;
|
||||
});
|
||||
|
||||
shown.forEach(function(item) {
|
||||
const checkbox = E('input', { 'type': 'checkbox' });
|
||||
checkbox.checked = !!item.selected;
|
||||
checkbox.disabled = !item.ok || item.duplicate;
|
||||
checkbox.addEventListener('change', function() {
|
||||
item.selected = checkbox.checked;
|
||||
renderResults();
|
||||
});
|
||||
|
||||
let resultText, resultClass;
|
||||
if (!item.ok) {
|
||||
resultText = item.error;
|
||||
resultClass = 'bad';
|
||||
} else if (item.duplicate) {
|
||||
resultText = _('Already exists, skipped');
|
||||
resultClass = 'dup';
|
||||
} else {
|
||||
resultText = _('Ready to import');
|
||||
resultClass = 'good';
|
||||
}
|
||||
|
||||
resultBody.appendChild(E('div', { 'class': 'dd-conv-result' }, [
|
||||
checkbox,
|
||||
E('span', { 'class': 'dd-conv-name', 'title': item.name }, item.name),
|
||||
E('span', { 'class': 'dd-conv-proto' }, item.type.toUpperCase()),
|
||||
E('span', { 'class': 'dd-conv-result-state ' + resultClass, 'title': resultText }, resultText)
|
||||
]));
|
||||
});
|
||||
|
||||
const compatible = state.results.filter(function(item) { return item.ok; }).length;
|
||||
const unsupported = state.results.length - compatible;
|
||||
const duplicates = state.results.filter(function(item) { return item.duplicate; }).length;
|
||||
summary.textContent = _('Detected %d · compatible %d · duplicates %d · incompatible %d')
|
||||
.format(state.results.length, compatible, duplicates, unsupported);
|
||||
importButton.textContent = _('Import selected nodes (%d)').format(selectedResults().length);
|
||||
importButton.disabled = selectedResults().length === 0;
|
||||
};
|
||||
|
||||
const acceptYaml = function(text) {
|
||||
let documentValue;
|
||||
try {
|
||||
documentValue = data.yaml.load(text);
|
||||
} catch (e) {
|
||||
setStatus(_('YAML parse failed: %s').format(e.message || e), 'err');
|
||||
return;
|
||||
}
|
||||
if (!documentValue || !Array.isArray(documentValue.proxies) || documentValue.proxies.length === 0) {
|
||||
setStatus(_('No top-level proxies list was found'), 'err');
|
||||
return;
|
||||
}
|
||||
|
||||
state.results = clashConverter.convertProxies(documentValue.proxies).map(function(item) {
|
||||
item.selected = item.ok;
|
||||
item.duplicate = false;
|
||||
return item;
|
||||
});
|
||||
if (state.target === 'dae')
|
||||
classifyDaeDuplicates();
|
||||
setStatus(_('Conversion preview is ready'), 'ok');
|
||||
renderResults();
|
||||
};
|
||||
|
||||
parseUrl.addEventListener('click', function() {
|
||||
const value = urlInput.value.trim();
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
setStatus(_('Enter an HTTP or HTTPS subscription URL'), 'err');
|
||||
return;
|
||||
}
|
||||
parseUrl.disabled = true;
|
||||
setStatus(_('Fetching subscription…'));
|
||||
fs.exec(FETCHER, [ value ]).then(function(res) {
|
||||
if (!res || res.code !== 0)
|
||||
throw new Error((res && (res.stderr || res.stdout)) || _('Fetch failed'));
|
||||
acceptYaml(res.stdout || '');
|
||||
}).catch(function(e) {
|
||||
setStatus(_('Fetch failed: %s').format(String(e.message || e).trim()), 'err');
|
||||
}).finally(function() { parseUrl.disabled = false; });
|
||||
});
|
||||
|
||||
parsePaste.addEventListener('click', function() {
|
||||
const value = yamlInput.value.trim();
|
||||
if (!value) {
|
||||
setStatus(_('Paste Clash YAML first'), 'err');
|
||||
return;
|
||||
}
|
||||
acceptYaml(value);
|
||||
});
|
||||
|
||||
filterInput.addEventListener('input', function() {
|
||||
state.filter = filterInput.value;
|
||||
renderResults();
|
||||
});
|
||||
selectNew.addEventListener('click', function() {
|
||||
state.results.forEach(function(item) { item.selected = item.ok && !item.duplicate; });
|
||||
renderResults();
|
||||
});
|
||||
clearSelection.addEventListener('click', function() {
|
||||
state.results.forEach(function(item) { item.selected = false; });
|
||||
renderResults();
|
||||
});
|
||||
targetSelect.addEventListener('change', function() {
|
||||
state.target = targetSelect.value;
|
||||
state.results.forEach(function(item) { item.duplicate = false; });
|
||||
if (state.target === 'dae')
|
||||
classifyDaeDuplicates();
|
||||
renderResults();
|
||||
});
|
||||
|
||||
const importDae = function(items) {
|
||||
const usedTags = {};
|
||||
(uci.sections('dae', 'node') || []).forEach(function(section) {
|
||||
if (section.tag)
|
||||
usedTags[section.tag] = true;
|
||||
});
|
||||
items.forEach(function(item, index) {
|
||||
const sid = uci.add('dae', 'node');
|
||||
uci.set('dae', sid, 'tag', uniqueNodeTag(usedTags, index));
|
||||
uci.set('dae', sid, 'link', item.link);
|
||||
uci.set('dae', sid, 'enabled', '1');
|
||||
});
|
||||
|
||||
return uci.save().then(function() {
|
||||
return uci.changes().then(function(changes) {
|
||||
return changes && Object.keys(changes).length ? uci.apply() : null;
|
||||
});
|
||||
}).then(function() {
|
||||
return fs.exec(GENERATOR, [ 'generate' ]);
|
||||
}).then(function(res) {
|
||||
if (res && res.code !== 0)
|
||||
throw new Error(res.stderr || res.stdout || ('exit ' + res.code));
|
||||
items.forEach(function(item) { item.duplicate = true; item.selected = false; });
|
||||
return { added: items.length, duplicates: 0, failed: 0 };
|
||||
});
|
||||
};
|
||||
|
||||
const importDaed = function(items) {
|
||||
let credentials;
|
||||
let token = '';
|
||||
const endpoint = daedEndpoint();
|
||||
return requestDaedCredentials().then(function(value) {
|
||||
credentials = value;
|
||||
return graphQL(endpoint,
|
||||
'query Login($username:String!,$password:String!){token(username:$username,password:$password)}',
|
||||
credentials);
|
||||
}).then(function(login) {
|
||||
token = login.token;
|
||||
credentials.password = '';
|
||||
credentials = null;
|
||||
return graphQL(endpoint, 'query ExistingNodes{nodes(first:10000){edges{link}}}', {}, token);
|
||||
}).then(function(existingData) {
|
||||
const existing = {};
|
||||
(existingData.nodes.edges || []).forEach(function(node) {
|
||||
existing[clashConverter.normalizeLink(node.link)] = true;
|
||||
});
|
||||
const fresh = items.filter(function(item) {
|
||||
const duplicate = !!existing[clashConverter.normalizeLink(item.link)];
|
||||
if (duplicate) {
|
||||
item.duplicate = true;
|
||||
item.selected = false;
|
||||
}
|
||||
return !duplicate;
|
||||
});
|
||||
if (!fresh.length)
|
||||
return { importNodes: [], preDuplicates: items.length };
|
||||
return graphQL(endpoint,
|
||||
'mutation ImportNodes($args:[ImportArgument!]!){importNodes(rollbackError:false,args:$args){link error node{id}}}',
|
||||
{ args: fresh.map(function(item) { return { link: item.link }; }) }, token)
|
||||
.then(function(value) { value.preDuplicates = items.length - fresh.length; return value; });
|
||||
}).then(function(result) {
|
||||
const rows = result.importNodes || [];
|
||||
const importedItems = {};
|
||||
items.forEach(function(item) { importedItems[clashConverter.normalizeLink(item.link)] = item; });
|
||||
rows.forEach(function(row) {
|
||||
if (!row.error || row.error === 'node already exists') {
|
||||
const item = importedItems[clashConverter.normalizeLink(row.link)];
|
||||
if (item) {
|
||||
item.duplicate = true;
|
||||
item.selected = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
const failed = rows.filter(function(row) { return !!row.error && row.error !== 'node already exists'; }).length;
|
||||
const apiDuplicates = rows.filter(function(row) { return row.error === 'node already exists'; }).length;
|
||||
const duplicates = (result.preDuplicates || 0) + apiDuplicates;
|
||||
const added = rows.filter(function(row) { return !row.error; }).length;
|
||||
return { added: added, duplicates: duplicates, failed: failed };
|
||||
}).finally(function() {
|
||||
if (credentials)
|
||||
credentials.password = '';
|
||||
credentials = null;
|
||||
token = '';
|
||||
});
|
||||
};
|
||||
|
||||
importButton.addEventListener('click', function() {
|
||||
const items = selectedResults();
|
||||
if (!items.length)
|
||||
return;
|
||||
importButton.disabled = true;
|
||||
setStatus(_('Importing selected nodes…'));
|
||||
const action = state.target === 'dae' ? importDae(items) : importDaed(items);
|
||||
action.then(function(result) {
|
||||
setStatus(_('Import complete: added %d, duplicates %d, failed %d')
|
||||
.format(result.added, result.duplicates, result.failed), result.failed ? 'err' : 'ok');
|
||||
renderResults();
|
||||
}).catch(function(e) {
|
||||
setStatus(_('Import failed: %s').format(e.message || e), 'err');
|
||||
}).finally(function() {
|
||||
importButton.disabled = selectedResults().length === 0;
|
||||
});
|
||||
});
|
||||
|
||||
renderResults();
|
||||
|
||||
return E('div', { 'class': 'dd-wrap dd-converter' }, [
|
||||
E('style', {}, styles.CSS),
|
||||
E('div', { 'class': 'dd-card dd-conv-card' }, [
|
||||
E('h3', {}, _('Subscription Converter')),
|
||||
E('p', { 'class': 'dd-settings-descr' }, _('Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved.')),
|
||||
E('h4', { 'class': 'dd-card-title' }, _('1. Input Clash YAML')),
|
||||
E('div', { 'class': 'dd-conv-url-row' }, [ urlInput, parseUrl ]),
|
||||
E('div', { 'class': 'dd-conv-or' }, _('or')),
|
||||
yamlInput,
|
||||
E('div', { 'class': 'dd-actions' }, [ parsePaste ])
|
||||
]),
|
||||
E('div', { 'class': 'dd-card dd-conv-card' }, [
|
||||
E('div', { 'class': 'dd-conv-heading' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('2. Conversion Preview')),
|
||||
summary
|
||||
]),
|
||||
E('div', { 'class': 'dd-conv-toolbar' }, [ selectNew, clearSelection, filterInput ]),
|
||||
resultBody
|
||||
]),
|
||||
E('div', { 'class': 'dd-card dd-conv-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('3. Import Selected Nodes')),
|
||||
E('div', { 'class': 'dd-conv-import' }, [
|
||||
E('label', {}, _('Target backend')),
|
||||
targetSelect,
|
||||
importButton
|
||||
]),
|
||||
status
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -178,7 +178,31 @@ const CSS = [
|
||||
'.dd-settings-card .cbi-section-table-row>td:nth-child(2){flex:1 1 auto !important}',
|
||||
'.dd-settings-card .cbi-section-table-row>td:nth-child(3){flex:0 0 auto !important;text-align:center}',
|
||||
'.dd-settings-card .cbi-section-table-row>td.cbi-section-actions{flex:0 0 auto !important;display:flex;gap:4px}',
|
||||
'}'
|
||||
'}',
|
||||
'.dd-conv-card{padding:14px 16px;margin-bottom:10px}',
|
||||
'.dd-conv-card h3{font-size:18px;margin:0 0 6px}',
|
||||
'.dd-conv-url-row,.dd-conv-import,.dd-conv-toolbar,.dd-conv-heading{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
||||
'.dd-conv-url-row input,.dd-conv-filter,.dd-conv-target,.dd-conv-yaml{box-sizing:border-box;border:1px solid rgba(128,128,128,.28);border-radius:6px;background:transparent;color:inherit;font-size:12px;padding:7px 9px}',
|
||||
'.dd-conv-url-row input{flex:1;min-width:240px}',
|
||||
'.dd-conv-yaml{width:100%;min-height:150px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.5}',
|
||||
'.dd-conv-or{text-align:center;font-size:11px;opacity:.5;margin:7px 0}',
|
||||
'.dd-conv-heading{justify-content:space-between;margin-bottom:8px}',
|
||||
'.dd-conv-heading .dd-card-title{margin:0}',
|
||||
'.dd-conv-toolbar{margin-bottom:8px}',
|
||||
'.dd-conv-filter{margin-left:auto;min-width:220px}',
|
||||
'.dd-conv-results{border:1px solid rgba(128,128,128,.18);border-radius:7px;max-height:420px;overflow:auto}',
|
||||
'.dd-conv-result{display:grid;grid-template-columns:22px minmax(100px,1fr) 90px minmax(130px,1.3fr);gap:8px;align-items:center;padding:7px 9px;border-bottom:1px solid rgba(128,128,128,.13);font-size:12px}',
|
||||
'.dd-conv-result:last-child{border-bottom:0}',
|
||||
'.dd-conv-result input{appearance:checkbox !important;-webkit-appearance:checkbox !important;accent-color:#4aa065;width:15px;height:15px}',
|
||||
'.dd-conv-name,.dd-conv-result-state{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
'.dd-conv-proto{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;opacity:.7}',
|
||||
'.dd-conv-result-state.good,.dd-conv-status.ok{color:#3da66a}',
|
||||
'.dd-conv-result-state.dup{opacity:.55}',
|
||||
'.dd-conv-result-state.bad,.dd-conv-status.err{color:#d96d6d}',
|
||||
'.dd-conv-import label{font-size:12px;font-weight:600;opacity:.72}',
|
||||
'.dd-conv-import .cbi-button{margin-left:auto}',
|
||||
'.dd-conv-status{margin-top:9px;font-size:11.5px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;min-height:16px}',
|
||||
'@media(max-width:640px){.dd-conv-filter{margin-left:0;width:100%}.dd-conv-result{grid-template-columns:20px minmax(90px,1fr) 68px}.dd-conv-result-state{grid-column:2/4;font-size:11px}.dd-conv-import .cbi-button{margin-left:0;width:100%}.dd-conv-url-row input{min-width:100%;width:100%}.dd-conv-url-row .cbi-button{width:100%}}'
|
||||
].join('');
|
||||
|
||||
return baseclass.extend({ CSS: CSS });
|
||||
|
||||
21
luci-app-daede/htdocs/luci-static/resources/view/daede/vendor/js-yaml.LICENSE
vendored
Normal file
21
luci-app-daede/htdocs/luci-static/resources/view/daede/vendor/js-yaml.LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (C) 2011-2015 by Vitaly Puzrin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
33
luci-app-daede/htdocs/luci-static/resources/view/daede/vendor/js-yaml.min.js
vendored
Normal file
33
luci-app-daede/htdocs/luci-static/resources/view/daede/vendor/js-yaml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -421,6 +421,182 @@ msgstr ""
|
||||
msgid "Starting…"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Name is required"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Group name must be unique"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Updating…"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Subscriptions updated (dae reloaded)"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Save manual config"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Start failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Subscription Converter"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved."
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "1. Input Clash YAML"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetch and parse"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Or paste Clash YAML here"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Parse pasted YAML"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "2. Conversion Preview"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Select compatible new nodes"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Clear selection"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Filter name or protocol"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "3. Import Selected Nodes"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Target backend"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import selected nodes (%d)"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Detected %d · compatible %d · duplicates %d · incompatible %d"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Ready to import"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Already exists, skipped"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Sign in to daed"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Credentials are used only for this import and are not saved."
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Sign in and import"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
#: converter.js
|
||||
msgid "YAML parser failed to load"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "daed returned HTTP %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import cancelled"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import selected nodes"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "No YAML parsed yet"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "YAML parse failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "No top-level proxies list was found"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Conversion preview is ready"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Enter an HTTP or HTTPS subscription URL"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetching subscription…"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetch failed"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetch failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Paste Clash YAML first"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "or"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Importing selected nodes…"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import complete: added %d, duplicates %d, failed %d"
|
||||
msgstr ""
|
||||
|
||||
@ -689,6 +689,21 @@ msgstr "已保存并应用"
|
||||
msgid "Saved"
|
||||
msgstr "已保存"
|
||||
|
||||
msgid "Name is required"
|
||||
msgstr "名称不能为空"
|
||||
|
||||
msgid "Group name must be unique"
|
||||
msgstr "分组名称必须唯一"
|
||||
|
||||
msgid "Updating…"
|
||||
msgstr "更新中…"
|
||||
|
||||
msgid "Subscriptions updated (dae reloaded)"
|
||||
msgstr "订阅已更新(dae 已重载)"
|
||||
|
||||
msgid "Save manual config"
|
||||
msgstr "保存手动配置"
|
||||
|
||||
msgid "Saved · started"
|
||||
msgstr "已保存并启动"
|
||||
|
||||
@ -697,3 +712,121 @@ msgstr "启动中…"
|
||||
|
||||
msgid "Start failed: %s"
|
||||
msgstr "启动失败:%s"
|
||||
|
||||
# ---- converter.js ----
|
||||
msgid "Subscription Converter"
|
||||
msgstr "订阅转换"
|
||||
|
||||
msgid "Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved."
|
||||
msgstr "将 Clash YAML 转换为分享链接,预览后把所选节点导入 dae 或 daed。输入内容和凭据不会保存。"
|
||||
|
||||
msgid "1. Input Clash YAML"
|
||||
msgstr "1. 输入 Clash YAML"
|
||||
|
||||
msgid "Fetch and parse"
|
||||
msgstr "获取并解析"
|
||||
|
||||
msgid "Or paste Clash YAML here"
|
||||
msgstr "或在此粘贴 Clash YAML"
|
||||
|
||||
msgid "Parse pasted YAML"
|
||||
msgstr "解析粘贴内容"
|
||||
|
||||
msgid "2. Conversion Preview"
|
||||
msgstr "2. 转换预览"
|
||||
|
||||
msgid "Select compatible new nodes"
|
||||
msgstr "选择可导入的新节点"
|
||||
|
||||
msgid "Clear selection"
|
||||
msgstr "清空选择"
|
||||
|
||||
msgid "Filter name or protocol"
|
||||
msgstr "筛选节点名称或协议"
|
||||
|
||||
msgid "3. Import Selected Nodes"
|
||||
msgstr "3. 导入所选节点"
|
||||
|
||||
msgid "Target backend"
|
||||
msgstr "目标后端"
|
||||
|
||||
msgid "Import selected nodes (%d)"
|
||||
msgstr "导入所选节点(%d)"
|
||||
|
||||
msgid "Detected %d · compatible %d · duplicates %d · incompatible %d"
|
||||
msgstr "检测到 %d 个 · 可导入 %d 个 · 重复 %d 个 · 不兼容 %d 个"
|
||||
|
||||
msgid "Ready to import"
|
||||
msgstr "可导入"
|
||||
|
||||
msgid "Already exists, skipped"
|
||||
msgstr "已存在,跳过"
|
||||
|
||||
msgid "Sign in to daed"
|
||||
msgstr "登录 daed"
|
||||
|
||||
msgid "Credentials are used only for this import and are not saved."
|
||||
msgstr "凭据仅用于本次导入,不会保存。"
|
||||
|
||||
msgid "Sign in and import"
|
||||
msgstr "登录并导入"
|
||||
|
||||
msgid "YAML parser failed to load"
|
||||
msgstr "YAML 解析器加载失败"
|
||||
|
||||
msgid "daed returned HTTP %s"
|
||||
msgstr "daed 返回 HTTP %s"
|
||||
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Import cancelled"
|
||||
msgstr "导入已取消"
|
||||
|
||||
msgid "Import selected nodes"
|
||||
msgstr "导入所选节点"
|
||||
|
||||
msgid "No YAML parsed yet"
|
||||
msgstr "尚未解析 YAML"
|
||||
|
||||
msgid "YAML parse failed: %s"
|
||||
msgstr "YAML 解析失败:%s"
|
||||
|
||||
msgid "No top-level proxies list was found"
|
||||
msgstr "未找到顶层 proxies 列表"
|
||||
|
||||
msgid "Conversion preview is ready"
|
||||
msgstr "转换预览已就绪"
|
||||
|
||||
msgid "Enter an HTTP or HTTPS subscription URL"
|
||||
msgstr "请输入 HTTP 或 HTTPS 订阅链接"
|
||||
|
||||
msgid "Fetching subscription…"
|
||||
msgstr "获取订阅中…"
|
||||
|
||||
msgid "Fetch failed"
|
||||
msgstr "获取失败"
|
||||
|
||||
msgid "Fetch failed: %s"
|
||||
msgstr "获取失败:%s"
|
||||
|
||||
msgid "Paste Clash YAML first"
|
||||
msgstr "请先粘贴 Clash YAML"
|
||||
|
||||
msgid "or"
|
||||
msgstr "或"
|
||||
|
||||
msgid "Importing selected nodes…"
|
||||
msgstr "导入所选节点中…"
|
||||
|
||||
msgid "Import failed: %s"
|
||||
msgstr "导入失败:%s"
|
||||
|
||||
msgid "Import complete: added %d, duplicates %d, failed %d"
|
||||
msgstr "导入完成:新增 %d,重复 %d,失败 %d"
|
||||
|
||||
@ -374,14 +374,8 @@ msgstr "自动滚动"
|
||||
msgid "Pause"
|
||||
msgstr "暂停"
|
||||
|
||||
msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
msgid "Node Status"
|
||||
msgstr "节点状态"
|
||||
|
||||
msgid "Proxy Traffic"
|
||||
msgstr "代理流量"
|
||||
msgid "Filter (substring)"
|
||||
msgstr "过滤(子串)"
|
||||
|
||||
msgid "Clear View"
|
||||
msgstr "清空显示"
|
||||
@ -436,14 +430,227 @@ msgid "Edit config DSL — subscriptions, nodes, routing, DNS. Load template via
|
||||
msgstr "编辑 dae DSL 配置——订阅、节点、路由、DNS。通过「从示例初始化」载入模板。保存前务必替换占位订阅链接。图形化管理请切换至 daed。"
|
||||
|
||||
# ---- config.js: Proxy Health ----
|
||||
msgid "Check Proxy Status"
|
||||
msgstr "检测代理状态"
|
||||
msgid "Test YouTube"
|
||||
msgstr "测试 YouTube"
|
||||
|
||||
msgid "OK, %d flows through proxy"
|
||||
msgstr "正常,%d 条代理流量"
|
||||
msgid "Testing…"
|
||||
msgstr "测试中…"
|
||||
|
||||
msgid "No proxy traffic"
|
||||
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 "固定链接,无需更新。"
|
||||
|
||||
# ---- dae.js: i18n cleanup (was Chinese msgid) ----
|
||||
msgid "One node share link per line — ss, vmess, vless, trojan, tuic, hysteria2, socks5."
|
||||
@ -470,9 +677,6 @@ msgstr "高级设置"
|
||||
msgid "auto-generated"
|
||||
msgstr "自动生成"
|
||||
|
||||
msgid "Save and Apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Save config"
|
||||
msgstr "保存配置"
|
||||
|
||||
@ -485,6 +689,21 @@ msgstr "已保存并应用"
|
||||
msgid "Saved"
|
||||
msgstr "已保存"
|
||||
|
||||
msgid "Name is required"
|
||||
msgstr "名称不能为空"
|
||||
|
||||
msgid "Group name must be unique"
|
||||
msgstr "分组名称必须唯一"
|
||||
|
||||
msgid "Updating…"
|
||||
msgstr "更新中…"
|
||||
|
||||
msgid "Subscriptions updated (dae reloaded)"
|
||||
msgstr "订阅已更新(dae 已重载)"
|
||||
|
||||
msgid "Save manual config"
|
||||
msgstr "保存手动配置"
|
||||
|
||||
msgid "Saved · started"
|
||||
msgstr "已保存并启动"
|
||||
|
||||
@ -493,3 +712,121 @@ msgstr "启动中…"
|
||||
|
||||
msgid "Start failed: %s"
|
||||
msgstr "启动失败:%s"
|
||||
|
||||
# ---- converter.js ----
|
||||
msgid "Subscription Converter"
|
||||
msgstr "订阅转换"
|
||||
|
||||
msgid "Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved."
|
||||
msgstr "将 Clash YAML 转换为分享链接,预览后把所选节点导入 dae 或 daed。输入内容和凭据不会保存。"
|
||||
|
||||
msgid "1. Input Clash YAML"
|
||||
msgstr "1. 输入 Clash YAML"
|
||||
|
||||
msgid "Fetch and parse"
|
||||
msgstr "获取并解析"
|
||||
|
||||
msgid "Or paste Clash YAML here"
|
||||
msgstr "或在此粘贴 Clash YAML"
|
||||
|
||||
msgid "Parse pasted YAML"
|
||||
msgstr "解析粘贴内容"
|
||||
|
||||
msgid "2. Conversion Preview"
|
||||
msgstr "2. 转换预览"
|
||||
|
||||
msgid "Select compatible new nodes"
|
||||
msgstr "选择可导入的新节点"
|
||||
|
||||
msgid "Clear selection"
|
||||
msgstr "清空选择"
|
||||
|
||||
msgid "Filter name or protocol"
|
||||
msgstr "筛选节点名称或协议"
|
||||
|
||||
msgid "3. Import Selected Nodes"
|
||||
msgstr "3. 导入所选节点"
|
||||
|
||||
msgid "Target backend"
|
||||
msgstr "目标后端"
|
||||
|
||||
msgid "Import selected nodes (%d)"
|
||||
msgstr "导入所选节点(%d)"
|
||||
|
||||
msgid "Detected %d · compatible %d · duplicates %d · incompatible %d"
|
||||
msgstr "检测到 %d 个 · 可导入 %d 个 · 重复 %d 个 · 不兼容 %d 个"
|
||||
|
||||
msgid "Ready to import"
|
||||
msgstr "可导入"
|
||||
|
||||
msgid "Already exists, skipped"
|
||||
msgstr "已存在,跳过"
|
||||
|
||||
msgid "Sign in to daed"
|
||||
msgstr "登录 daed"
|
||||
|
||||
msgid "Credentials are used only for this import and are not saved."
|
||||
msgstr "凭据仅用于本次导入,不会保存。"
|
||||
|
||||
msgid "Sign in and import"
|
||||
msgstr "登录并导入"
|
||||
|
||||
msgid "YAML parser failed to load"
|
||||
msgstr "YAML 解析器加载失败"
|
||||
|
||||
msgid "daed returned HTTP %s"
|
||||
msgstr "daed 返回 HTTP %s"
|
||||
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Import cancelled"
|
||||
msgstr "导入已取消"
|
||||
|
||||
msgid "Import selected nodes"
|
||||
msgstr "导入所选节点"
|
||||
|
||||
msgid "No YAML parsed yet"
|
||||
msgstr "尚未解析 YAML"
|
||||
|
||||
msgid "YAML parse failed: %s"
|
||||
msgstr "YAML 解析失败:%s"
|
||||
|
||||
msgid "No top-level proxies list was found"
|
||||
msgstr "未找到顶层 proxies 列表"
|
||||
|
||||
msgid "Conversion preview is ready"
|
||||
msgstr "转换预览已就绪"
|
||||
|
||||
msgid "Enter an HTTP or HTTPS subscription URL"
|
||||
msgstr "请输入 HTTP 或 HTTPS 订阅链接"
|
||||
|
||||
msgid "Fetching subscription…"
|
||||
msgstr "获取订阅中…"
|
||||
|
||||
msgid "Fetch failed"
|
||||
msgstr "获取失败"
|
||||
|
||||
msgid "Fetch failed: %s"
|
||||
msgstr "获取失败:%s"
|
||||
|
||||
msgid "Paste Clash YAML first"
|
||||
msgstr "请先粘贴 Clash YAML"
|
||||
|
||||
msgid "or"
|
||||
msgstr "或"
|
||||
|
||||
msgid "Importing selected nodes…"
|
||||
msgstr "导入所选节点中…"
|
||||
|
||||
msgid "Import failed: %s"
|
||||
msgstr "导入失败:%s"
|
||||
|
||||
msgid "Import complete: added %d, duplicates %d, failed %d"
|
||||
msgstr "导入完成:新增 %d,重复 %d,失败 %d"
|
||||
|
||||
37
luci-app-daede/root/usr/share/luci-app-daede/fetch-clash-yaml.sh
Executable file
37
luci-app-daede/root/usr/share/luci-app-daede/fetch-clash-yaml.sh
Executable file
@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -eu
|
||||
|
||||
URL="${1:-}"
|
||||
MAX_BYTES=5242880
|
||||
FETCH_BIN="${DAEDE_FETCH_BIN:-uclient-fetch}"
|
||||
|
||||
case "$URL" in
|
||||
http://*|https://*) ;;
|
||||
*)
|
||||
echo "Subscription URL must use HTTP or HTTPS" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP="$(mktemp /tmp/daede-clash-yaml.XXXXXX)"
|
||||
trap 'rm -f "$TMP"' EXIT INT TERM
|
||||
chmod 600 "$TMP"
|
||||
|
||||
if ! "$FETCH_BIN" -q -T 20 -U "ClashMeta" -O "$TMP" "$URL"; then
|
||||
echo "Failed to fetch subscription" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
SIZE="$(wc -c <"$TMP" | tr -d ' ')"
|
||||
if [ "$SIZE" -eq 0 ]; then
|
||||
echo "Subscription response is empty" >&2
|
||||
exit 4
|
||||
fi
|
||||
if [ "$SIZE" -gt "$MAX_BYTES" ]; then
|
||||
echo "Subscription response is too large (maximum 5 MiB)" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
cat "$TMP"
|
||||
@ -17,9 +17,17 @@
|
||||
"path": "daede/config"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/converter": {
|
||||
"title": "Subscription Converter",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/converter"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/updates": {
|
||||
"title": "Updates",
|
||||
"order": 20,
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/updates"
|
||||
@ -27,7 +35,7 @@
|
||||
},
|
||||
"admin/services/daede/log": {
|
||||
"title": "Log",
|
||||
"order": 30,
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/log"
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"/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/fetch-clash-yaml.sh *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh generate": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ]
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user