🎄 Sync 2026-06-20 00:22:22

This commit is contained in:
github-actions[bot] committed 2026-06-20 00:22:22 +08:00
1 parent 49943e94f1
commit 985f8de6e8
12 files changed
+295 -105

No files matched your search

@@ -154,6 +154,10 @@ function findManagedGroup(groups, record) {
}) || null;
}
function findGroupByName(groups, name) {
return (groups || []).find(function(group) { return group.name === name; }) || null;
}
const api = {
deriveAirportName: deriveAirportName,
nextPastedName: nextPastedName,
@@ -167,6 +171,7 @@ const api = {
airportSectionValues: airportSectionValues,
matchAirport: matchAirport,
findManagedGroup: findManagedGroup,
findGroupByName: findGroupByName,
safeOldNodeIds: safeOldNodeIds
};
@@ -225,7 +225,8 @@ function isMetadataProxy(node) {
if (!name)
return false;
return /^(?:traffic|bandwidth|expire|expiry|expiration|subscription(?:\s+info)?|official\s+website|website)\s*[::|]/i.test(name) ||
return /(?:官网|QQ|流量|续费|应急|重置|到期|过期|剩余|套餐)/i.test(name) ||
/^(?:traffic|bandwidth|expire|expiry|expiration|subscription(?:\s+info)?|official\s+website|website)\s*[::|]/i.test(name) ||
/^(?:(?:剩余|可用|已用|总)?流量|套餐到期|到期时间|到期日|过期时间|有效期|订阅信息)\s*[::|]/i.test(name) ||
/^(?:官方网站|官网地址|官方网址|网站地址)\s*[::|]/i.test(name) ||
/^(?:加入|联系|客服)?\s*QQ\s*(?:群|交流群|客服|联系)\s*[::|]/i.test(name);
@@ -8,6 +8,7 @@
'require view.daede.airport-sync as airportSync';
'require view.daede.backend as backend';
'require view.daede.clash-converter as clashConverter';
'require view.daede.daed-session as daedSession';
'require view.daede.styles as styles';
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
@@ -125,16 +126,37 @@ function requestDaedCredentials() {
resolve(result);
});
ui.showModal(_('Sign in to daed'), [
E('p', {}, _('Credentials are used only for this import and are not saved.')),
ui.showModal(_('Sign in to daed'), [ E('div', { 'class': 'dd-daed-login' }, [
E('p', {}, _('The password is not saved. Sign-in is remembered in this browser for up to 30 days.')),
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 ])
]);
E('div', { 'class': 'right dd-daed-login-actions' }, [ cancel, ' ', login ])
]) ]);
username.focus();
});
}
function requestDaedToken(endpoint, forceLogin) {
const cached = forceLogin ? '' : daedSession.load(window.localStorage);
if (cached)
return Promise.resolve({ token: cached, cached: true });
let credentials;
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) {
daedSession.save(window.localStorage, login.token);
return { token: login.token, cached: false };
}).finally(function() {
if (credentials)
credentials.password = '';
credentials = null;
});
}
function applyUciChanges() {
return uci.save().then(function() {
return uci.changes().then(function(changes) {
@@ -227,6 +249,9 @@ return view.extend({
const targetSelect = E('select', { 'class': 'dd-conv-target' });
const airportName = E('input', { 'class': 'dd-conv-airport-name', 'placeholder': _('Group name'), 'autocomplete': 'off' });
const importButton = E('button', { 'class': 'cbi-button cbi-button-positive', 'disabled': 'disabled' }, _('Import node group'));
const importTitle = E('h4', { 'class': 'dd-card-title' });
const importDescription = E('p', { 'class': 'dd-settings-descr' });
const airportNameLabel = E('label');
const summary = E('div', { 'class': 'dd-conv-summary' });
const groupSummary = E('div', { 'class': 'dd-conv-group-summary' });
const sourceStatus = E('div', { 'class': 'dd-conv-status' }, '');
@@ -271,6 +296,65 @@ return view.extend({
});
};
const updateResultSummary = function() {
const isDaed = state.target === 'daed';
importTitle.textContent = isDaed ? _('3. Import Subscription') : _('3. Import Node Group');
importDescription.textContent = isDaed
? _('Import selected nodes as one subscription and add it to the default proxy group.')
: _('Import the selected nodes as one named group, so large airport node lists stay easy to manage.');
airportNameLabel.textContent = isDaed ? _('Subscription name') : _('Group name');
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;
while (summary.firstChild)
summary.removeChild(summary.firstChild);
[
[ _('Detected'), state.results.length + state.ignoredInfo ],
[ _('Compatible'), compatible ],
[ _('Duplicates'), duplicates ],
[ _('Incompatible'), unsupported ],
[ _('Ignored info'), state.ignoredInfo ]
].forEach(function(item) {
summary.appendChild(E('span', { 'class': 'dd-conv-summary-item' }, [
E('span', { 'class': 'dd-conv-summary-label' }, item[0]),
E('strong', {}, String(item[1]))
]));
});
const count = selectedResults().length;
const name = airportName.value.trim();
const existing = currentAirport();
const validName = airportSync.isGroupNameValid(name, state.target);
if (!name) {
groupSummary.textContent = isDaed
? _('Enter a subscription name to import the selected nodes.')
: _('Enter a group name to import the selected nodes.');
groupSummary.className = 'dd-conv-group-summary';
} else if (!validName) {
groupSummary.textContent = _('dae group names may only contain letters, numbers, underscores, and hyphens, and must start with a letter or underscore.');
groupSummary.className = 'dd-conv-group-summary err';
} else if (isDaed && existing && count === 0) {
groupSummary.textContent = _('Subscription "%s" is available on daed. Select nodes above to update it.').format(name);
groupSummary.className = 'dd-conv-group-summary';
} else if (isDaed) {
groupSummary.textContent = existing
? _('Update subscription "%s" with %d selected nodes in default group "proxy".').format(name, count)
: _('Import %d selected nodes as subscription "%s" into default group "proxy".').format(count, name);
groupSummary.className = 'dd-conv-group-summary';
} else if (existing && count === 0) {
groupSummary.textContent = _('Node group "%s" is available on %s. Select nodes above to update it.').format(name, state.target);
groupSummary.className = 'dd-conv-group-summary';
} else {
groupSummary.textContent = existing
? _('Update node group "%s": replace it with %d selected nodes on %s.').format(name, count, state.target)
: _('Import %d selected nodes into group "%s" on %s.').format(count, name, state.target);
groupSummary.className = 'dd-conv-group-summary';
}
importButton.textContent = isDaed
? (existing ? _('Update subscription (%d)').format(count) : _('Import subscription (%d)').format(count))
: (existing ? _('Update node group (%d)').format(count) : _('Import node group (%d)').format(count));
importButton.disabled = count === 0 || !validName;
};
const renderResults = function() {
while (resultBody.firstChild)
resultBody.removeChild(resultBody.firstChild);
@@ -286,7 +370,7 @@ return view.extend({
checkbox.disabled = !item.ok;
checkbox.addEventListener('change', function() {
item.selected = checkbox.checked;
renderResults();
updateResultSummary();
});
let resultText, resultClass;
@@ -313,46 +397,7 @@ return view.extend({
if (!shown.length)
resultBody.appendChild(E('div', { 'class': 'dd-conv-empty' }, _('No nodes to preview yet. Fetch or paste Clash YAML above.')));
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;
while (summary.firstChild)
summary.removeChild(summary.firstChild);
[
[ _('Detected'), state.results.length + state.ignoredInfo ],
[ _('Compatible'), compatible ],
[ _('Duplicates'), duplicates ],
[ _('Incompatible'), unsupported ],
[ _('Ignored info'), state.ignoredInfo ]
].forEach(function(item) {
summary.appendChild(E('span', { 'class': 'dd-conv-summary-item' }, [
E('span', { 'class': 'dd-conv-summary-label' }, item[0]),
E('strong', {}, String(item[1]))
]));
});
const count = selectedResults().length;
const name = airportName.value.trim();
const existing = currentAirport();
const validName = airportSync.isGroupNameValid(name, state.target);
if (!name) {
groupSummary.textContent = _('Enter a group name to import the selected nodes.');
groupSummary.className = 'dd-conv-group-summary';
} else if (!validName) {
groupSummary.textContent = _('dae group names may only contain letters, numbers, underscores, and hyphens, and must start with a letter or underscore.');
groupSummary.className = 'dd-conv-group-summary err';
} else if (existing && count === 0) {
groupSummary.textContent = _('Node group "%s" is available on %s. Select nodes above to update it.').format(name, state.target);
groupSummary.className = 'dd-conv-group-summary';
} else {
groupSummary.textContent = existing
? _('Update node group "%s": replace it with %d selected nodes on %s.').format(name, count, state.target)
: _('Import %d selected nodes into group "%s" on %s.').format(count, name, state.target);
groupSummary.className = 'dd-conv-group-summary';
}
importButton.textContent = existing
? _('Update node group (%d)').format(count)
: _('Import node group (%d)').format(count);
importButton.disabled = count === 0 || !validName;
updateResultSummary();
};
const clearPreview = function() {
@@ -580,8 +625,8 @@ return view.extend({
};
const importDaed = function(items) {
let credentials;
let token = '';
let usedCachedToken = false;
const existingAirport = currentAirport();
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
const name = airportName.value.trim();
@@ -603,19 +648,21 @@ return view.extend({
const oldNodeIds = existingAirport ? existingAirport.node_ids : [];
const dropStage = function() { return fs.exec('/bin/rm', [ '-f', stageFile ]).catch(function() {}); };
const loadState = function(forceLogin) {
return requestDaedToken(endpoint, forceLogin).then(function(auth) {
token = auth.token;
usedCachedToken = auth.cached;
return graphQL(endpoint, 'query State{groups{id name nodes{id} subscriptions{subscription{id}}}}', {}, token);
});
};
return fs.write(stageFile, b64).then(function() {
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 State{groups{id name nodes{id} subscriptions{subscription{id}}}}', {}, token);
return loadState(false).catch(function(error) {
if (!usedCachedToken || !daedSession.isAccessDenied(error))
throw error;
daedSession.clear(window.localStorage);
return loadState(true);
});
}).then(function(dataValue) {
before = dataValue;
// drop this airport's previous subscription first: the new one
@@ -639,44 +686,27 @@ return view.extend({
const rows = result.importSubscription.nodeImportResult || [];
const failed = rows.filter(function(r) { return !!r.error && r.error !== 'node already exists'; }).length;
const oldGroup = airportSync.findManagedGroup(before.groups, existingAirport);
const ensureGroup = oldGroup
? Promise.resolve(oldGroup.id)
const oldManagedGroup = airportSync.findManagedGroup(before.groups, existingAirport);
const proxyGroup = airportSync.findGroupByName(before.groups, 'proxy');
const ensureGroup = proxyGroup
? Promise.resolve(proxyGroup.id)
: graphQL(endpoint,
'mutation CreateGroup($name:String!,$policy:Policy!){createGroup(name:$name,policy:$policy){id}}',
{ name: groupName, policy: 'min_moving_avg' }, token).then(function(value) {
{ name: 'proxy', policy: 'min_moving_avg' }, token).then(function(value) {
createdGroupId = value.createGroup.id;
return createdGroupId;
});
return ensureGroup.then(function(groupId) {
const rename = oldGroup && oldGroup.name !== groupName
? graphQL(endpoint, 'mutation Rename($id:ID!,$name:String!){renameGroup(id:$id,name:$name)}', { id: groupId, name: groupName }, token)
: Promise.resolve();
return rename.then(function() {
return graphQL(endpoint, 'mutation Pol($id:ID!,$policy:Policy!){groupSetPolicy(id:$id,policy:$policy)}', { id: groupId, policy: 'min_moving_avg' }, token);
}).then(function() {
// attach the whole subscription to the group
return graphQL(endpoint, 'mutation AddSubs($id:ID!,$ids:[ID!]!){groupAddSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: [ newSubId ] }, token);
}).then(function() {
return graphQL(endpoint, 'mutation AddSubs($id:ID!,$ids:[ID!]!){groupAddSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: [ newSubId ] }, token).then(function() {
groupReady = true;
// scrub the group of its previous members: old manual nodes
// (from the legacy importNodes flow) and any other subscription.
const oldMemberNodes = oldGroup ? (oldGroup.nodes || []).map(function(n) { return n.id; }) : [];
const oldGroupSubs = oldGroup
? (oldGroup.subscriptions || []).map(function(s) { return s.subscription && s.subscription.id; }).filter(function(id) { return id && id !== newSubId; })
: [];
const scrub = [];
if (oldMemberNodes.length)
scrub.push(graphQL(endpoint, 'mutation DelNodes($id:ID!,$nodeIDs:[ID!]!){groupDelNodes(id:$id,nodeIDs:$nodeIDs)}', { id: groupId, nodeIDs: oldMemberNodes }, token).catch(function() {}));
if (oldGroupSubs.length)
scrub.push(graphQL(endpoint, 'mutation DelSubs($id:ID!,$ids:[ID!]!){groupDelSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: oldGroupSubs }, token).catch(function() {}));
return Promise.all(scrub).then(function() {
// delete this airport's orphaned legacy manual nodes (the old
// subscription was already removed before the import).
if (!oldNodeIds.length)
return null;
return graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: oldNodeIds }, token).catch(function() {});
}).then(function() {
const cleanup = [];
// Migrate converter-managed airport groups to proxy, but never
// disturb proxy's existing nodes or other subscriptions.
if (oldManagedGroup && oldManagedGroup.id !== groupId)
cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: oldManagedGroup.id }, token).catch(function() {}));
if (oldNodeIds.length)
cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: oldNodeIds }, token).catch(function() {}));
return Promise.all(cleanup).then(function() {
writeAirportRecord(existingAirport, {
id: airportId,
backend: 'daed',
@@ -694,6 +724,8 @@ return view.extend({
});
});
}).catch(function(error) {
if (daedSession.isAccessDenied(error))
daedSession.clear(window.localStorage);
if (groupReady || !token || (!newSubId && !createdGroupId))
throw error;
const cleanup = [];
@@ -703,9 +735,6 @@ return view.extend({
cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: createdGroupId }, token));
return Promise.all(cleanup).catch(function() {}).then(function() { throw error; });
}).finally(function() {
if (credentials)
credentials.password = '';
credentials = null;
token = '';
return dropStage();
});
@@ -763,10 +792,10 @@ return view.extend({
resultBody
]),
E('div', { 'class': 'dd-card dd-conv-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('3. Import Node Group')),
E('p', { 'class': 'dd-settings-descr' }, _('Import the selected nodes as one named group, so large airport node lists stay easy to manage.')),
importTitle,
importDescription,
E('div', { 'class': 'dd-conv-import dd-conv-airport' }, [
E('label', {}, _('Group name')),
airportNameLabel,
airportName,
E('label', {}, _('Target backend')),
targetSelect
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require baseclass';
const TOKEN_KEY = 'luci-app-daede.daed-token';
function clear(storage) {
try { storage.removeItem(TOKEN_KEY); } catch (e) {}
}
function tokenExpiry(token) {
try {
const part = String(token || '').split('.')[1];
if (!part)
return 0;
const normalized = part.replace(/-/g, '+').replace(/_/g, '/');
const payload = JSON.parse(atob(normalized));
return Number(payload.exp) || 0;
} catch (e) {
return 0;
}
}
function load(storage, now) {
let token = '';
try { token = storage.getItem(TOKEN_KEY) || ''; } catch (e) { return ''; }
const current = now == null ? Date.now() / 1000 : Number(now);
if (!token || tokenExpiry(token) <= current + 30) {
clear(storage);
return '';
}
return token;
}
function save(storage, token) {
try { storage.setItem(TOKEN_KEY, String(token || '')); } catch (e) {}
}
function isAccessDenied(error) {
return /access denied|unauthori[sz]ed|invalid token/i.test(String(error && error.message || error || ''));
}
const api = {
TOKEN_KEY: TOKEN_KEY,
load: load,
save: save,
clear: clear,
isAccessDenied: isAccessDenied
};
if (typeof module !== 'undefined' && module.exports)
module.exports = api;
if (typeof baseclass !== 'undefined')
return baseclass.extend(api);
return api;
@@ -237,7 +237,12 @@ const CSS = [
'.dd-conv-submit{justify-content:flex-end}',
'.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-card{padding:14px}.dd-conv-heading{align-items:flex-start}.dd-conv-heading .dd-card-title{width:100%}.dd-conv-summary{justify-content:flex-start;width:100%;gap:5px 12px;padding-top:2px}.dd-conv-summary-item{min-width:74px}.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-submit .cbi-button{width:100%!important}.dd-conv-airport{grid-template-columns:82px minmax(0,1fr)}.dd-conv-airport>*{max-width:none !important}.dd-conv-url-row{grid-template-columns:1fr}.dd-conv-url-row input,.dd-conv-url-row .dd-conv-ua,.dd-conv-url-row .cbi-button{width:100%;max-width:none}}'
'.dd-daed-login{box-sizing:border-box;width:min(560px,100%);margin:0 auto}',
'.dd-daed-login .cbi-value{display:grid!important;grid-template-columns:110px minmax(0,1fr)!important;gap:12px;align-items:center;padding:6px 0!important}',
'.dd-daed-login .cbi-value-title,.dd-daed-login .cbi-value-field{box-sizing:border-box;width:auto!important;min-width:0!important;margin:0!important;padding:0!important}',
'.dd-daed-login .cbi-value-field input{box-sizing:border-box;width:100%!important;max-width:none!important;min-width:0!important;height:36px!important;margin:0!important;padding:6px 10px!important}',
'.dd-daed-login-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}',
'@media(max-width:640px){.dd-conv-card{padding:14px}.dd-conv-heading{align-items:flex-start}.dd-conv-heading .dd-card-title{width:100%}.dd-conv-summary{justify-content:flex-start;width:100%;gap:5px 12px;padding-top:2px}.dd-conv-summary-item{min-width:74px}.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-submit .cbi-button{width:100%!important}.dd-conv-airport{grid-template-columns:82px minmax(0,1fr)}.dd-conv-airport>*{max-width:none !important}.dd-conv-url-row{grid-template-columns:1fr}.dd-conv-url-row input,.dd-conv-url-row .dd-conv-ua,.dd-conv-url-row .cbi-button{width:100%;max-width:none}.dd-daed-login .cbi-value{grid-template-columns:1fr!important;gap:4px}.dd-daed-login-actions .cbi-button{flex:1 1 0}}'
].join('');
return baseclass.extend({ CSS: CSS });