Skip to content

Commit dfd7979

Browse files
kingparsa1010theking12bHidden-Node
authored
add support for bulk processing multiple VLESS and Trojan configurations (#3)
* feat: add support for bulk processing multiple VLESS and Trojan configurations * fix: only auto-fill server field for a single config in enhancer --------- Co-authored-by: theking12b <pars.atarkash10@gmail.com> Co-authored-by: Hidden-Node <mr.regista6@gmail.com>
1 parent 2e349ff commit dfd7979

4 files changed

Lines changed: 168 additions & 52 deletions

File tree

README-fa.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
### 🧬 Fragment + Fingerprint
1515
<ul dir="rtl" style="direction: rtl; text-align: right;">
16-
<li><b>جای‌گذاری و ارتقا</b>: لینک <code>vless://</code> یا <code>trojan://</code> را جای‌گذاری کنید و لینکی ارتقایافته با پارامترهای <code>cs</code>، <code>fm</code> و <code>fp</code> دریافت کنید.</li>
17-
<li><b>تغییر سرور</b>: فیلد سرور (IP/دامنه) به‌صورت خودکار از همان URL استخراج می‌شود و برای کاربر قابل ویرایش است (پشتیبانی از IPv4، IPv6 و دامنه).</li>
16+
<li><b>جای‌گذاری و ارتقا</b>: لینک تکی یا چندگانه (تک‌به‌تک در هر خط) از <code>vless://</code> یا <code>trojan://</code> را جای‌گذاری کنید و لینک‌های ارتقایافته با پارامترهای <code>cs</code>، <code>fm</code> و <code>fp</code> دریافت کنید.</li>
17+
<li><b>پشتیبانی از چندین کانفیگ همزمان</b>: امکان جای‌گذاری لیست انبوه کانفیگ‌ها جهت پردازش، اعتبارسنجی و ارتقای دسته‌جمعی بدون تداخل در remark کانفیگ‌ها.</li>
18+
<li><b>تغییر سرور</b>: فیلد سرور (IP/دامنه) فقط برای کانفیگ تکی به‌صورت خودکار از URL استخراج می‌شود و قابل ویرایش است. در حالت چند کانفیگ، این فیلد خالی می‌ماند و فقط اگر مقدار دلخواه وارد کنید روی همه کانفیگ‌ها اعمال می‌شود (پشتیبانی از IPv4، IPv6 و دامنه).</li>
1819
<li><b>اثر انگشت (Fingerprint)</b>: پیش‌فرض <code>unsafe</code> با گزینه‌های <code>chrome</code>، <code>firefox</code>، <code>safari</code>، <code>random</code> و <code>none</code>.</li>
1920
<li><b>آگاه از TLS</b>: پارامترهای <code>cs</code> و <code>fm</code> فقط زمانی اضافه می‌شوند که کانفیگ از امنیت <code>tls</code> استفاده کند. با خالی کردن هر فیلد، آن پارامتر حذف می‌شود.</li>
2021
<li><b>کپی با یک کلیک</b>: کپی مستقیم لینک ارتقایافته در کلیپ‌بورد.</li>

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ All processing happens in your browser. No data is sent to any server.
1212
## 🚀 Features
1313

1414
### 🧬 Fragment + Fingerprint
15-
- **Paste & Enhance**: Paste any `vless://` or `trojan://` URL and get an enhanced link with `cs`, `fm` and `fp` parameters added.
16-
- **Server Override**: The server (IP/domain) field is auto-filled from the pasted URL and is user-editable (supports IPv4, IPv6 and domains).
15+
- **Paste & Enhance**: Paste single or multiple `vless://` or `trojan://` URLs (one per line) and get enhanced links with `cs`, `fm` and `fp` parameters added.
16+
- **Multi-Config Support**: Paste bulk proxy URL lists to parse, validate, and batch enhance all URLs cleanly without remark collision.
17+
- **Server Override**: The server (IP/domain) field is auto-filled from the URL for a single config and is user-editable. With multiple configs the field stays empty and only applies to all configs if you type a custom value (supports IPv4, IPv6 and domains).
1718
- **Fingerprint**: Default `unsafe`, with `chrome`, `firefox`, `safari`, `random` and `none` options.
1819
- **TLS-aware**: `cs` and `fm` are only added when the config uses `tls` security. Clear a field to skip that parameter.
1920
- **One-click Copy**: Copy the enhanced URL straight to the clipboard.

index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ <h4>⚠️ Important Notes</h4>
375375
<div>
376376
<h2>Proxy URL Enhancer</h2>
377377
<p class="card-hint">Adds <code>cs</code> (cipher suites), <code>fm</code> (fragment mask)
378-
and <code>fp</code> to VLESS / Trojan URLs</p>
378+
and <code>fp</code> to VLESS / Trojan URLs (supports single or multiple URLs)</p>
379379
</div>
380380
</div>
381381
<div class="card-header-right">
@@ -384,7 +384,7 @@ <h2>Proxy URL Enhancer</h2>
384384
</div>
385385
<div class="input-group">
386386
<textarea id="enhancer-input" class="config-input"
387-
placeholder="Paste VLESS or Trojan URL here...&#10;e.g. vless://uuid@address:port?params#remark"
387+
placeholder="Paste VLESS or Trojan URL(s) here (one per line)...&#10;e.g. vless://uuid@address:port?params#remark"
388388
spellcheck="false"></textarea>
389389
<button class="btn-clear" id="enhancer-clear" title="Clear"></button>
390390
</div>
@@ -428,13 +428,13 @@ <h3 class="settings-title">⚙️ Enhancement Options</h3>
428428
<span class="btn-icon"></span>
429429
Enhance URL
430430
</button>
431-
<p class="generate-hint" id="enhance-hint">Paste a VLESS or Trojan URL above to enable</p>
431+
<p class="generate-hint" id="enhance-hint">Paste VLESS or Trojan URL(s) above to enable</p>
432432
</div>
433433

434434
<!-- Output -->
435435
<div class="output-section" id="enhancer-output-section" style="display:none;">
436436
<div class="output-header">
437-
<h3>📋 Enhanced URL</h3>
437+
<h3>📋 Enhanced URL(s)</h3>
438438
<div class="output-actions">
439439
<button class="btn-copy" id="btn-copy-enhancer" title="Copy to clipboard">
440440
<span class="copy-icon">📋</span> Copy
@@ -448,7 +448,7 @@ <h3>📋 Enhanced URL</h3>
448448
<details class="info-section">
449449
<summary>ℹ️ How the Enhancer Works</summary>
450450
<div class="info-content">
451-
<p>Paste a <strong>VLESS</strong> or <strong>Trojan</strong> URL and this tool adds:</p>
451+
<p>Paste <strong>VLESS</strong> or <strong>Trojan</strong> URL(s) (one per line) and this tool adds:</p>
452452
<ul>
453453
<li><strong>cs</strong> — TLS cipher suite list for the handshake.</li>
454454
<li><strong>fm</strong> — final mask / fragment settings (packet fragmentation for TLS

script.js

Lines changed: 157 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,15 @@
117117
}
118118

119119
// ===== URL Parser =====
120-
function parseProxyURL(raw) {
120+
function extractLines(raw) {
121+
if (!raw) return [];
122+
return raw
123+
.split(/[\r\n]+/)
124+
.map(line => line.trim())
125+
.filter(line => line.length > 0);
126+
}
127+
128+
function parseProxyURLSingle(raw) {
121129
const url = raw.trim();
122130
if (!url) return null;
123131

@@ -138,6 +146,19 @@
138146
return { error: 'Unknown protocol. Supported: vless, vmess, trojan, ss, socks, http' };
139147
}
140148

149+
function parseProxyURL(raw) {
150+
const lines = extractLines(raw);
151+
if (lines.length === 0) return null;
152+
if (lines.length === 1) return parseProxyURLSingle(lines[0]);
153+
154+
const parsedList = lines.map(line => parseProxyURLSingle(line));
155+
const validList = parsedList.filter(p => p && !p.error);
156+
if (validList.length > 0) {
157+
return validList;
158+
}
159+
return parsedList[0] || { error: 'Unknown protocol. Supported: vless, vmess, trojan, ss, socks, http' };
160+
}
161+
141162
// ===== SSH Parser (reads from form fields) =====
142163
function parseSSH(configNum) {
143164
const server = document.getElementById(`ssh-server${configNum}`).value.trim();
@@ -426,66 +447,123 @@
426447

427448
function onEnhancerInput() {
428449
const val = enhancerInput.value.trim();
429-
let parsed = null;
430-
let unsupported = false;
431-
if (val) {
432-
parsed = parseProxyURL(val);
433-
if (parsed && parsed.error) {
434-
parsed = null;
435-
}
436-
if (parsed && parsed.protocol !== 'vless' && parsed.protocol !== 'trojan') {
437-
unsupported = true;
438-
parsed = null;
439-
}
450+
const lines = extractLines(val);
451+
let parsedList = [];
452+
let unsupportedCount = 0;
453+
let invalidCount = 0;
454+
455+
if (lines.length > 0) {
456+
lines.forEach(line => {
457+
const p = parseProxyURLSingle(line);
458+
if (p && !p.error) {
459+
if (p.protocol === 'vless' || p.protocol === 'trojan') {
460+
parsedList.push(p);
461+
} else {
462+
unsupportedCount++;
463+
}
464+
} else {
465+
invalidCount++;
466+
}
467+
});
440468
}
441469

442-
// Auto-fill the server field from the URL, unless the user typed a custom value
443-
if (parsed) {
470+
// Auto-fill the server field from the URL — only for a single config.
471+
// With multiple configs, auto-filling from the first URL would silently
472+
// override every config's server with the first one's. A user-typed
473+
// custom value is kept: it acts as a deliberate override for all configs.
474+
if (parsedList.length === 1) {
475+
const firstServer = parsedList[0].server || '';
444476
const current = enhancerServer.value.trim();
445-
if (!current || current === lastAutoServer || current === parsed.server) {
446-
enhancerServer.value = parsed.server || '';
447-
lastAutoServer = parsed.server || '';
477+
if (!current || current === lastAutoServer || current === firstServer) {
478+
enhancerServer.value = firstServer;
479+
lastAutoServer = firstServer;
480+
}
481+
} else if (parsedList.length > 1) {
482+
const current = enhancerServer.value.trim();
483+
if (!current || current === lastAutoServer) {
484+
enhancerServer.value = '';
485+
lastAutoServer = '';
448486
}
449487
} else if (!enhancerServer.value.trim()) {
450488
lastAutoServer = '';
451489
}
452490

453-
renderParsedInfo(
454-
parsed
455-
? parsed
456-
: (val ? { error: unsupported ? 'Only VLESS and Trojan URLs are supported' : 'Failed to parse URL' } : null),
457-
enhancerParsed
458-
);
459-
updateProtocolTag(enhancerProtocolTag, parsed);
491+
if (lines.length > 0) {
492+
if (parsedList.length > 0) {
493+
renderParsedInfo(parsedList.length === 1 ? parsedList[0] : parsedList, enhancerParsed);
494+
const protocols = Array.from(new Set(parsedList.map(p => p.protocol)));
495+
if (protocols.length === 1) {
496+
updateProtocolTag(enhancerProtocolTag, parsedList[0]);
497+
} else {
498+
enhancerProtocolTag.textContent = 'MULTI';
499+
enhancerProtocolTag.classList.add('active');
500+
enhancerProtocolTag.style.color = '#7c5cff';
501+
enhancerProtocolTag.style.borderColor = '#7c5cff4d';
502+
enhancerProtocolTag.style.background = '#7c5cff1a';
503+
}
504+
} else {
505+
renderParsedInfo({
506+
error: unsupportedCount > 0
507+
? 'Only VLESS and Trojan URLs are supported'
508+
: 'Failed to parse URL'
509+
}, enhancerParsed);
510+
updateProtocolTag(enhancerProtocolTag, null);
511+
}
512+
} else {
513+
renderParsedInfo(null, enhancerParsed);
514+
updateProtocolTag(enhancerProtocolTag, null);
515+
}
460516

461517
enhancerCard.classList.remove('valid', 'invalid');
462-
if (val && parsed) {
518+
if (lines.length > 0 && parsedList.length > 0) {
463519
enhancerCard.classList.add('valid');
464-
} else if (val) {
520+
} else if (lines.length > 0) {
465521
enhancerCard.classList.add('invalid');
466522
}
467523

468-
btnEnhance.disabled = !parsed;
469-
enhanceHint.textContent = parsed
470-
? 'Ready to enhance!'
471-
: 'Paste a VLESS or Trojan URL above to enable';
472-
enhanceHint.style.color = parsed ? '#4cdf86' : '';
524+
const count = parsedList.length;
525+
btnEnhance.disabled = count === 0;
526+
btnEnhance.innerHTML = `<span class="btn-icon">✨</span> Enhance ${count > 1 ? count + ' URLs' : 'URL'}`;
527+
enhanceHint.textContent = count > 0
528+
? `Ready to enhance ${count} URL${count > 1 ? 's' : ''}!`
529+
: 'Paste VLESS or Trojan URL(s) above to enable';
530+
enhanceHint.style.color = count > 0 ? '#4cdf86' : '';
473531
}
474532

475533
function onEnhance() {
476-
const result = enhanceURL(enhancerInput.value);
477-
if (!result || result.error) {
534+
const val = enhancerInput.value.trim();
535+
const lines = extractLines(val);
536+
if (lines.length === 0) {
537+
enhancerOutputSection.style.display = 'none';
538+
return;
539+
}
540+
541+
const enhancedUrls = [];
542+
lines.forEach(line => {
543+
const p = parseProxyURLSingle(line);
544+
if (p && !p.error && (p.protocol === 'vless' || p.protocol === 'trojan')) {
545+
const res = enhanceURL(line);
546+
if (res && res.url) {
547+
enhancedUrls.push(res.url);
548+
}
549+
}
550+
});
551+
552+
if (enhancedUrls.length === 0) {
478553
enhancerOutputSection.style.display = 'none';
479554
return;
480555
}
481556

482-
const parsed = parseProxyURL(enhancerInput.value);
483-
const remark = parsed && parsed.remark
484-
? `✨ ${parsed.protocol.toUpperCase()} ${parsed.server}:${parsed.port} | enhanced`
485-
: '✨ Enhanced';
557+
const count = enhancedUrls.length;
558+
const firstParsed = parseProxyURLSingle(lines[0]);
559+
const remark = count === 1
560+
? (firstParsed && firstParsed.remark
561+
? `✨ ${firstParsed.protocol.toUpperCase()} ${firstParsed.server}:${firstParsed.port} | enhanced`
562+
: '✨ Enhanced')
563+
: `✨ Enhanced ${count} URLs`;
486564

487565
enhancerOutputRemark.textContent = remark;
488-
enhancerOutputUrl.textContent = result.url;
566+
enhancerOutputUrl.textContent = enhancedUrls.join('\n\n');
489567
enhancerOutputSection.style.display = 'block';
490568
enhancerOutputSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
491569
}
@@ -1379,6 +1457,33 @@
13791457
return;
13801458
}
13811459

1460+
if (Array.isArray(params)) {
1461+
if (params.length === 0) {
1462+
container.innerHTML = '';
1463+
return;
1464+
}
1465+
if (params.length === 1) {
1466+
renderParsedInfo(params[0], container);
1467+
return;
1468+
}
1469+
1470+
const rows = [];
1471+
rows.push(['Loaded Configs', `Found ${params.length} URLs`]);
1472+
params.forEach((cfg, i) => {
1473+
const label = `#${i + 1} ${cfg.protocol ? cfg.protocol.toUpperCase() : ''}`;
1474+
const val = `${cfg.server || ''}:${cfg.port || ''}${cfg.remark ? ' (' + truncateStr(cfg.remark, 30) + ')' : ''}`;
1475+
rows.push([label, val]);
1476+
});
1477+
1478+
container.innerHTML = rows.map(([label, value]) =>
1479+
`<div class="info-row">
1480+
<span class="info-label">${label}</span>
1481+
<span class="info-value">${escapeHtml(String(value))}</span>
1482+
</div>`
1483+
).join('');
1484+
return;
1485+
}
1486+
13821487
const rows = [];
13831488
rows.push(['Protocol', params.protocol.toUpperCase()]);
13841489
rows.push(['Server', params.server]);
@@ -1477,14 +1582,21 @@
14771582
parsed = val ? parseProxyURL(val) : null;
14781583
}
14791584

1585+
let singleConfig = null;
1586+
if (Array.isArray(parsed)) {
1587+
singleConfig = parsed[0];
1588+
} else if (parsed && !parsed.error) {
1589+
singleConfig = parsed;
1590+
}
1591+
14801592
if (isConfig1) {
1481-
parsedConfig1 = parsed && !parsed.error ? parsed : null;
1593+
parsedConfig1 = singleConfig;
14821594
} else {
1483-
parsedConfig2 = parsed && !parsed.error ? parsed : null;
1595+
parsedConfig2 = singleConfig;
14841596
}
14851597

14861598
renderParsedInfo(parsed, parsedContainer);
1487-
updateProtocolTag(protocolTag, parsed);
1599+
updateProtocolTag(protocolTag, singleConfig || (parsed && !Array.isArray(parsed) ? parsed : null));
14881600

14891601
// For SSH mode, we also consider the card valid if server is filled
14901602
card.classList.remove('valid', 'invalid');
@@ -1500,8 +1612,10 @@
15001612
}
15011613
} else {
15021614
const val = inputEl.value.trim();
1503-
if (val && parsed) {
1504-
card.classList.add(parsed.error ? 'invalid' : 'valid');
1615+
if (val && (singleConfig || (parsed && !parsed.error))) {
1616+
card.classList.add('valid');
1617+
} else if (val) {
1618+
card.classList.add('invalid');
15051619
}
15061620
}
15071621

0 commit comments

Comments
 (0)