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

This commit is contained in:
github-actions[bot] 2026-06-20 00:22:22 +08:00
parent d92edece61
commit ba56b2c37b
12 changed files with 295 additions and 105 deletions

View File

@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.0
PKG_RELEASE:=18
PKG_RELEASE:=19
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -532,7 +532,34 @@ msgid "Sign in to daed"
msgstr ""
#: 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 ""
#: converter.js

View File

@ -764,8 +764,35 @@ msgstr "已存在,跳过"
msgid "Sign in to daed"
msgstr "登录 daed"
msgid "Credentials are used only for this import and are not saved."
msgstr "凭据仅用于本次导入,不会保存。"
msgid "The password is not saved. Sign-in is remembered in this browser for up to 30 days."
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"
msgstr "登录并导入"

View File

@ -764,8 +764,35 @@ msgstr "已存在,跳过"
msgid "Sign in to daed"
msgstr "登录 daed"
msgid "Credentials are used only for this import and are not saved."
msgstr "凭据仅用于本次导入,不会保存。"
msgid "The password is not saved. Sign-in is remembered in this browser for up to 30 days."
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"
msgstr "登录并导入"

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2
PKG_RELEASE:=160
PKG_RELEASE:=161
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -99,7 +99,9 @@ local function build_common(node)
elseif net == "http" then
local opts = node["http-opts"]
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)
end

View File

@ -597,7 +597,16 @@ local function processData(szType, content, add_mode, group, sub_cfg)
elseif result.type == "Xray" and info.net == "tcp" then
info.net = "raw"
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"
result.transport = (result.type == "Xray") and "xhttp" or "http"
else