feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement (#6488)

* feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement

* fix(sub): address PR review findings on routing parity, agent regex, and i18n
This commit is contained in:
Pejman Yousefi
2026-09-13 14:23:57 +03:30
committed by GitHub
parent c7518c4038
commit cba8f0672f
10 changed files with 306 additions and 166 deletions
@@ -51,55 +51,19 @@ export default function HappSettingsContent({
};
const handleBuildDeeplink = () => {
interface FieldRule {
type: string;
outboundTag: string;
domain?: string[];
ip?: string[];
network?: string;
}
const rules: FieldRule[] = [];
const profile = {
Name: 'Custom Rules',
GlobalProxy: 'true',
DirectSites: parseList(directDomains),
DirectIp: parseList(directIPs),
ProxySites: parseList(proxyDomains),
ProxyIp: parseList(proxyIPs),
BlockSites: parseList(blockDomains),
BlockIp: parseList(blockIPs),
DomainStrategy: 'IPIfNonMatch',
};
const bDom = parseList(blockDomains);
const bIp = parseList(blockIPs);
if (bDom.length > 0 || bIp.length > 0) {
rules.push({
type: 'field',
outboundTag: 'block',
...(bDom.length > 0 ? { domain: bDom } : {}),
...(bIp.length > 0 ? { ip: bIp } : {}),
});
}
const dDom = parseList(directDomains);
const dIp = parseList(directIPs);
if (dDom.length > 0 || dIp.length > 0) {
rules.push({
type: 'field',
outboundTag: 'direct',
...(dDom.length > 0 ? { domain: dDom } : {}),
...(dIp.length > 0 ? { ip: dIp } : {}),
});
}
const pDom = parseList(proxyDomains);
const pIp = parseList(proxyIPs);
if (pDom.length > 0 || pIp.length > 0) {
rules.push({
type: 'field',
outboundTag: 'proxy',
...(pDom.length > 0 ? { domain: pDom } : {}),
...(pIp.length > 0 ? { ip: pIp } : {}),
});
}
rules.push({
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
});
const deeplink = 'happ://routing/onadd/' + toBase64Utf8(JSON.stringify({ rules }));
const deeplink = 'happ://routing/onadd/' + toBase64Utf8(JSON.stringify(profile));
updateSetting({ subRoutingRules: deeplink });
setIsModalOpen(false);
message.success(t('pages.settings.subHappDeeplinkGenerated'));
@@ -107,6 +71,17 @@ export default function HappSettingsContent({
return (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappAutoDetect')}
description={t('pages.settings.subHappAutoDetectDesc')}
>
<Switch
checked={allSetting.subHappAutoDetect}
onChange={(v) => updateSetting({ subHappAutoDetect: v })}
/>
</SettingListItem>
<Tabs
type="card"
size="small"
@@ -454,11 +429,43 @@ export default function HappSettingsContent({
title={t('pages.settings.subHappColorProfile')}
description={t('pages.settings.subHappColorProfileDesc')}
>
<Input
value={allSetting.subHappColorProfile}
placeholder="default, violet, turquoise, cyberpunk, or custom JSON"
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
/>
<Space orientation="vertical" style={{ width: '100%' }}>
<Input
value={allSetting.subHappColorProfile}
placeholder='{"serverRowBackgroundColor":"#21003D67"} or resetcolors'
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
/>
<Space wrap size="small">
<Button
size="small"
onClick={() => updateSetting({ subHappColorProfile: 'resetcolors' })}
>
{t('reset')}
</Button>
<Button
size="small"
onClick={() =>
updateSetting({
subHappColorProfile:
'{"serverRowBackgroundColor":"#21003D67","cardBackgroundColor":"#120023B3"}',
})
}
>
Violet
</Button>
<Button
size="small"
onClick={() =>
updateSetting({
subHappColorProfile:
'{"serverRowBackgroundColor":"#002B3667","cardBackgroundColor":"#001F27B3"}',
})
}
>
Turquoise
</Button>
</Space>
</Space>
</SettingListItem>
</>
),
@@ -472,17 +479,6 @@ export default function HappSettingsContent({
),
children: (
<>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappAutoDetect')}
description={t('pages.settings.subHappAutoDetectDesc')}
>
<Switch
checked={allSetting.subHappAutoDetect}
onChange={(v) => updateSetting({ subHappAutoDetect: v })}
/>
</SettingListItem>
<SettingListItem
paddings="small"
title={t('pages.settings.subHappProviderId')}
+36 -60
View File
@@ -25,24 +25,15 @@ export function buildHappPresetDeeplink(preset: string): string {
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'direct',
domain: ['domain:ir', 'regexp:.*\\.ir$'],
ip: ['geoip:ir', 'geoip:private'],
},
{
type: 'field',
outboundTag: 'block',
domain: ['geosite:category-ads-all'],
},
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
Name: 'Iran Bypass',
GlobalProxy: 'true',
DirectSites: ['domain:ir', 'regexp:.*\\.ir$'],
DirectIp: ['geoip:ir', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
BlockSites: ['geosite:category-ads-all'],
BlockIp: [],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'IPIfNonMatch',
}),
)
);
@@ -51,24 +42,15 @@ export function buildHappPresetDeeplink(preset: string): string {
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'direct',
domain: ['domain:cn', 'geosite:cn'],
ip: ['geoip:cn', 'geoip:private'],
},
{
type: 'field',
outboundTag: 'block',
domain: ['geosite:category-ads-all'],
},
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
Name: 'China Direct',
GlobalProxy: 'true',
DirectSites: ['geosite:cn', 'geosite:geolocation-cn'],
DirectIp: ['geoip:cn', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
BlockSites: ['geosite:category-ads-all'],
BlockIp: [],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'IPIfNonMatch',
}),
)
);
@@ -77,23 +59,15 @@ export function buildHappPresetDeeplink(preset: string): string {
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'block',
domain: ['geosite:category-ads-all'],
},
{
type: 'field',
outboundTag: 'direct',
ip: ['geoip:private'],
},
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
Name: 'AdBlock',
GlobalProxy: 'true',
DirectSites: [],
DirectIp: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
BlockSites: ['geosite:category-ads-all'],
BlockIp: [],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'IPIfNonMatch',
}),
)
);
@@ -102,13 +76,15 @@ export function buildHappPresetDeeplink(preset: string): string {
'happ://routing/onadd/' +
toBase64Utf8(
JSON.stringify({
rules: [
{
type: 'field',
outboundTag: 'proxy',
network: 'tcp,udp',
},
],
Name: 'Global Proxy',
GlobalProxy: 'true',
DirectSites: [],
DirectIp: [],
BlockSites: [],
BlockIp: [],
ProxySites: [],
ProxyIp: [],
DomainStrategy: 'AsIs',
}),
)
);
+12 -22
View File
@@ -22,19 +22,11 @@ describe('Happ presets and helpers', () => {
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
expect(parsed).toHaveProperty('rules');
expect(Array.isArray(parsed.rules)).toBe(true);
const directRule = parsed.rules.find(
(r: { outboundTag: string }) => r.outboundTag === 'direct',
);
expect(directRule).toBeDefined();
expect(directRule.domain).toContain('domain:ir');
expect(directRule.ip).toContain('geoip:ir');
const blockRule = parsed.rules.find((r: { outboundTag: string }) => r.outboundTag === 'block');
expect(blockRule).toBeDefined();
expect(blockRule.domain).toContain('geosite:category-ads-all');
expect(parsed.Name).toBe('Iran Bypass');
expect(parsed.GlobalProxy).toBe('true');
expect(parsed.DirectSites).toContain('domain:ir');
expect(parsed.DirectIp).toContain('geoip:ir');
expect(parsed.BlockSites).toContain('geosite:category-ads-all');
});
it('generates valid base64 payload for china-direct preset', () => {
@@ -45,11 +37,9 @@ describe('Happ presets and helpers', () => {
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
const directRule = parsed.rules.find(
(r: { outboundTag: string }) => r.outboundTag === 'direct',
);
expect(directRule.domain).toContain('domain:cn');
expect(directRule.ip).toContain('geoip:cn');
expect(parsed.Name).toBe('China Direct');
expect(parsed.DirectSites).toContain('geosite:cn');
expect(parsed.DirectIp).toContain('geoip:cn');
});
it('generates valid base64 payload for adblock preset', () => {
@@ -58,8 +48,8 @@ describe('Happ presets and helpers', () => {
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
const blockRule = parsed.rules.find((r: { outboundTag: string }) => r.outboundTag === 'block');
expect(blockRule.domain).toContain('geosite:category-ads-all');
expect(parsed.Name).toBe('AdBlock');
expect(parsed.BlockSites).toContain('geosite:category-ads-all');
});
it('generates valid base64 payload for global preset', () => {
@@ -68,8 +58,8 @@ describe('Happ presets and helpers', () => {
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
expect(parsed.rules[0].outboundTag).toBe('proxy');
expect(parsed.rules[0].network).toBe('tcp,udp');
expect(parsed.Name).toBe('Global Proxy');
expect(parsed.DomainStrategy).toBe('AsIs');
});
it('encodes unicode properly via toBase64Utf8', () => {
@@ -69,4 +69,25 @@ describe('Happ TUN Mode select', () => {
const select = selectFor('TUN Mode');
expect(select.querySelector('.ant-select-content')?.textContent).toBe('Default');
});
it('renders Auto-Detection master switch at the top and triggers update', () => {
const updateSetting = vi.fn();
const allSetting = new AllSetting();
allSetting.subHappAutoDetect = false;
renderWithProviders(
<HappSettingsContent
allSetting={allSetting}
updateSetting={updateSetting}
isMobile={false}
remoteSourceBadge={() => null}
/>,
);
const switchBtn = document.querySelector('.ant-switch');
if (!switchBtn) throw new Error('switch not found');
fireEvent.click(switchBtn);
expect(updateSetting).toHaveBeenCalledWith({ subHappAutoDetect: true });
});
});