feat(happ): add routing editor, optional ad blocking, and LAN bypass preset (#6545)

* feat(happ): make ad blocking optional in routing presets

Add an independent AdBlock toggle for Iran, China, and global presets, applied only when generating routing rules. Update the China preset to Bypass-CN and cover preset behavior and localized controls.

* feat: add visual routing editor with JSON support and localization updates

- Implemented a new modal for editing routing profiles with basic and advanced tabs.
- Added functionality to load, parse, and generate routing profiles in JSON format.
- Enhanced user experience with validation and error handling for JSON input.
- Updated translations for Russian, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional) to include new routing editor terms.
- Created helper functions for managing routing profiles and generating deep links.
- Added unit tests for routing editor functionalities and JSON handling.

* feat: update routing editor to preserve null lists in profiles and enhance validation messages
This commit is contained in:
NgaiYeanCoi
2026-09-27 04:57:01 +08:00
committed by GitHub
parent 94fa317e76
commit 5e15120cec
22 changed files with 1363 additions and 209 deletions
+77 -19
View File
@@ -1,14 +1,8 @@
import { describe, expect, it } from 'vitest';
import { buildHappPresetDeeplink, parseList, toBase64Utf8 } from '@/pages/settings/happPresets';
import { buildHappPresetDeeplink, toBase64Utf8 } from '@/pages/settings/happPresets';
describe('Happ presets and helpers', () => {
it('correctly parses comma and newline separated lists', () => {
const raw = 'domain:ir\nregexp:.*\\.ir$\n, example.com, , google.com';
const parsed = parseList(raw);
expect(parsed).toEqual(['domain:ir', 'regexp:.*\\.ir$', 'example.com', 'google.com']);
});
it('generates valid happ://routing/off for off preset', () => {
const link = buildHappPresetDeeplink('off');
expect(link).toBe('happ://routing/off');
@@ -26,7 +20,7 @@ describe('Happ presets and helpers', () => {
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');
expect(parsed.BlockSites).toEqual([]);
});
it('generates valid base64 payload for china-direct preset', () => {
@@ -37,28 +31,92 @@ describe('Happ presets and helpers', () => {
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
expect(parsed.Name).toBe('China Direct');
expect(parsed.DirectSites).toContain('geosite:cn');
expect(parsed.DirectIp).toContain('geoip:cn');
expect(parsed).toEqual({
Name: 'Bypass-CN',
GlobalProxy: 'true',
RouteOrder: 'block-proxy-direct',
RemoteDNSType: 'DoH',
RemoteDNSDomain: 'https://cloudflare-dns.com/dns-query',
RemoteDNSIP: '1.1.1.1',
DomesticDNSType: 'DoH',
DomesticDNSDomain: 'https://dns.alidns.com/dns-query',
DomesticDNSIP: '223.5.5.5',
DnsHosts: {
'cloudflare-dns.com': '1.1.1.1',
'dns.alidns.com': '223.5.5.5',
},
DirectSites: ['geosite:private', 'geosite:cn', 'geosite:geolocation-cn'],
DirectIp: [
'geoip:cn',
'geoip:private',
'127.0.0.0/8',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'169.254.0.0/16',
'224.0.0.0/4',
'255.255.255.255',
],
ProxySites: [],
ProxyIp: [],
BlockSites: [],
BlockIp: [],
DomainStrategy: 'IPIfNonMatch',
FakeDNS: 'false',
UseChunkFiles: 'true',
});
});
it('generates valid base64 payload for adblock preset', () => {
const link = buildHappPresetDeeplink('adblock');
const b64 = link.replace('happ://routing/onadd/', '');
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
it.each(['iran-bypass', 'china-direct', 'global', 'lan-bypass'])(
'adds ad blocking only when opted in for %s without changing its routing',
(preset) => {
const decode = (link: string) => JSON.parse(atob(link.replace('happ://routing/onadd/', '')));
const base = decode(buildHappPresetDeeplink(preset));
const withAds = decode(buildHappPresetDeeplink(preset, true));
const withoutAds = decode(buildHappPresetDeeplink(preset, false));
expect(parsed.Name).toBe('AdBlock');
expect(parsed.BlockSites).toContain('geosite:category-ads-all');
expect(base.BlockSites).toEqual([]);
expect(withAds.BlockSites).toEqual(['geosite:category-ads-all']);
expect({ ...withAds, BlockSites: [] }).toEqual(base);
expect(withoutAds).toEqual(base);
},
);
it('keeps routing disabled even when ad blocking is selected', () => {
expect(buildHappPresetDeeplink('off', true)).toBe('happ://routing/off');
});
it('generates valid base64 payload for global preset', () => {
it.each(['adblock', 'unknown'])(
'does not generate a profile for unsupported preset %s',
(preset) => {
expect(buildHappPresetDeeplink(preset)).toBe('');
},
);
it('keeps the global preset without direct exceptions', () => {
const link = buildHappPresetDeeplink('global');
const b64 = link.replace('happ://routing/onadd/', '');
const jsonStr = atob(b64);
const parsed = JSON.parse(jsonStr);
expect(parsed.Name).toBe('Global Proxy');
expect(parsed.GlobalProxy).toBe('true');
expect(parsed.DirectSites).toEqual([]);
expect(parsed.DirectIp).toEqual([]);
expect(parsed.DomainStrategy).toBe('AsIs');
});
it('generates a separate LAN bypass preset with proxy as the default', () => {
const link = buildHappPresetDeeplink('lan-bypass');
const prefix = 'happ://routing/onadd/';
expect(link.startsWith(prefix)).toBe(true);
const parsed = JSON.parse(atob(link.slice(prefix.length)));
expect(parsed.Name).toBe('Global Bypass Local Network');
expect(parsed.GlobalProxy).toBe('true');
expect(parsed.DirectSites).toEqual(['geosite:private']);
expect(parsed.DirectIp).toEqual(['geoip:private']);
expect(parsed.BlockSites).toEqual([]);
expect(parsed.DomainStrategy).toBe('AsIs');
});