Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/app/createApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { normalizeRuntime } from '../runtime/runtimeConfig.js';
import { PREDEFINED_RULE_SETS, SING_BOX_CONFIG, SING_BOX_CONFIG_V1_11, generateSubconverterConfig } from '../config/index.js';

const DEFAULT_USER_AGENT = 'curl/7.74.0';
const VALID_COUNTRY_GROUP_TYPES = ['url-test', 'select', 'fallback'];
function parseCountryGroupType(raw) {
return VALID_COUNTRY_GROUP_TYPES.includes(raw) ? raw : 'url-test';
}

export function createApp(bindings = {}) {
const runtime = normalizeRuntime(bindings);
Expand Down Expand Up @@ -80,6 +84,7 @@ export function createApp(bindings = {}) {
const ua = c.req.query('ua') || getRequestHeader(c.req, 'User-Agent') || DEFAULT_USER_AGENT;
const groupByCountry = parseBooleanFlag(c.req.query('group_by_country'));
const includeAutoSelect = c.req.query('include_auto_select') !== 'false';
const countryGroupType = parseCountryGroupType(c.req.query('country_group_type'));
const enableClashUI = parseBooleanFlag(c.req.query('enable_clash_ui'));
const externalController = c.req.query('external_controller');
const externalUiDownloadUrl = c.req.query('external_ui_download_url');
Expand Down Expand Up @@ -111,7 +116,8 @@ export function createApp(bindings = {}) {
externalController,
externalUiDownloadUrl,
singboxConfigVersion,
includeAutoSelect
includeAutoSelect,
countryGroupType
);
await builder.build();
return c.json(builder.config);
Expand All @@ -132,6 +138,7 @@ export function createApp(bindings = {}) {
const ua = c.req.query('ua') || getRequestHeader(c.req, 'User-Agent') || DEFAULT_USER_AGENT;
const groupByCountry = parseBooleanFlag(c.req.query('group_by_country'));
const includeAutoSelect = c.req.query('include_auto_select') !== 'false';
const countryGroupType = parseCountryGroupType(c.req.query('country_group_type'));
const enableClashUI = parseBooleanFlag(c.req.query('enable_clash_ui'));
const externalController = c.req.query('external_controller');
const externalUiDownloadUrl = c.req.query('external_ui_download_url');
Expand All @@ -155,7 +162,8 @@ export function createApp(bindings = {}) {
enableClashUI,
externalController,
externalUiDownloadUrl,
includeAutoSelect
includeAutoSelect,
countryGroupType
);
await builder.build();
return c.text(builder.formatConfig(), 200, {
Expand All @@ -178,6 +186,7 @@ export function createApp(bindings = {}) {
const ua = c.req.query('ua') || getRequestHeader(c.req, 'User-Agent') || DEFAULT_USER_AGENT;
const groupByCountry = parseBooleanFlag(c.req.query('group_by_country'));
const includeAutoSelect = c.req.query('include_auto_select') !== 'false';
const countryGroupType = parseCountryGroupType(c.req.query('country_group_type'));
const configId = c.req.query('configId');
const lang = c.get('lang');

Expand All @@ -195,7 +204,8 @@ export function createApp(bindings = {}) {
lang,
ua,
groupByCountry,
includeAutoSelect
includeAutoSelect,
countryGroupType
);
builder.setSubscriptionUrl(c.req.url);
await builder.build();
Expand Down
3 changes: 2 additions & 1 deletion src/builders/BaseConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createTranslator } from '../i18n/index.js';
import { generateRules, getOutbounds, PREDEFINED_RULE_SETS } from '../config/index.js';

export class BaseConfigBuilder {
constructor(inputString, baseConfig, lang, userAgent, groupByCountry = false, includeAutoSelect = true) {
constructor(inputString, baseConfig, lang, userAgent, groupByCountry = false, includeAutoSelect = true, countryGroupType = 'url-test') {
this.inputString = inputString;
this.config = deepCopy(baseConfig);
this.customRules = [];
Expand All @@ -14,6 +14,7 @@ export class BaseConfigBuilder {
this.appliedOverrideKeys = new Set();
this.groupByCountry = groupByCountry;
this.includeAutoSelect = includeAutoSelect;
this.countryGroupType = countryGroupType;
this.providerUrls = []; // URLs to use as providers (auto-sync)
}

Expand Down
21 changes: 12 additions & 9 deletions src/builders/ClashConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ function supportsMrsFormat(userAgent) {
}

export class ClashConfigBuilder extends BaseConfigBuilder {
constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry = false, enableClashUI = false, externalController, externalUiDownloadUrl, includeAutoSelect = true) {
constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry = false, enableClashUI = false, externalController, externalUiDownloadUrl, includeAutoSelect = true, countryGroupType = 'url-test') {
if (!baseConfig) {
baseConfig = CLASH_CONFIG;
}
super(inputString, baseConfig, lang, userAgent, groupByCountry, includeAutoSelect);
super(inputString, baseConfig, lang, userAgent, groupByCountry, includeAutoSelect, countryGroupType);
this.selectedRules = selectedRules;
this.customRules = customRules;
this.countryGroupNames = [];
Expand Down Expand Up @@ -450,14 +450,17 @@ export class ClashConfigBuilder extends BaseConfigBuilder {
const groupName = `${emoji} ${name}`;
const norm = normalizeGroupName(groupName);
if (!existingNames.has(norm)) {
this.config['proxy-groups'].push({
const countryGroup = {
name: groupName,
type: 'url-test',
proxies: proxies,
url: 'https://www.gstatic.com/generate_204',
interval: 300,
lazy: false
});
type: this.countryGroupType,
proxies: proxies
};
if (countryGroup.type === 'url-test' || countryGroup.type === 'fallback') {
countryGroup.url = 'https://www.gstatic.com/generate_204';
countryGroup.interval = 300;
countryGroup.lazy = false;
}
this.config['proxy-groups'].push(countryGroup);
existingNames.add(norm);
}
countryGroupNames.push(groupName);
Expand Down
8 changes: 5 additions & 3 deletions src/builders/SingboxConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { buildSelectorMembers as buildSelectorMemberList, buildNodeSelectMembers
import { normalizeGroupName } from './helpers/groupNameUtils.js';

export class SingboxConfigBuilder extends BaseConfigBuilder {
constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry = false, enableClashUI = false, externalController, externalUiDownloadUrl, singboxVersion = '1.12', includeAutoSelect = true) {
constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry = false, enableClashUI = false, externalController, externalUiDownloadUrl, singboxVersion = '1.12', includeAutoSelect = true, countryGroupType = 'url-test') {
const resolvedBaseConfig = baseConfig ?? SING_BOX_CONFIG;
super(inputString, resolvedBaseConfig, lang, userAgent, groupByCountry, includeAutoSelect);
super(inputString, resolvedBaseConfig, lang, userAgent, groupByCountry, includeAutoSelect, countryGroupType);

this.selectedRules = selectedRules;
this.customRules = customRules;
Expand Down Expand Up @@ -276,9 +276,11 @@ export class SingboxConfigBuilder extends BaseConfigBuilder {
const groupName = `${emoji} ${name}`;
const norm = normalizeGroupName(groupName);
if (!existingTags.has(norm)) {
// sing-box has no native fallback type; urltest is the closest equivalent
const singboxType = this.countryGroupType === 'select' ? 'selector' : 'urltest';
this.config.outbounds.push({
tag: groupName,
type: 'urltest',
type: singboxType,
outbounds: countryProxies
});
existingTags.add(norm);
Expand Down
8 changes: 5 additions & 3 deletions src/builders/SurgeConfigBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { addProxyWithDedup } from './helpers/proxyHelpers.js';
import { buildSelectorMembers, buildNodeSelectMembers, uniqueNames } from './helpers/groupBuilder.js';

export class SurgeConfigBuilder extends BaseConfigBuilder {
constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry, includeAutoSelect = true) {
constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry, includeAutoSelect = true, countryGroupType = 'url-test') {
const resolvedBaseConfig = baseConfig ?? SURGE_CONFIG;
super(inputString, resolvedBaseConfig, lang, userAgent, groupByCountry, includeAutoSelect);
super(inputString, resolvedBaseConfig, lang, userAgent, groupByCountry, includeAutoSelect, countryGroupType);
this.selectedRules = selectedRules;
this.customRules = customRules;
this.subscriptionUrl = null;
Expand Down Expand Up @@ -343,8 +343,10 @@ export class SurgeConfigBuilder extends BaseConfigBuilder {
const groupName = `${emoji} ${name}`;
countryGroupNames.push(groupName);
if (!existing.has(groupName.trim())) {
const type = this.countryGroupType;
const extra = type === 'select' ? '' : ', url=https://www.gstatic.com/generate_204, interval=300';
this.config['proxy-groups'].push(
this.createProxyGroup(groupName, 'url-test', proxies, ', url=https://www.gstatic.com/generate_204, interval=300')
this.createProxyGroup(groupName, type, proxies, extra)
);
existing.add(groupName.trim());
}
Expand Down
22 changes: 16 additions & 6 deletions src/components/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,23 @@ export const Form = (props) => {
</h3>

<div class="space-y-4">
<label class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30 hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors cursor-pointer">
<span class="font-medium text-gray-700 dark:text-gray-300">{t('groupByCountry')}</span>
<div class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="groupByCountry" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
<div class="p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30">
<label class="flex items-center justify-between cursor-pointer">
<span class="font-medium text-gray-700 dark:text-gray-300">{t('groupByCountry')}</span>
<div class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="groupByCountry" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
</div>
</label>
<div x-show="groupByCountry" {...{'x-transition:enter': 'transition ease-out duration-200', 'x-transition:enter-start': 'opacity-0', 'x-transition:enter-end': 'opacity-100', 'x-transition:leave': 'transition ease-in duration-150', 'x-transition:leave-start': 'opacity-100', 'x-transition:leave-end': 'opacity-0'}} class="mt-2 flex items-center gap-2">
<span class="text-sm text-gray-500 dark:text-gray-400">{t('countryGroupType')}</span>
<select x-model="countryGroupType" class="text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-2 py-1">
<option value="url-test">{t('countryGroupTypeUrlTest')}</option>
<option value="select">{t('countryGroupTypeSelect')}</option>
<option value="fallback">{t('countryGroupTypeFallback')}</option>
</select>
</div>
</label>
</div>

<label class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-700/30 hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors cursor-pointer">
<span class="font-medium text-gray-700 dark:text-gray-300">{t('includeAutoSelect')}</span>
Expand Down
5 changes: 5 additions & 0 deletions src/components/formLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const formLogicFn = (t) => {
selectedPredefinedRule: 'balanced',
subconverterCopied: false,
groupByCountry: false,
countryGroupType: 'url-test',
includeAutoSelect: true,
enableClashUI: false,
externalController: '',
Expand Down Expand Up @@ -64,6 +65,7 @@ export const formLogicFn = (t) => {
this.input = localStorage.getItem('inputTextarea') || '';
this.showAdvanced = localStorage.getItem('advancedToggle') === 'true';
this.groupByCountry = localStorage.getItem('groupByCountry') === 'true';
this.countryGroupType = localStorage.getItem('countryGroupType') || 'url-test';
this.includeAutoSelect = localStorage.getItem('includeAutoSelect') !== 'false';
this.enableClashUI = localStorage.getItem('enableClashUI') === 'true';
this.externalController = localStorage.getItem('externalController') || '';
Expand Down Expand Up @@ -95,6 +97,7 @@ export const formLogicFn = (t) => {
});
this.$watch('showAdvanced', val => localStorage.setItem('advancedToggle', val));
this.$watch('groupByCountry', val => localStorage.setItem('groupByCountry', val));
this.$watch('countryGroupType', val => localStorage.setItem('countryGroupType', val));
this.$watch('includeAutoSelect', val => localStorage.setItem('includeAutoSelect', val));
this.$watch('enableClashUI', val => localStorage.setItem('enableClashUI', val));
this.$watch('externalController', val => localStorage.setItem('externalController', val));
Expand Down Expand Up @@ -303,6 +306,7 @@ export const formLogicFn = (t) => {
params.append('customRules', JSON.stringify(customRules));

if (this.groupByCountry) params.append('group_by_country', 'true');
if (this.groupByCountry && this.countryGroupType !== 'url-test') params.append('country_group_type', this.countryGroupType);
if (!this.includeAutoSelect) params.append('include_auto_select', 'false');
if (this.enableClashUI) params.append('enable_clash_ui', 'true');
if (this.externalController) params.append('external_controller', this.externalController);
Expand Down Expand Up @@ -545,6 +549,7 @@ export const formLogicFn = (t) => {

// Extract other parameters
this.groupByCountry = params.get('group_by_country') === 'true';
this.countryGroupType = params.get('country_group_type') || 'url-test';
this.includeAutoSelect = params.get('include_auto_select') !== 'false';
this.enableClashUI = params.get('enable_clash_ui') === 'true';

Expand Down
16 changes: 16 additions & 0 deletions src/i18n/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export const translations = {
generalSettings: '通用设置',
groupByCountry: '按国家分组',
groupByCountryTip: '仅 Clash/Surge/SingBox 生效',
countryGroupType: '分组策略',
countryGroupTypeUrlTest: '自动测速',
countryGroupTypeSelect: '手动选择',
countryGroupTypeFallback: '故障转移',
includeAutoSelect: '包含自动选择分组',
UASettings: '自定义UserAgent',
UAtip: '默认值curl/7.74.0',
Expand Down Expand Up @@ -291,6 +295,10 @@ export const translations = {
generalSettings: 'General Settings',
groupByCountry: 'Group by Country',
groupByCountryTip: 'Clash/Surge/SingBox only',
countryGroupType: 'Group Strategy',
countryGroupTypeUrlTest: 'Auto (url-test)',
countryGroupTypeSelect: 'Manual (select)',
countryGroupTypeFallback: 'Fallback',
includeAutoSelect: 'Include Auto Select Group',
UASettings: 'Custom UserAgent',
UAtip: 'By default it will use curl/7.74.0',
Expand Down Expand Up @@ -444,6 +452,10 @@ export const translations = {
generalSettings: 'تنظیمات عمومی',
groupByCountry: 'گروه‌بندی بر اساس کشور',
groupByCountryTip: 'فقط Clash/Surge/SingBox',
countryGroupType: 'استراتژی گروه',
countryGroupTypeUrlTest: 'خودکار (url-test)',
countryGroupTypeSelect: 'دستی (select)',
countryGroupTypeFallback: 'پشتیبان (fallback)',
includeAutoSelect: 'شامل گروه انتخاب خودکار',
UASettings: 'UserAgent سفارشی',
UAtip: 'به طور پیش‌فرض از curl/7.74.0 استفاده می‌کند',
Expand Down Expand Up @@ -597,6 +609,10 @@ export const translations = {
generalSettings: 'Общие настройки',
groupByCountry: 'Группировать по странам',
groupByCountryTip: 'Только для Clash/Surge/SingBox',
countryGroupType: 'Стратегия группы',
countryGroupTypeUrlTest: 'Авто (url-test)',
countryGroupTypeSelect: 'Вручную (select)',
countryGroupTypeFallback: 'Резервный (fallback)',
includeAutoSelect: 'Включить группу автовыбора',
UASettings: 'Пользовательский UserAgent',
UAtip: 'По умолчанию используется curl/7.74.0',
Expand Down
Loading