Skip to content

Commit f3f0b0a

Browse files
author
jiangwy
committed
feat: implement ClashClient for merging configurations and enhance proxy handling
1 parent f7dacb3 commit f3f0b0a

4 files changed

Lines changed: 171 additions & 67 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { ClashType } from '../../../types';
2+
import { fetchWithRetry } from 'cloudflare-tools';
3+
import { load } from 'js-yaml';
4+
5+
export class ClashClient {
6+
public async getConfig(urls: string[]): Promise<ClashType> {
7+
try {
8+
const configs = await Promise.all(urls.map(url => fetchWithRetry(url, { retries: 3 }).then(r => r.data.text())));
9+
const clashConfigs = configs.map(config => load(config) as ClashType);
10+
return this.mergeClashConfig(clashConfigs);
11+
} catch (error: any) {
12+
throw new Error(`Failed to get clash config: ${error.message || error}`);
13+
}
14+
}
15+
16+
/**
17+
* @description 合并配置
18+
* @param {ClashType[]} configs
19+
* @returns {ClashType} mergedConfig
20+
*/
21+
private mergeClashConfig(configs: ClashType[] = []): ClashType {
22+
try {
23+
if (!configs.length) {
24+
return {} as ClashType;
25+
}
26+
27+
const baseConfig = structuredClone(configs[0]);
28+
29+
// 如果只有一个配置,直接返回
30+
if (configs.length === 1) {
31+
return baseConfig;
32+
}
33+
34+
const mergedConfig: ClashType = {
35+
...baseConfig,
36+
proxies: baseConfig.proxies || [],
37+
'proxy-groups': baseConfig['proxy-groups'] || []
38+
};
39+
40+
// 预计算总代理数量
41+
const totalProxies = configs.reduce((total, config) => total + (config.proxies?.length || 0), 0);
42+
43+
// 使用 TypedArray 和 Set 提高性能
44+
const proxyIndices = new Int32Array(totalProxies);
45+
const existingProxies = new Set(baseConfig.proxies?.map(p => p.name));
46+
let proxyIndex = baseConfig.proxies?.length || 0;
47+
48+
// 使用 Map 存储代理组
49+
const groupMap = new Map(mergedConfig['proxy-groups'].map(group => [group.name, group]));
50+
51+
// 批量处理配置
52+
for (let i = 1; i < configs.length; i++) {
53+
const config = configs[i];
54+
55+
// 批量处理代理
56+
if (config.proxies?.length) {
57+
for (const proxy of config.proxies) {
58+
if (!existingProxies.has(proxy.name)) {
59+
mergedConfig.proxies[proxyIndex] = proxy;
60+
proxyIndices[proxyIndex] = proxyIndex;
61+
existingProxies.add(proxy.name);
62+
proxyIndex++;
63+
}
64+
}
65+
}
66+
67+
// 批量处理代理组
68+
if (config['proxy-groups']?.length) {
69+
for (const group of config['proxy-groups']) {
70+
const existingGroup = groupMap.get(group.name);
71+
72+
if (existingGroup) {
73+
// 使用 Set 优化代理列表去重
74+
const proxySet = new Set(existingGroup.proxies);
75+
for (const proxy of group.proxies || []) {
76+
proxySet.add(proxy);
77+
}
78+
existingGroup.proxies = Array.from(proxySet);
79+
80+
// 合并其他属性
81+
Object.assign(existingGroup, {
82+
...group,
83+
proxies: existingGroup.proxies
84+
});
85+
} else {
86+
mergedConfig['proxy-groups'].push(group);
87+
groupMap.set(group.name, group);
88+
}
89+
}
90+
}
91+
}
92+
93+
// 清理无效代理
94+
mergedConfig.proxies = mergedConfig.proxies.filter((_, i) => proxyIndices[i] !== -1);
95+
96+
return mergedConfig;
97+
} catch (error: any) {
98+
throw new Error(`Failed to merge clash config: ${error.message || error}`);
99+
}
100+
}
101+
}
102+

src/core/confuse/client/clash.ts

Lines changed: 66 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,39 @@ export class ClashClient {
77
try {
88
const configs = await Promise.all(urls.map(url => fetchWithRetry(url, { retries: 3 }).then(r => r.data.text())));
99
const clashConfigs = configs.map(config => load(config) as ClashType);
10-
return this.mergeClashConfig(clashConfigs);
10+
const mergedConfig = this.mergeClashConfig(clashConfigs);
11+
return mergedConfig;
1112
} catch (error: any) {
1213
throw new Error(`Failed to get clash config: ${error.message || error}`);
1314
}
1415
}
1516

17+
/**
18+
* 对比两个 proxies 数组是否完全相同
19+
* 使用 Set 对比,忽略顺序差异
20+
*/
21+
private isSameProxies(arr1: string[], arr2: string[]): boolean {
22+
if (arr1.length !== arr2.length) return false;
23+
const set1 = new Set(arr1);
24+
return arr2.every(item => set1.has(item));
25+
}
26+
27+
/**
28+
* 合并两个 proxies 数组,去重并保持顺序
29+
* 时间复杂度: O(n),使用 Set 优化查找
30+
*/
31+
private mergeGroupProxies(existing: string[], incoming: string[]): string[] {
32+
const seen = new Set(existing);
33+
const result = [...existing];
34+
for (const proxy of incoming) {
35+
if (!seen.has(proxy)) {
36+
seen.add(proxy);
37+
result.push(proxy);
38+
}
39+
}
40+
return result;
41+
}
42+
1643
/**
1744
* @description 合并配置
1845
* @param {ClashType[]} configs
@@ -24,74 +51,56 @@ export class ClashClient {
2451
return {} as ClashType;
2552
}
2653

27-
const baseConfig = structuredClone(configs[0]);
28-
2954
// 如果只有一个配置,直接返回
3055
if (configs.length === 1) {
31-
return baseConfig;
56+
return configs[0];
3257
}
3358

34-
const mergedConfig: ClashType = {
35-
...baseConfig,
36-
proxies: baseConfig.proxies || [],
37-
'proxy-groups': baseConfig['proxy-groups'] || []
38-
};
39-
40-
// 预计算总代理数量
41-
const totalProxies = configs.reduce((total, config) => total + (config.proxies?.length || 0), 0);
42-
43-
// 使用 TypedArray 和 Set 提高性能
44-
const proxyIndices = new Int32Array(totalProxies);
45-
const existingProxies = new Set(baseConfig.proxies?.map(p => p.name));
46-
let proxyIndex = baseConfig.proxies?.length || 0;
47-
48-
// 使用 Map 存储代理组
49-
const groupMap = new Map(mergedConfig['proxy-groups'].map(group => [group.name, group]));
50-
51-
// 批量处理配置
52-
for (let i = 1; i < configs.length; i++) {
53-
const config = configs[i];
54-
55-
// 批量处理代理
59+
// 合并 proxies: 直接展开所有配置的 proxies
60+
const mergedProxies: Array<Record<string, string>> = [];
61+
for (const config of configs) {
5662
if (config.proxies?.length) {
57-
for (const proxy of config.proxies) {
58-
if (!existingProxies.has(proxy.name)) {
59-
mergedConfig.proxies[proxyIndex] = proxy;
60-
proxyIndices[proxyIndex] = proxyIndex;
61-
existingProxies.add(proxy.name);
62-
proxyIndex++;
63-
}
64-
}
63+
mergedProxies.push(...config.proxies);
6564
}
65+
}
6666

67-
// 批量处理代理组
68-
if (config['proxy-groups']?.length) {
69-
for (const group of config['proxy-groups']) {
70-
const existingGroup = groupMap.get(group.name);
71-
72-
if (existingGroup) {
73-
// 使用 Set 优化代理列表去重
74-
const proxySet = new Set(existingGroup.proxies);
75-
for (const proxy of group.proxies || []) {
76-
proxySet.add(proxy);
77-
}
78-
existingGroup.proxies = Array.from(proxySet);
79-
80-
// 合并其他属性
81-
Object.assign(existingGroup, {
82-
...group,
83-
proxies: existingGroup.proxies
84-
});
85-
} else {
86-
mergedConfig['proxy-groups'].push(group);
87-
groupMap.set(group.name, group);
67+
// 合并 proxy-groups: 使用 Map 存储,O(1) 查找
68+
const groupMap = new Map<string, ClashType['proxy-groups'][0]>();
69+
const groupOrder: string[] = [];
70+
71+
for (const config of configs) {
72+
if (!config['proxy-groups']?.length) continue;
73+
74+
for (const group of config['proxy-groups']) {
75+
const existingGroup = groupMap.get(group.name);
76+
77+
if (!existingGroup) {
78+
// Map 中不存在该组名,直接添加
79+
groupMap.set(group.name, {
80+
...group,
81+
proxies: [...(group.proxies || [])] // 浅拷贝数组
82+
});
83+
groupOrder.push(group.name);
84+
} else {
85+
// Map 中已存在该组名,判断 proxies 是否相同
86+
const existingProxies = existingGroup.proxies || [];
87+
const incomingProxies = group.proxies || [];
88+
89+
if (!this.isSameProxies(existingProxies, incomingProxies)) {
90+
// proxies 不同,合并差异部分
91+
existingGroup.proxies = this.mergeGroupProxies(existingProxies, incomingProxies);
8892
}
93+
// proxies 相同则跳过,保持不变
8994
}
9095
}
9196
}
9297

93-
// 清理无效代理
94-
mergedConfig.proxies = mergedConfig.proxies.filter((_, i) => proxyIndices[i] !== -1);
98+
// 构建合并后的配置
99+
const mergedConfig: ClashType = {
100+
...configs[0], // 保留第一个配置的其他属性
101+
proxies: mergedProxies,
102+
'proxy-groups': groupOrder.map(name => groupMap.get(name)!)
103+
};
95104

96105
return mergedConfig;
97106
} catch (error: any) {

src/core/parser/protocol/vless.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ export class VlessParser extends Faker {
6969
if (proxy.network === 'ws') {
7070
proxy['ws-opts'] = {
7171
...proxy['ws-opts'],
72-
path: decodeURIComponent(this.originConfig.searchParams?.get('path') ?? '/'),
73-
headers: {
74-
...proxy['ws-opts'].headers,
75-
Host: this.originConfig.hostname
76-
}
72+
path: decodeURIComponent(this.originConfig.searchParams?.get('path') ?? '/')
7773
};
7874
}
7975
}

src/core/parser/protocol/vmess.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ export class VmessParser extends Faker {
6969
if (proxy.network === 'ws') {
7070
proxy['ws-opts'] = {
7171
...proxy['ws-opts'],
72-
path: this.originConfig.path,
73-
headers: {
74-
...proxy['ws-opts'].headers,
75-
Host: this.originConfig.add
76-
}
72+
path: this.originConfig.path
7773
};
7874
}
7975
}
@@ -144,3 +140,4 @@ export class VmessParser extends Faker {
144140
return this.#confuseConfig;
145141
}
146142
}
143+

0 commit comments

Comments
 (0)