🎄 Sync 2026-06-14 03:16:16

This commit is contained in:
github-actions[bot] 2026-06-14 03:16:16 +08:00
parent 8eb87f53b0
commit 79dca49776
13 changed files with 1477 additions and 24 deletions

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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
});

View File

@ -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 });

View 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.

File diff suppressed because one or more lines are too long

View File

@ -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 ""

View File

@ -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"

View File

@ -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"

View 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"

View File

@ -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"

View File

@ -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" ]
},