🎄 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 d92edece61
commit ba56b2c37b
12 files changed
+295 -105

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.0 PKG_VERSION:=1.14.0
PKG_RELEASE:=18 PKG_RELEASE:=19
PKG_MAINTAINER:=kenzok8 PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -154,6 +154,10 @@ function findManagedGroup(groups, record) {
}) || null; }) || null;
} }
function findGroupByName(groups, name) {
return (groups || []).find(function(group) { return group.name === name; }) || null;
}
const api = { const api = {
deriveAirportName: deriveAirportName, deriveAirportName: deriveAirportName,
nextPastedName: nextPastedName, nextPastedName: nextPastedName,
@@ -167,6 +171,7 @@ const api = {
airportSectionValues: airportSectionValues, airportSectionValues: airportSectionValues,
matchAirport: matchAirport, matchAirport: matchAirport,
findManagedGroup: findManagedGroup, findManagedGroup: findManagedGroup,
findGroupByName: findGroupByName,
safeOldNodeIds: safeOldNodeIds safeOldNodeIds: safeOldNodeIds
}; };
@@ -225,7 +225,8 @@ function isMetadataProxy(node) {
if (!name) if (!name)
return false; 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*[::|]/i.test(name) || /^(?:官方网站|官网地址|官方网址|网站地址)\s*[::|]/i.test(name) ||
/^(?:加入|联系|客服)?\s*QQ\s*(?:群|交流群|客服|联系)\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.airport-sync as airportSync';
'require view.daede.backend as backend'; 'require view.daede.backend as backend';
'require view.daede.clash-converter as clashConverter'; 'require view.daede.clash-converter as clashConverter';
'require view.daede.daed-session as daedSession';
'require view.daede.styles as styles'; 'require view.daede.styles as styles';
const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh'; const FETCHER = '/usr/share/luci-app-daede/fetch-clash-yaml.sh';
@@ -125,16 +126,37 @@ function requestDaedCredentials() {
resolve(result); resolve(result);
}); });
ui.showModal(_('Sign in to daed'), [ ui.showModal(_('Sign in to daed'), [ E('div', { 'class': 'dd-daed-login' }, [
E('p', {}, _('Credentials are used only for this import and are not saved.')), 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' }, _('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': '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(); 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() { function applyUciChanges() {
return uci.save().then(function() { return uci.save().then(function() {
return uci.changes().then(function(changes) { return uci.changes().then(function(changes) {
@@ -227,6 +249,9 @@ return view.extend({
const targetSelect = E('select', { 'class': 'dd-conv-target' }); const targetSelect = E('select', { 'class': 'dd-conv-target' });
const airportName = E('input', { 'class': 'dd-conv-airport-name', 'placeholder': _('Group name'), 'autocomplete': 'off' }); 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 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 summary = E('div', { 'class': 'dd-conv-summary' });
const groupSummary = E('div', { 'class': 'dd-conv-group-summary' }); const groupSummary = E('div', { 'class': 'dd-conv-group-summary' });
const sourceStatus = E('div', { 'class': 'dd-conv-status' }, ''); 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() { const renderResults = function() {
while (resultBody.firstChild) while (resultBody.firstChild)
resultBody.removeChild(resultBody.firstChild); resultBody.removeChild(resultBody.firstChild);
@@ -286,7 +370,7 @@ return view.extend({
checkbox.disabled = !item.ok; checkbox.disabled = !item.ok;
checkbox.addEventListener('change', function() { checkbox.addEventListener('change', function() {
item.selected = checkbox.checked; item.selected = checkbox.checked;
renderResults(); updateResultSummary();
}); });
let resultText, resultClass; let resultText, resultClass;
@@ -313,46 +397,7 @@ return view.extend({
if (!shown.length) if (!shown.length)
resultBody.appendChild(E('div', { 'class': 'dd-conv-empty' }, _('No nodes to preview yet. Fetch or paste Clash YAML above.'))); 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; updateResultSummary();
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;
}; };
const clearPreview = function() { const clearPreview = function() {
@@ -580,8 +625,8 @@ return view.extend({
}; };
const importDaed = function(items) { const importDaed = function(items) {
let credentials;
let token = ''; let token = '';
let usedCachedToken = false;
const existingAirport = currentAirport(); const existingAirport = currentAirport();
const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId(); const airportId = existingAirport ? existingAirport.id : airportSync.makeAirportId();
const name = airportName.value.trim(); const name = airportName.value.trim();
@@ -603,19 +648,21 @@ return view.extend({
const oldNodeIds = existingAirport ? existingAirport.node_ids : []; const oldNodeIds = existingAirport ? existingAirport.node_ids : [];
const dropStage = function() { return fs.exec('/bin/rm', [ '-f', stageFile ]).catch(function() {}); }; 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 fs.write(stageFile, b64).then(function() {
return requestDaedCredentials(); return loadState(false).catch(function(error) {
}).then(function(value) { if (!usedCachedToken || !daedSession.isAccessDenied(error))
credentials = value; throw error;
return graphQL(endpoint, daedSession.clear(window.localStorage);
'query Login($username:String!,$password:String!){token(username:$username,password:$password)}', return loadState(true);
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);
}).then(function(dataValue) { }).then(function(dataValue) {
before = dataValue; before = dataValue;
// drop this airport's previous subscription first: the new one // drop this airport's previous subscription first: the new one
@@ -639,44 +686,27 @@ return view.extend({
const rows = result.importSubscription.nodeImportResult || []; const rows = result.importSubscription.nodeImportResult || [];
const failed = rows.filter(function(r) { return !!r.error && r.error !== 'node already exists'; }).length; const failed = rows.filter(function(r) { return !!r.error && r.error !== 'node already exists'; }).length;
const oldGroup = airportSync.findManagedGroup(before.groups, existingAirport); const oldManagedGroup = airportSync.findManagedGroup(before.groups, existingAirport);
const ensureGroup = oldGroup const proxyGroup = airportSync.findGroupByName(before.groups, 'proxy');
? Promise.resolve(oldGroup.id) const ensureGroup = proxyGroup
? Promise.resolve(proxyGroup.id)
: graphQL(endpoint, : graphQL(endpoint,
'mutation CreateGroup($name:String!,$policy:Policy!){createGroup(name:$name,policy:$policy){id}}', '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; createdGroupId = value.createGroup.id;
return createdGroupId; return createdGroupId;
}); });
return ensureGroup.then(function(groupId) { return ensureGroup.then(function(groupId) {
const rename = oldGroup && oldGroup.name !== groupName return graphQL(endpoint, 'mutation AddSubs($id:ID!,$ids:[ID!]!){groupAddSubscriptions(id:$id,subscriptionIDs:$ids)}', { id: groupId, ids: [ newSubId ] }, token).then(function() {
? 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() {
groupReady = true; groupReady = true;
// scrub the group of its previous members: old manual nodes const cleanup = [];
// (from the legacy importNodes flow) and any other subscription. // Migrate converter-managed airport groups to proxy, but never
const oldMemberNodes = oldGroup ? (oldGroup.nodes || []).map(function(n) { return n.id; }) : []; // disturb proxy's existing nodes or other subscriptions.
const oldGroupSubs = oldGroup if (oldManagedGroup && oldManagedGroup.id !== groupId)
? (oldGroup.subscriptions || []).map(function(s) { return s.subscription && s.subscription.id; }).filter(function(id) { return id && id !== newSubId; }) cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: oldManagedGroup.id }, token).catch(function() {}));
: []; if (oldNodeIds.length)
const scrub = []; cleanup.push(graphQL(endpoint, 'mutation Rm($ids:[ID!]!){removeNodes(ids:$ids)}', { ids: oldNodeIds }, token).catch(function() {}));
if (oldMemberNodes.length) return Promise.all(cleanup).then(function() {
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() {
writeAirportRecord(existingAirport, { writeAirportRecord(existingAirport, {
id: airportId, id: airportId,
backend: 'daed', backend: 'daed',
@@ -694,6 +724,8 @@ return view.extend({
}); });
}); });
}).catch(function(error) { }).catch(function(error) {
if (daedSession.isAccessDenied(error))
daedSession.clear(window.localStorage);
if (groupReady || !token || (!newSubId && !createdGroupId)) if (groupReady || !token || (!newSubId && !createdGroupId))
throw error; throw error;
const cleanup = []; const cleanup = [];
@@ -703,9 +735,6 @@ return view.extend({
cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: createdGroupId }, token)); cleanup.push(graphQL(endpoint, 'mutation RmG($id:ID!){removeGroup(id:$id)}', { id: createdGroupId }, token));
return Promise.all(cleanup).catch(function() {}).then(function() { throw error; }); return Promise.all(cleanup).catch(function() {}).then(function() { throw error; });
}).finally(function() { }).finally(function() {
if (credentials)
credentials.password = '';
credentials = null;
token = ''; token = '';
return dropStage(); return dropStage();
}); });
@@ -763,10 +792,10 @@ return view.extend({
resultBody resultBody
]), ]),
E('div', { 'class': 'dd-card dd-conv-card' }, [ E('div', { 'class': 'dd-card dd-conv-card' }, [
E('h4', { 'class': 'dd-card-title' }, _('3. Import Node Group')), importTitle,
E('p', { 'class': 'dd-settings-descr' }, _('Import the selected nodes as one named group, so large airport node lists stay easy to manage.')), importDescription,
E('div', { 'class': 'dd-conv-import dd-conv-airport' }, [ E('div', { 'class': 'dd-conv-import dd-conv-airport' }, [
E('label', {}, _('Group name')), airportNameLabel,
airportName, airportName,
E('label', {}, _('Target backend')), E('label', {}, _('Target backend')),
targetSelect 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-submit{justify-content:flex-end}',
'.dd-conv-import .cbi-button{margin-left:auto}', '.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}', '.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(''); ].join('');
return baseclass.extend({ CSS: CSS }); return baseclass.extend({ CSS: CSS });
+28 -1
View File
@@ -532,7 +532,34 @@ msgid "Sign in to daed"
msgstr "" msgstr ""
#: converter.js #: converter.js
msgid "Credentials are used only for this import and are not saved." msgid "The password is not saved. Sign-in is remembered in this browser for up to 30 days."
msgstr ""
msgid "3. Import Subscription"
msgstr ""
msgid "Import selected nodes as one subscription and add it to the default proxy group."
msgstr ""
msgid "Subscription name"
msgstr ""
msgid "Enter a subscription name to import the selected nodes."
msgstr ""
msgid "Subscription \"%s\" is available on daed. Select nodes above to update it."
msgstr ""
msgid "Update subscription \"%s\" with %d selected nodes in default group \"proxy\"."
msgstr ""
msgid "Import %d selected nodes as subscription \"%s\" into default group \"proxy\"."
msgstr ""
msgid "Update subscription (%d)"
msgstr ""
msgid "Import subscription (%d)"
msgstr "" msgstr ""
#: converter.js #: converter.js
+29 -2
View File
@@ -764,8 +764,35 @@ msgstr "已存在,跳过"
msgid "Sign in to daed" msgid "Sign in to daed"
msgstr "登录 daed" msgstr "登录 daed"
msgid "Credentials are used only for this import and are not saved." msgid "The password is not saved. Sign-in is remembered in this browser for up to 30 days."
msgstr "凭据仅用于本次导入,不会保存。" msgstr "不会保存密码。此浏览器将在 30 天内记住登录状态。"
msgid "3. Import Subscription"
msgstr "3. 导入订阅"
msgid "Import selected nodes as one subscription and add it to the default proxy group."
msgstr "将所选节点作为一条订阅导入,并加入默认 proxy 群组。"
msgid "Subscription name"
msgstr "订阅名称"
msgid "Enter a subscription name to import the selected nodes."
msgstr "请输入订阅名称以导入所选节点。"
msgid "Subscription \"%s\" is available on daed. Select nodes above to update it."
msgstr "daed 中已有订阅“%s”。请在上方选择节点以更新。"
msgid "Update subscription \"%s\" with %d selected nodes in default group \"proxy\"."
msgstr "更新订阅“%s”,使用 %d 个所选节点,并保留在默认 proxy 群组中。"
msgid "Import %d selected nodes as subscription \"%s\" into default group \"proxy\"."
msgstr "将 %d 个所选节点作为订阅“%s”导入默认 proxy 群组。"
msgid "Update subscription (%d)"
msgstr "更新订阅(%d)"
msgid "Import subscription (%d)"
msgstr "导入订阅(%d)"
msgid "Sign in and import" msgid "Sign in and import"
msgstr "登录并导入" msgstr "登录并导入"
+29 -2
View File
@@ -764,8 +764,35 @@ msgstr "已存在,跳过"
msgid "Sign in to daed" msgid "Sign in to daed"
msgstr "登录 daed" msgstr "登录 daed"
msgid "Credentials are used only for this import and are not saved." msgid "The password is not saved. Sign-in is remembered in this browser for up to 30 days."
msgstr "凭据仅用于本次导入,不会保存。" msgstr "不会保存密码。此浏览器将在 30 天内记住登录状态。"
msgid "3. Import Subscription"
msgstr "3. 导入订阅"
msgid "Import selected nodes as one subscription and add it to the default proxy group."
msgstr "将所选节点作为一条订阅导入,并加入默认 proxy 群组。"
msgid "Subscription name"
msgstr "订阅名称"
msgid "Enter a subscription name to import the selected nodes."
msgstr "请输入订阅名称以导入所选节点。"
msgid "Subscription \"%s\" is available on daed. Select nodes above to update it."
msgstr "daed 中已有订阅“%s”。请在上方选择节点以更新。"
msgid "Update subscription \"%s\" with %d selected nodes in default group \"proxy\"."
msgstr "更新订阅“%s”,使用 %d 个所选节点,并保留在默认 proxy 群组中。"
msgid "Import %d selected nodes as subscription \"%s\" into default group \"proxy\"."
msgstr "将 %d 个所选节点作为订阅“%s”导入默认 proxy 群组。"
msgid "Update subscription (%d)"
msgstr "更新订阅(%d)"
msgid "Import subscription (%d)"
msgstr "导入订阅(%d)"
msgid "Sign in and import" msgid "Sign in and import"
msgstr "登录并导入" msgstr "登录并导入"
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2 PKG_VERSION:=26.6.2
PKG_RELEASE:=160 PKG_RELEASE:=161
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -99,7 +99,9 @@ local function build_common(node)
elseif net == "http" then elseif net == "http" then
local opts = node["http-opts"] local opts = node["http-opts"]
if opts then if opts then
o.transport.host = get_first(opts.host) -- Clash http-opts carries the camouflage Host under headers.Host
-- (a list), like ws-opts; opts.host is not standard for http.
o.transport.host = get_first(opts.host) or (opts.headers and get_first(opts.headers.Host))
o.transport.path = get_first(opts.path) o.transport.path = get_first(opts.path)
end end
@@ -597,7 +597,16 @@ local function processData(szType, content, add_mode, group, sub_cfg)
elseif result.type == "Xray" and info.net == "tcp" then elseif result.type == "Xray" and info.net == "tcp" then
info.net = "raw" info.net = "raw"
end end
if info.net == 'h2' or info.net == 'http' then -- Clash 'network: http' = TCP + HTTP/1.1 header obfuscation (no TLS),
-- i.e. Xray raw transport with tcp header type "http" -- NOT xhttp
-- (splithttp). Normalize to 'raw' + guise 'http' so the tcp_guise
-- block below handles it; the server otherwise replies HTTP 400.
-- 'h2' still maps to xhttp (Xray dropped the standalone h2 transport).
if info.net == 'http' and result.type == "Xray" then
info.net = "raw"
info.type = "http"
result.transport = "raw"
elseif info.net == 'h2' or info.net == 'http' then
info.net = "http" info.net = "http"
result.transport = (result.type == "Xray") and "xhttp" or "http" result.transport = (result.type == "Xray") and "xhttp" or "http"
else else