diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index 61a10958..b51a7992 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -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) diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/airport-sync.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/airport-sync.js index 0fba0e58..7b1f00a3 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/airport-sync.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/airport-sync.js @@ -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 }; diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/clash-converter.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/clash-converter.js index c12d5d9c..27cf8e94 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/clash-converter.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/clash-converter.js @@ -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); diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js index 8577594d..8c48148b 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/converter.js @@ -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 diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/daed-session.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/daed-session.js new file mode 100644 index 00000000..f0996783 --- /dev/null +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/daed-session.js @@ -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; diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js index b13a46fe..fe66c78b 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/styles.js @@ -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 }); diff --git a/luci-app-daede/po/templates/daede.pot b/luci-app-daede/po/templates/daede.pot index 95677547..087c653f 100644 --- a/luci-app-daede/po/templates/daede.pot +++ b/luci-app-daede/po/templates/daede.pot @@ -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 diff --git a/luci-app-daede/po/zh-cn/daede.po b/luci-app-daede/po/zh-cn/daede.po index 3ab94412..7ac80335 100644 --- a/luci-app-daede/po/zh-cn/daede.po +++ b/luci-app-daede/po/zh-cn/daede.po @@ -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 "登录并导入" diff --git a/luci-app-daede/po/zh_Hans/daede.po b/luci-app-daede/po/zh_Hans/daede.po index 3ab94412..7ac80335 100644 --- a/luci-app-daede/po/zh_Hans/daede.po +++ b/luci-app-daede/po/zh_Hans/daede.po @@ -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 "登录并导入" diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 0b5f6481..39e3ab9a 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -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:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua index 5ad9444b..c2aaea13 100644 --- a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua +++ b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua @@ -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 diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index f7863103..87066bc1 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -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